fix: improve note sorting behavior for drag and drop operations (#9971)

* fix: improve note sorting behavior for drag and drop operations

- Skip automatic sorting when performing same-level drag reordering
- Preserve treePath during same-level moves to maintain manual ordering
- Return special indicator for manual reorder operations to prevent conflicts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: type safety issue

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Pleasure1234 2025-09-06 23:15:51 +08:00 committed by GitHub
parent 2361c1b211
commit d09743d254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 18 deletions

View File

@ -563,8 +563,10 @@ const NotesPage: FC = () => {
const handleMoveNode = useCallback(
async (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => {
try {
await moveNode(sourceNodeId, targetNodeId, position)
await sortAllLevels(sortType)
const result = await moveNode(sourceNodeId, targetNodeId, position)
if (result.success && result.type !== 'manual_reorder') {
await sortAllLevels(sortType)
}
} catch (error) {
logger.error('Failed to move nodes:', error as Error)
}

View File

@ -19,6 +19,8 @@ const NOTES_TREE_ID = 'notes-tree-structure'
const logger = loggerService.withContext('NotesService')
export type MoveNodeResult = { success: false } | { success: true; type: 'file_system_move' | 'manual_reorder' }
/**
* /
*/
@ -182,7 +184,7 @@ export async function moveNode(
sourceNodeId: string,
targetNodeId: string,
position: 'before' | 'after' | 'inside'
): Promise<boolean> {
): Promise<MoveNodeResult> {
try {
const tree = await getNotesTree()
@ -192,19 +194,19 @@ export async function moveNode(
if (!sourceNode || !targetNode) {
logger.error(`Move nodes failed: node not found (source: ${sourceNodeId}, target: ${targetNodeId})`)
return false
return { success: false }
}
// 不允许文件夹被放入文件中
if (position === 'inside' && targetNode.type === 'file' && sourceNode.type === 'folder') {
logger.error('Move nodes failed: cannot move a folder inside a file')
return false
return { success: false }
}
// 不允许将节点移动到自身内部
if (position === 'inside' && isParentNode(tree, sourceNodeId, targetNodeId)) {
logger.error('Move nodes failed: cannot move a node inside itself or its descendants')
return false
return { success: false }
}
let targetPath: string = ''
@ -215,7 +217,7 @@ export async function moveNode(
targetPath = targetNode.externalPath
} else {
logger.error('Cannot move node inside a file node')
return false
return { success: false }
}
} else {
const targetParent = findParentNode(tree, targetNodeId)
@ -226,6 +228,20 @@ export async function moveNode(
}
}
// 检查是否为同级拖动排序
const sourceParent = findParentNode(tree, sourceNodeId)
const sourceDir = sourceParent ? sourceParent.externalPath : getFileDirectory(sourceNode.externalPath!)
const isSameLevelReorder = position !== 'inside' && sourceDir === targetPath
if (isSameLevelReorder) {
// 同级拖动排序:跳过文件系统操作,只更新树结构
logger.debug(`Same level reorder detected, skipping file system operations`)
const success = await moveNodeInTree(tree, sourceNodeId, targetNodeId, position)
// 返回一个特殊标识,告诉调用方这是手动排序,不需要重新自动排序
return success ? { success: true, type: 'manual_reorder' } : { success: false }
}
// 构建新的文件路径
const sourceName = sourceNode.externalPath!.split('/').pop()!
const sourceNameWithoutExt = sourceName.replace(sourceNode.type === 'file' ? MARKDOWN_EXT : '', '')
@ -250,14 +266,15 @@ export async function moveNode(
logger.debug(`Moved external ${sourceNode.type} to: ${newPath}`)
} catch (error) {
logger.error(`Failed to move external ${sourceNode.type}:`, error as Error)
return false
return { success: false }
}
}
return await moveNodeInTree(tree, sourceNodeId, targetNodeId, position)
const success = await moveNodeInTree(tree, sourceNodeId, targetNodeId, position)
return success ? { success: true, type: 'file_system_move' } : { success: false }
} catch (error) {
logger.error('Move nodes failed:', error as Error)
return false
return { success: false }
}
}

View File

@ -89,8 +89,9 @@ export async function moveNodeInTree(
return false
}
// 先保存源节点的副本,以防操作失败需要恢复(暂未实现恢复逻辑)
// const sourceNodeCopy = { ...sourceNode }
// 在移除节点之前先获取源节点的父节点信息,用于后续判断是否为同级排序
const sourceParent = findParentNode(tree, sourceNodeId)
const targetParent = findParentNode(tree, targetNodeId)
// 从原位置移除节点(不保存数据库,只在内存中操作)
const removed = removeNodeFromTreeInMemory(tree, sourceNodeId)
@ -110,7 +111,6 @@ export async function moveNodeInTree(
sourceNode.treePath = `${targetNode.treePath}/${sourceNode.name}`
} else {
const targetParent = findParentNode(tree, targetNodeId)
const targetList = targetParent ? targetParent.children! : tree
const targetIndex = targetList.findIndex((node) => node.id === targetNodeId)
@ -123,11 +123,16 @@ export async function moveNodeInTree(
const insertIndex = position === 'before' ? targetIndex : targetIndex + 1
targetList.splice(insertIndex, 0, sourceNode)
// 更新节点路径
if (targetParent) {
sourceNode.treePath = `${targetParent.treePath}/${sourceNode.name}`
} else {
sourceNode.treePath = `/${sourceNode.name}`
// 检查是否为同级排序,如果是则保持原有的 treePath
const isSameLevelReorder = sourceParent === targetParent
// 只有在跨级移动时才更新节点路径
if (!isSameLevelReorder) {
if (targetParent) {
sourceNode.treePath = `${targetParent.treePath}/${sourceNode.name}`
} else {
sourceNode.treePath = `/${sourceNode.name}`
}
}
}