diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx index 9be54560ad..bae0ecdf18 100644 --- a/web/app/components/workflow/skill/file-tree/index.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -72,7 +72,7 @@ const FileTree: React.FC = ({ className }) => { handleRootDragOver, handleRootDrop, resetRootDragCounter, - } = useRootFileDrop() + } = useRootFileDrop({ treeChildren }) // Root dropzone highlight (when dragging to root, not to a specific folder) const isRootDropzone = dragOverFolderId === ROOT_ID @@ -200,8 +200,8 @@ const FileTree: React.FC = ({ className }) => { }, [treeChildren]) const renderTreeNode = useCallback((props: NodeRendererProps) => { - return - }, []) + return + }, [treeChildren]) useSyncTreeWithActiveTab({ treeRef, diff --git a/web/app/components/workflow/skill/file-tree/tree-node.tsx b/web/app/components/workflow/skill/file-tree/tree-node.tsx index 8896d9e6bd..ed71e357ea 100644 --- a/web/app/components/workflow/skill/file-tree/tree-node.tsx +++ b/web/app/components/workflow/skill/file-tree/tree-node.tsx @@ -20,9 +20,11 @@ import TreeEditInput from './tree-edit-input' import TreeGuideLines from './tree-guide-lines' import { TreeNodeIcon } from './tree-node-icon' -type TreeNodeProps = NodeRendererProps +type TreeNodeProps = NodeRendererProps & { + treeChildren: TreeNodeData[] +} -const TreeNode = ({ node, style, dragHandle }: TreeNodeProps) => { +const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => { const { t } = useTranslation('workflow') const isFolder = node.data.node_type === 'folder' const isSelected = node.isSelected @@ -75,7 +77,7 @@ const TreeNode = ({ node, style, dragHandle }: TreeNodeProps) => { } = useTreeNodeHandlers({ node }) // Get file drop visual state (for external file uploads) - const { isDragOver: isFileDragOver, isBlinking, dragHandlers } = useFolderFileDrop({ node }) + const { isDragOver: isFileDragOver, isBlinking, dragHandlers } = useFolderFileDrop({ node, treeChildren }) // Combine internal drag target (willReceiveDrop) with external file drag (isFileDragOver) const isDragOver = isFileDragOver || (isFolder && node.willReceiveDrop) diff --git a/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts b/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts index 678b872cf3..552c78a2ee 100644 --- a/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts +++ b/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts @@ -4,10 +4,11 @@ import type { NodeApi } from 'react-arborist' import type { TreeNodeData } from '../type' +import type { AppAssetTreeView } from '@/types/app-asset' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useStore } from '@/app/components/workflow/store' -import { isFileDrag } from '../utils/drag-utils' -import { useFileDrop } from './use-file-drop' +import { isDragEvent } from '../utils/drag-utils' +import { useUnifiedDrag } from './use-unified-drag' type UseFolderFileDropReturn = { isDragOver: boolean @@ -26,14 +27,15 @@ const AUTO_EXPAND_DELAY_MS = 2000 type UseFolderFileDropOptions = { node: NodeApi + treeChildren: AppAssetTreeView[] } -export function useFolderFileDrop({ node }: UseFolderFileDropOptions): UseFolderFileDropReturn { +export function useFolderFileDrop({ node, treeChildren: _treeChildren }: UseFolderFileDropOptions): UseFolderFileDropReturn { const isFolder = node.data.node_type === 'folder' const dragOverFolderId = useStore(s => s.dragOverFolderId) const isDragOver = isFolder && dragOverFolderId === node.data.id - const { handleDragOver, handleDrop } = useFileDrop() + const { handleDragOver, handleDrop } = useUnifiedDrag() const expandTimerRef = useRef(null) const blinkTimerRef = useRef(null) @@ -84,7 +86,7 @@ export function useFolderFileDrop({ node }: UseFolderFileDropOptions): UseFolder }, [clearExpandTimer]) const handleFolderDragEnter = useCallback((e: React.DragEvent) => { - if (!isFolder || !isFileDrag(e)) + if (!isFolder || !isDragEvent(e)) return dragCounterRef.current += 1 if (dragCounterRef.current === 1) @@ -92,13 +94,13 @@ export function useFolderFileDrop({ node }: UseFolderFileDropOptions): UseFolder }, [isFolder, scheduleAutoExpand]) const handleFolderDragOver = useCallback((e: React.DragEvent) => { - if (!isFolder || !isFileDrag(e)) + if (!isFolder || !isDragEvent(e)) return handleDragOver(e, { folderId: node.data.id, isFolder: true }) }, [handleDragOver, isFolder, node.data.id]) const handleFolderDragLeave = useCallback((e: React.DragEvent) => { - if (!isFolder || !isFileDrag(e)) + if (!isFolder || !isDragEvent(e)) return dragCounterRef.current = Math.max(dragCounterRef.current - 1, 0) if (dragCounterRef.current === 0) diff --git a/web/app/components/workflow/skill/hooks/use-root-file-drop.ts b/web/app/components/workflow/skill/hooks/use-root-file-drop.ts index f7bb8b4d72..3f5be0bfcd 100644 --- a/web/app/components/workflow/skill/hooks/use-root-file-drop.ts +++ b/web/app/components/workflow/skill/hooks/use-root-file-drop.ts @@ -2,9 +2,10 @@ // Root-level file drop handler with drag counter to handle nested DOM events +import type { AppAssetTreeView } from '@/types/app-asset' import { useCallback, useRef } from 'react' -import { isFileDrag } from '../utils/drag-utils' -import { useFileDrop } from './use-file-drop' +import { isDragEvent } from '../utils/drag-utils' +import { useUnifiedDrag } from './use-unified-drag' type UseRootFileDropReturn = { handleRootDragEnter: (e: React.DragEvent) => void @@ -14,12 +15,16 @@ type UseRootFileDropReturn = { resetRootDragCounter: () => void } -export function useRootFileDrop(): UseRootFileDropReturn { - const { handleDragOver, handleDragLeave, handleDrop } = useFileDrop() +type UseRootFileDropOptions = { + treeChildren: AppAssetTreeView[] +} + +export function useRootFileDrop({ treeChildren: _treeChildren }: UseRootFileDropOptions): UseRootFileDropReturn { + const { handleDragOver, handleDragLeave, handleDrop } = useUnifiedDrag() const dragCounterRef = useRef(0) const handleRootDragEnter = useCallback((e: React.DragEvent) => { - if (!isFileDrag(e)) + if (!isDragEvent(e)) return dragCounterRef.current += 1 }, []) @@ -29,7 +34,7 @@ export function useRootFileDrop(): UseRootFileDropReturn { }, [handleDragOver]) const handleRootDragLeave = useCallback((e: React.DragEvent) => { - if (!isFileDrag(e)) + if (!isDragEvent(e)) return dragCounterRef.current = Math.max(dragCounterRef.current - 1, 0) if (dragCounterRef.current === 0) diff --git a/web/app/components/workflow/skill/hooks/use-unified-drag.ts b/web/app/components/workflow/skill/hooks/use-unified-drag.ts new file mode 100644 index 0000000000..d4f967fb6d --- /dev/null +++ b/web/app/components/workflow/skill/hooks/use-unified-drag.ts @@ -0,0 +1,40 @@ +'use client' + +// Unified drag handler for external file uploads +// Internal node drag-move is now handled by react-arborist's built-in drag system + +import { useCallback } from 'react' +import { isFileDrag } from '../utils/drag-utils' +import { useFileDrop } from './use-file-drop' + +type DragTarget = { + folderId: string | null + isFolder: boolean +} + +export function useUnifiedDrag() { + const fileDrop = useFileDrop() + + // Only handle external file drags - internal node drags are handled by react-arborist + const handleDragOver = useCallback((e: React.DragEvent, target: DragTarget) => { + if (isFileDrag(e)) + fileDrop.handleDragOver(e, target) + }, [fileDrop]) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + if (isFileDrag(e)) + fileDrop.handleDragLeave(e) + }, [fileDrop]) + + const handleDrop = useCallback((e: React.DragEvent, targetFolderId: string | null) => { + if (isFileDrag(e)) + return fileDrop.handleDrop(e, targetFolderId) + }, [fileDrop]) + + return { + handleDragOver, + handleDragLeave, + handleDrop, + isUploading: fileDrop.isUploading, + } +}