Files
dify/web/app/components/workflow/skill/hooks/use-folder-file-drop.ts
yyh 3205f98d05 refactor(web): unify auto-expand trigger for drag-and-drop
Replace event-based auto-expand trigger with Zustand state-driven
approach. Now both external file uploads and internal node drag use
the same isDragOver state as the single source of truth for folder
auto-expand timing (1s blink, 2s expand).
2026-01-20 18:10:52 +08:00

111 lines
3.4 KiB
TypeScript

'use client'
// Folder node drop handler with VSCode-style blink animation and auto-expand
// Works for both external file uploads and internal node drag-and-drop
// Auto-expand is triggered by Zustand isDragOver state (single source of truth)
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 { isDragEvent } from '../utils/drag-utils'
import { useUnifiedDrag } from './use-unified-drag'
type UseFolderFileDropReturn = {
isDragOver: boolean
isBlinking: boolean
dragHandlers: {
onDragEnter: (e: React.DragEvent) => void
onDragOver: (e: React.DragEvent) => void
onDragLeave: (e: React.DragEvent) => void
onDrop: (e: React.DragEvent) => void
}
}
// Blink starts at 1s, folder expands at 2s
const BLINK_START_DELAY_MS = 1000
const AUTO_EXPAND_DELAY_MS = 2000
type UseFolderFileDropOptions = {
node: NodeApi<TreeNodeData>
treeChildren: AppAssetTreeView[]
}
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 } = useUnifiedDrag()
const dragCounterRef = useRef(0)
const [isBlinking, setIsBlinking] = useState(false)
// Auto-expand logic triggered by isDragOver state change (single source of truth)
// Works for both external file drag and internal node drag
useEffect(() => {
if (!isDragOver || node.isOpen)
return
// Start blinking after 1 second
const blinkTimer = setTimeout(() => {
setIsBlinking(true)
}, BLINK_START_DELAY_MS)
// Expand folder after 2 seconds
const expandTimer = setTimeout(() => {
setIsBlinking(false)
if (!node.isOpen)
node.open()
}, AUTO_EXPAND_DELAY_MS)
return () => {
clearTimeout(blinkTimer)
clearTimeout(expandTimer)
setIsBlinking(false)
}
}, [isDragOver, node.isOpen, node])
// dragEnter only used for drag counter (handles nested DOM events)
const handleFolderDragEnter = useCallback((e: React.DragEvent) => {
if (!isFolder || !isDragEvent(e))
return
dragCounterRef.current += 1
}, [isFolder])
const handleFolderDragOver = useCallback((e: React.DragEvent) => {
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 || !isDragEvent(e))
return
dragCounterRef.current = Math.max(dragCounterRef.current - 1, 0)
}, [isFolder])
const handleFolderDrop = useCallback((e: React.DragEvent) => {
if (!isFolder)
return
dragCounterRef.current = 0
handleDrop(e, node.data.id)
}, [handleDrop, isFolder, node.data.id])
const dragHandlers = useMemo(() => {
return {
onDragEnter: handleFolderDragEnter,
onDragOver: handleFolderDragOver,
onDragLeave: handleFolderDragLeave,
onDrop: handleFolderDrop,
}
}, [handleFolderDragEnter, handleFolderDragLeave, handleFolderDragOver, handleFolderDrop])
return {
isDragOver,
isBlinking,
dragHandlers,
}
}