Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(folders): implement folder tree structure with order management
  • Loading branch information
antonreshetov committed Feb 25, 2025
commit 8229c03d67d1d872db44abb711571be3a61125c8
11 changes: 11 additions & 0 deletions src/main/api/dto/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const foldersUpdate = t.Object({
defaultLanguage: t.String(),
parentId: t.Union([t.Number(), t.Null()]),
isOpen: t.Number({ minimum: 0, maximum: 1 }),
orderIndex: t.Number(),
})

const foldersItem = t.Object({
Expand All @@ -18,17 +19,27 @@ const foldersItem = t.Object({
createdAt: t.Number(),
updatedAt: t.Number(),
icon: t.Union([t.String(), t.Null()]),
parentId: t.Union([t.Number(), t.Null()]),
isOpen: t.Number(),
defaultLanguage: t.String(),
orderIndex: t.Number(),
})

const foldersItemWithChildren = t.Object({
...foldersItem.properties,
children: t.Array(foldersItem),
})

const foldersResponse = t.Array(foldersItem)
const foldersTreeResponse = t.Array(foldersItemWithChildren)

export const foldersDTO = new Elysia().model({
foldersAdd,
foldersResponse,
foldersUpdate,
foldersTreeResponse,
})

export type FoldersAdd = typeof foldersAdd.static
export type FoldersResponse = typeof foldersResponse.static
export type FoldersTree = typeof foldersTreeResponse.static
233 changes: 204 additions & 29 deletions src/main/api/routes/folders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FoldersResponse } from '../dto/folders'
import type { FoldersResponse, FoldersTree } from '../dto/folders'
import { Elysia } from 'elysia'
import { useDB } from '../../db'
import { commonAddResponse } from '../dto/common/response'
Expand All @@ -18,10 +18,12 @@ app
id,
name,
defaultLanguage,
parentId,
orderIndex,
isOpen,
icon,
createdAt,
updatedAt,
icon
updatedAt
FROM folders
ORDER BY createdAt DESC
`)
Expand All @@ -37,24 +39,94 @@ app
},
},
)
// Получение папок в виде древовидной структуры
.get(
'/tree',
() => {
const allFolders = db
.prepare(
`

const newOrder = maxOrder + 1

SELECT *
FROM folders
ORDER BY parentId, orderIndex
`,
)
.all() as FoldersTree

// Создаем карту для быстрого доступа к папкам по id
const folderMap = new Map()

allFolders.forEach((folder) => {
folder.children = []
folderMap.set(folder.id, folder)
})

const rootFolders: FoldersTree = []

allFolders.forEach((folder) => {
if (folder.parentId === null) {
rootFolders.push(folder)
}
else {
const parent = folderMap.get(folder.parentId)
if (parent) {
parent.children.push(folder)
}
}
})

return rootFolders
},
{
response: 'foldersTreeResponse',
detail: {
tags: ['Folders'],
},
},
)
// Добавление папки
.post(
'/',
({ body }) => {
const { name } = body
const now = Date.now()

const { maxOrder } = db
.prepare(
`
SELECT COALESCE(MAX(orderIndex), -1) as maxOrder
FROM folders
WHERE parentId IS NULL
`,
)
.get() as { maxOrder: number }

const newOrder = maxOrder + 1

const stmt = db.prepare(`
INSERT INTO folders (
name,
defaultLanguage,
parentId,
isOpen,
createdAt,
updatedAt
) VALUES (?, ?, ?, ?, ?)
updatedAt,
orderIndex
) VALUES (?, ?, ?, ?, ?, ?, ?)
`)

const { lastInsertRowid } = stmt.run(name, 'plain_text', 0, now, now)
const { lastInsertRowid } = stmt.run(
name,
'plain_text',
null,
0,
now,
now,
newOrder,
)

return { id: lastInsertRowid }
},
Expand All @@ -72,32 +144,135 @@ app
({ params, body }) => {
const now = Date.now()
const { id } = params
const { name, icon, defaultLanguage, parentId, isOpen } = body
const { name, icon, defaultLanguage, parentId, isOpen, orderIndex }
= body

const stmt = db.prepare(`
UPDATE folders
SET name = ?,
icon = ?,
defaultLanguage = ?,
isOpen = ?,
parentId = ?,
updatedAt = ?
WHERE id = ?
`)
const transaction = db.transaction(() => {
// Получаем текущую папку
const currentFolder = db
.prepare(
`
SELECT parentId, orderIndex
FROM folders
WHERE id = ?
`,
)
.get(id) as { parentId: number | null, orderIndex: number }

const { changes } = stmt.run(
name,
icon,
defaultLanguage,
isOpen,
parentId,
now,
id,
)
if (!currentFolder) {
throw new Error('Folder not found')
}

// Если изменился родитель или позиция
if (
parentId !== currentFolder.parentId
|| orderIndex !== currentFolder.orderIndex
) {
if (parentId === currentFolder.parentId) {
// Перемещение в пределах одного родителя
if (orderIndex > currentFolder.orderIndex) {
// Двигаем вниз - уменьшаем индексы папок между старой и новой позицией
db.prepare(
`
UPDATE folders
SET orderIndex = orderIndex - 1
WHERE parentId ${currentFolder.parentId === null ? 'IS NULL' : '= ?'}
AND orderIndex > ?
AND orderIndex <= ?
`,
).run(
...(currentFolder.parentId === null
? [currentFolder.orderIndex, orderIndex]
: [
currentFolder.parentId,
currentFolder.orderIndex,
orderIndex,
]),
)
}
else {
// Двигаем вверх - увеличиваем индексы папок между новой и старой позицией
db.prepare(
`
UPDATE folders
SET orderIndex = orderIndex + 1
WHERE parentId ${currentFolder.parentId === null ? 'IS NULL' : '= ?'}
AND orderIndex >= ?
AND orderIndex < ?
`,
).run(
...(currentFolder.parentId === null
? [orderIndex, currentFolder.orderIndex]
: [
currentFolder.parentId,
orderIndex,
currentFolder.orderIndex,
]),
)
}
}
else {
// Перемещение между разными родителями
// 1. Обновляем индексы в старом родителе
db.prepare(
`
UPDATE folders
SET orderIndex = orderIndex - 1
WHERE parentId ${currentFolder.parentId === null ? 'IS NULL' : '= ?'}
AND orderIndex > ?
`,
).run(
...(currentFolder.parentId === null
? [currentFolder.orderIndex]
: [currentFolder.parentId, currentFolder.orderIndex]),
)

if (!changes) {
throw new Error('Folder not found')
}
// 2. Обновляем индексы в новом родителе
db.prepare(
`
UPDATE folders
SET orderIndex = orderIndex + 1
WHERE parentId ${parentId === null ? 'IS NULL' : '= ?'}
AND orderIndex >= ?
`,
).run(
...(parentId === null ? [orderIndex] : [parentId, orderIndex]),
)
}
}

// Обновляем саму папку
const { changes } = db
.prepare(
`
UPDATE folders
SET name = ?,
icon = ?,
defaultLanguage = ?,
isOpen = ?,
parentId = ?,
orderIndex = ?,
updatedAt = ?
WHERE id = ?
`,
)
.run(
name,
icon,
defaultLanguage,
isOpen,
parentId,
orderIndex,
now,
id,
)

if (!changes) {
throw new Error('Folder not found')
}
})

transaction()

return { message: 'Folder updated' }
},
Expand Down
3 changes: 2 additions & 1 deletion src/main/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export function useDB() {
defaultLanguage TEXT NOT NULL,
parentId INTEGER,
isOpen INTEGER NOT NULL,
orderIndex INTEGER NOT NULL DEFAULT 0,
icon TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
icon TEXT,
FOREIGN KEY(parentId) REFERENCES folders(id)
)
`)
Expand Down
5 changes: 3 additions & 2 deletions src/main/db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { JSONDB } from './types'
export function migrateJsonToSqlite(jsonData: JSONDB, db: Database.Database) {
// Подготовленные выражения для вставки данных
const insertFolderStmt = db.prepare(`
INSERT INTO folders (name, defaultLanguage, parentId, isOpen, createdAt, updatedAt, icon)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO folders (name, defaultLanguage, parentId, isOpen, createdAt, updatedAt, icon, orderIndex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`)

const updateFolderParentStmt = db.prepare(`
Expand Down Expand Up @@ -49,6 +49,7 @@ export function migrateJsonToSqlite(jsonData: JSONDB, db: Database.Database) {
folder.createdAt,
folder.updatedAt,
folder.icon || null,
folder.index,
)
folderIdMap[folder.id] = Number(result.lastInsertRowid)
})
Expand Down
1 change: 1 addition & 0 deletions src/main/db/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface Folder {
parentId: string | null
isOpen: boolean
isSystem: boolean
index: number
createdAt: number
updatedAt: number
icon: string | null
Expand Down
Loading