Files
dify/web/app/components/workflow/skill/hooks/use-file-drop.ts
yyh b527921f3f feat: unified drag-and-drop for skill file tree
Implement unified drag system that supports both internal node moves
and external file uploads with consistent UI feedback. Uses native
HTML5 drag API with shared visual states (isDragOver, isBlinking,
DragActionTooltip showing 'Move to' or 'Upload to').
2026-01-20 18:09:08 +08:00

112 lines
3.1 KiB
TypeScript

'use client'
// Base drag-and-drop handler for file uploads
// Used by use-root-file-drop and use-folder-file-drop
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import Toast from '@/app/components/base/toast'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useCreateAppAssetFile } from '@/service/use-app-asset'
import { ROOT_ID } from '../constants'
type FileDropTarget = {
folderId: string | null
isFolder: boolean
}
export function useFileDrop() {
const { t } = useTranslation('workflow')
const appDetail = useAppStore(s => s.appDetail)
const appId = appDetail?.id || ''
const storeApi = useWorkflowStore()
const createFile = useCreateAppAssetFile()
const handleDragOver = useCallback((e: React.DragEvent, target: FileDropTarget) => {
e.preventDefault()
e.stopPropagation()
// Only handle file drops from the system (not internal tree drags)
if (!e.dataTransfer.types.includes('Files'))
return
e.dataTransfer.dropEffect = 'copy'
// Use ROOT_ID to indicate dragging over root (to distinguish from null = "not dragging")
storeApi.getState().setCurrentDragType('upload')
storeApi.getState().setDragOverFolderId(target.folderId ?? ROOT_ID)
}, [storeApi])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
storeApi.getState().setCurrentDragType(null)
storeApi.getState().setDragOverFolderId(null)
}, [storeApi])
const handleDrop = useCallback(async (e: React.DragEvent, targetFolderId: string | null) => {
e.preventDefault()
e.stopPropagation()
storeApi.getState().setCurrentDragType(null)
storeApi.getState().setDragOverFolderId(null)
// Get files from dataTransfer, filter out directories (which have no type)
const items = Array.from(e.dataTransfer.items || [])
const files: File[] = []
for (const item of items) {
if (item.kind === 'file') {
const entry = item.webkitGetAsEntry?.()
// Skip directories - they have isDirectory = true
if (entry?.isDirectory) {
Toast.notify({
type: 'error',
message: t('skillSidebar.menu.folderDropNotSupported'),
})
continue
}
const file = item.getAsFile()
if (file)
files.push(file)
}
}
if (files.length === 0)
return
try {
await Promise.all(
files.map(file =>
createFile.mutateAsync({
appId,
name: file.name,
file,
parentId: targetFolderId,
}),
),
)
Toast.notify({
type: 'success',
message: t('skillSidebar.menu.filesUploaded', { count: files.length }),
})
}
catch {
Toast.notify({
type: 'error',
message: t('skillSidebar.menu.uploadError'),
})
}
}, [appId, createFile, t, storeApi])
return {
handleDragOver,
handleDragLeave,
handleDrop,
isUploading: createFile.isPending,
}
}