fix: support dropping file tree nodes to root blank area

This commit is contained in:
yyh
2026-03-30 16:30:01 +08:00
parent 9ae94b3217
commit 09b3e53c43
2 changed files with 101 additions and 20 deletions

View File

@@ -37,6 +37,11 @@ type MockInlineCreateNodeResult = {
type MockTreeApi = {
deselectAll: () => void
dragNode?: {
id: string
parent: { id: string, isRoot?: boolean } | null
} | null
get: (id: string | null) => { id: string, children?: Array<{ id: string }> } | undefined
state: {
nodes: {
drag: {
@@ -89,6 +94,12 @@ function createNode(overrides: Partial<AppAssetTreeView> = {}): AppAssetTreeView
function createTreeApiMock(): MockTreeApi {
return {
deselectAll: vi.fn(),
dragNode: null,
get: vi.fn((id: string | null) => {
if (id === 'root')
return undefined
return id ? { id, children: [] } : undefined
}),
state: {
nodes: {
drag: {
@@ -440,6 +451,25 @@ describe('FileTree', () => {
expect(mocks.rootDropHandlers.handleRootDragLeave).toHaveBeenCalledTimes(1)
expect(mocks.rootDropHandlers.handleRootDrop).toHaveBeenCalledTimes(1)
})
it('should commit internal move when dropping in root blank area', () => {
mocks.treeApi.dragDestinationIndex = 2
mocks.treeApi.dragNode = {
id: 'file-1',
parent: { id: 'folder-1', isRoot: false },
}
mocks.treeApi.state.nodes.drag = {
id: 'file-1',
destinationParentId: 'root',
destinationIndex: 2,
}
render(<FileTree />)
fireEvent.drop(getTreeDropZone())
expect(mocks.executeMoveNode).toHaveBeenCalledWith('file-1', null)
})
})
describe('Tree callbacks', () => {

View File

@@ -41,6 +41,13 @@ type DragInsertTarget = {
parentId: string | null
index: number
}
type CommitInternalMoveArgs = {
nodeId: string
draggedNode: NodeApi<TreeNodeData>
parentId: string | null
index: number
parentNode?: NodeApi<TreeNodeData> | null
}
const normalizeParentId = (node: NodeApi<TreeNodeData> | null | undefined) => {
if (!node || node.isRoot)
@@ -185,6 +192,35 @@ const FileTree = ({ className }: FileTreeProps) => {
const { executeMoveNode } = useNodeMove()
const { executeReorderNode } = useNodeReorder()
const commitInternalMove = useCallback(({
nodeId,
draggedNode,
parentId,
index,
parentNode,
}: CommitInternalMoveArgs) => {
const tree = treeRef.current
const destinationIndex = tree?.dragDestinationIndex
const isInsertLine = destinationIndex !== null && destinationIndex !== undefined
const targetParentId = parentId ?? null
const sourceParentId = normalizeParentId(draggedNode.parent)
if (isInsertLine && sourceParentId === targetParentId) {
const siblingIds = getSiblingIds(parentNode, tree)
const afterNodeId = getAfterNodeIdForReorder(
siblingIds,
nodeId,
destinationIndex ?? index,
)
if (afterNodeId !== undefined) {
executeReorderNode(nodeId, afterNodeId)
return
}
}
executeMoveNode(nodeId, targetParentId)
}, [executeMoveNode, executeReorderNode])
const syncDragInsertTarget = useCallback(() => {
const tree = treeRef.current
if (!tree)
@@ -231,28 +267,43 @@ const FileTree = ({ className }: FileTreeProps) => {
if (!nodeId || !draggedNode)
return
commitInternalMove({
nodeId,
draggedNode,
parentId,
index,
parentNode,
})
}, [commitInternalMove])
const handleTreeDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
handleRootDrop(e)
const tree = treeRef.current
const destinationIndex = tree?.dragDestinationIndex
const isInsertLine = destinationIndex !== null && destinationIndex !== undefined
const targetParentId = parentId ?? null
const sourceParentId = normalizeParentId(draggedNode.parent)
const dragTarget = dragInsertTargetRef.current
if (!tree || !dragTarget)
return
if (isInsertLine && sourceParentId === targetParentId) {
const siblingIds = getSiblingIds(parentNode, tree)
const afterNodeId = getAfterNodeIdForReorder(
siblingIds,
nodeId,
destinationIndex ?? index,
)
if (afterNodeId !== undefined) {
executeReorderNode(nodeId, afterNodeId)
return
}
}
const targetElement = e.target as HTMLElement | null
if (targetElement?.closest('[role="treeitem"]'))
return
// parentId from react-arborist is null for root, otherwise folder ID
executeMoveNode(nodeId, targetParentId)
}, [executeMoveNode, executeReorderNode, treeRef])
const draggedNode = tree.dragNode
if (!draggedNode)
return
const parentNode = dragTarget.parentId === null
? tree.root
: tree.get(dragTarget.parentId)
commitInternalMove({
nodeId: draggedNode.id,
draggedNode,
parentId: dragTarget.parentId,
index: dragTarget.index,
parentNode,
})
}, [commitInternalMove, handleRootDrop])
// react-arborist disableDrop callback - returns true to prevent drop
const handleDisableDrop = useCallback((args: {
@@ -387,7 +438,7 @@ const FileTree = ({ className }: FileTreeProps) => {
onDragEnter={handleRootDragEnter}
onDragOver={handleRootDragOver}
onDragLeave={handleRootDragLeave}
onDrop={handleRootDrop}
onDrop={handleTreeDrop}
>
<Tree<TreeNodeData>
ref={treeRef}