mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
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').
This commit is contained in:
@ -5,6 +5,9 @@
|
||||
// Root folder identifier (convert to null for API calls via toApiParentId)
|
||||
export const ROOT_ID = 'root' as const
|
||||
|
||||
// Drag type identifier for internal tree node dragging
|
||||
export const INTERNAL_NODE_DRAG_TYPE = 'application/x-dify-tree-node'
|
||||
|
||||
// Context menu trigger types (describes WHERE user clicked)
|
||||
export const CONTEXT_MENU_TYPE = {
|
||||
BLANK: 'blank',
|
||||
|
||||
@ -54,19 +54,22 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
||||
const isMutating = useIsMutating() > 0
|
||||
|
||||
const expandedFolderIds = useStore(s => s.expandedFolderIds)
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const dragOverFolderId = useStore(s => s.dragOverFolderId)
|
||||
const currentDragType = useStore(s => s.currentDragType)
|
||||
const searchTerm = useStore(s => s.fileTreeSearchTerm)
|
||||
const storeApi = useWorkflowStore()
|
||||
|
||||
const treeChildren = treeData?.children ?? emptyTreeNodes
|
||||
|
||||
const {
|
||||
handleRootDragEnter,
|
||||
handleRootDragLeave,
|
||||
handleRootDragOver,
|
||||
handleRootDrop,
|
||||
resetRootDragCounter,
|
||||
} = useRootFileDrop()
|
||||
|
||||
const expandedFolderIds = useStore(s => s.expandedFolderIds)
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const dragOverFolderId = useStore(s => s.dragOverFolderId)
|
||||
const searchTerm = useStore(s => s.fileTreeSearchTerm)
|
||||
const storeApi = useWorkflowStore()
|
||||
} = useRootFileDrop({ treeChildren })
|
||||
|
||||
// Root dropzone highlight (when dragging to root, not to a specific folder)
|
||||
const isRootDropzone = dragOverFolderId === ROOT_ID
|
||||
@ -76,7 +79,6 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
resetRootDragCounter()
|
||||
}, [dragOverFolderId, resetRootDragCounter])
|
||||
|
||||
const treeChildren = treeData?.children ?? emptyTreeNodes
|
||||
const {
|
||||
treeNodes,
|
||||
handleRename,
|
||||
@ -263,7 +265,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
</div>
|
||||
</div>
|
||||
{dragOverFolderId
|
||||
? <DragActionTooltip action="upload" />
|
||||
? <DragActionTooltip action={currentDragType ?? 'upload'} />
|
||||
: <DropTip />}
|
||||
<ArtifactsSection />
|
||||
<TreeContextMenu treeRef={treeRef} />
|
||||
|
||||
@ -4,7 +4,7 @@ import type { NodeRendererProps } from 'react-arborist'
|
||||
import type { TreeNodeData } from '../type'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -14,13 +14,17 @@ import {
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useFolderFileDrop } from '../hooks/use-folder-file-drop'
|
||||
import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree'
|
||||
import { useTreeNodeHandlers } from '../hooks/use-tree-node-handlers'
|
||||
import { useUnifiedDrag } from '../hooks/use-unified-drag'
|
||||
import NodeMenu from './node-menu'
|
||||
import TreeEditInput from './tree-edit-input'
|
||||
import TreeGuideLines from './tree-guide-lines'
|
||||
import { TreeNodeIcon } from './tree-node-icon'
|
||||
|
||||
const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>) => {
|
||||
const emptyTreeChildren: TreeNodeData[] = []
|
||||
|
||||
const TreeNode = ({ node, style }: NodeRendererProps<TreeNodeData>) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const isFolder = node.data.node_type === 'folder'
|
||||
const isSelected = node.isSelected
|
||||
@ -31,6 +35,10 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
|
||||
// Get tree data from TanStack Query cache (no extra request)
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
const treeChildren = useMemo(() => treeData?.children ?? emptyTreeChildren, [treeData?.children])
|
||||
|
||||
const {
|
||||
handleClick,
|
||||
handleDoubleClick,
|
||||
@ -39,7 +47,13 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
handleKeyDown,
|
||||
} = useTreeNodeHandlers({ node })
|
||||
|
||||
const { isDragOver, isBlinking, dragHandlers } = useFolderFileDrop(node)
|
||||
const { isDragOver, isBlinking, dragHandlers } = useFolderFileDrop({ node, treeChildren })
|
||||
const { handleNodeDragStart, handleNodeDragEnd } = useUnifiedDrag({ treeChildren })
|
||||
|
||||
// Currently only supports single node drag
|
||||
const handleDragStart = useCallback((e: React.DragEvent) => {
|
||||
handleNodeDragStart(e, node.data.id)
|
||||
}, [handleNodeDragStart, node.data.id])
|
||||
|
||||
const handleMoreClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@ -48,12 +62,14 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dragHandle}
|
||||
style={style}
|
||||
role="treeitem"
|
||||
tabIndex={0}
|
||||
aria-selected={isSelected}
|
||||
aria-expanded={isFolder ? node.isOpen : undefined}
|
||||
draggable={true}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleNodeDragEnd}
|
||||
className={cn(
|
||||
'group relative flex h-6 cursor-pointer items-center rounded-md px-2',
|
||||
'hover:bg-state-base-hover',
|
||||
|
||||
@ -34,6 +34,7 @@ export function useFileDrop() {
|
||||
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])
|
||||
|
||||
@ -41,6 +42,7 @@ export function useFileDrop() {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
storeApi.getState().setCurrentDragType(null)
|
||||
storeApi.getState().setDragOverFolderId(null)
|
||||
}, [storeApi])
|
||||
|
||||
@ -48,6 +50,7 @@ export function useFileDrop() {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
storeApi.getState().setCurrentDragType(null)
|
||||
storeApi.getState().setDragOverFolderId(null)
|
||||
|
||||
// Get files from dataTransfer, filter out directories (which have no type)
|
||||
|
||||
@ -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
|
||||
@ -24,12 +25,17 @@ type UseFolderFileDropReturn = {
|
||||
const BLINK_START_DELAY_MS = 1000
|
||||
const AUTO_EXPAND_DELAY_MS = 2000
|
||||
|
||||
export function useFolderFileDrop(node: NodeApi<TreeNodeData>): UseFolderFileDropReturn {
|
||||
type UseFolderFileDropOptions = {
|
||||
node: NodeApi<TreeNodeData>
|
||||
treeChildren: AppAssetTreeView[]
|
||||
}
|
||||
|
||||
export function useFolderFileDrop({ node, 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({ treeChildren })
|
||||
|
||||
const expandTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const blinkTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
@ -80,7 +86,7 @@ export function useFolderFileDrop(node: NodeApi<TreeNodeData>): UseFolderFileDro
|
||||
}, [clearExpandTimer])
|
||||
|
||||
const handleFolderDragEnter = useCallback((e: React.DragEvent) => {
|
||||
if (!isFolder || !isFileDrag(e))
|
||||
if (!isFolder || !isDragEvent(e))
|
||||
return
|
||||
dragCounterRef.current += 1
|
||||
if (dragCounterRef.current === 1)
|
||||
@ -88,13 +94,13 @@ export function useFolderFileDrop(node: NodeApi<TreeNodeData>): UseFolderFileDro
|
||||
}, [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)
|
||||
|
||||
119
web/app/components/workflow/skill/hooks/use-node-move.ts
Normal file
119
web/app/components/workflow/skill/hooks/use-node-move.ts
Normal file
@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
|
||||
// Internal tree node move handler (drag-and-drop within tree)
|
||||
|
||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
||||
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 { useMoveAppAssetNode } from '@/service/use-app-asset'
|
||||
import { INTERNAL_NODE_DRAG_TYPE, ROOT_ID } from '../constants'
|
||||
import { findNodeById, isDescendantOf, toApiParentId } from '../utils/tree-utils'
|
||||
|
||||
type NodeMoveTarget = {
|
||||
folderId: string | null
|
||||
isFolder: boolean
|
||||
}
|
||||
|
||||
type UseNodeMoveOptions = {
|
||||
treeChildren: AppAssetTreeView[]
|
||||
}
|
||||
|
||||
export function useNodeMove({ treeChildren }: UseNodeMoveOptions) {
|
||||
const { t } = useTranslation('workflow')
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id || ''
|
||||
const storeApi = useWorkflowStore()
|
||||
const moveNode = useMoveAppAssetNode()
|
||||
|
||||
const handleDragStart = useCallback((e: React.DragEvent, nodeId: string) => {
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
e.dataTransfer.setData(INTERNAL_NODE_DRAG_TYPE, nodeId)
|
||||
storeApi.getState().setCurrentDragType('move')
|
||||
}, [storeApi])
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
storeApi.getState().setCurrentDragType(null)
|
||||
storeApi.getState().setDragOverFolderId(null)
|
||||
}, [storeApi])
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent, target: NodeMoveTarget) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (!e.dataTransfer.types.includes(INTERNAL_NODE_DRAG_TYPE))
|
||||
return
|
||||
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
storeApi.getState().setDragOverFolderId(target.folderId ?? ROOT_ID)
|
||||
}, [storeApi])
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
storeApi.getState().setDragOverFolderId(null)
|
||||
}, [storeApi])
|
||||
|
||||
const handleDrop = useCallback(async (e: React.DragEvent, targetFolderId: string | null) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
storeApi.getState().setDragOverFolderId(null)
|
||||
storeApi.getState().setCurrentDragType(null)
|
||||
|
||||
const nodeId = e.dataTransfer.getData(INTERNAL_NODE_DRAG_TYPE)
|
||||
if (!nodeId)
|
||||
return
|
||||
|
||||
// Prevent dropping node into itself
|
||||
if (nodeId === targetFolderId) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.cannotMoveToSelf'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent circular move (dropping folder into its descendant)
|
||||
const draggedNode = findNodeById(treeChildren, nodeId)
|
||||
if (draggedNode?.node_type === 'folder' && targetFolderId) {
|
||||
if (isDescendantOf(targetFolderId, nodeId, treeChildren)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.cannotMoveToDescendant'),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await moveNode.mutateAsync({
|
||||
appId,
|
||||
nodeId,
|
||||
payload: { parent_id: toApiParentId(targetFolderId) },
|
||||
})
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('skillSidebar.menu.moved'),
|
||||
})
|
||||
}
|
||||
catch {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('skillSidebar.menu.moveError'),
|
||||
})
|
||||
}
|
||||
}, [appId, moveNode, t, storeApi, treeChildren])
|
||||
|
||||
return {
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
isMoving: moveNode.isPending,
|
||||
}
|
||||
}
|
||||
@ -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 }: UseRootFileDropOptions): UseRootFileDropReturn {
|
||||
const { handleDragOver, handleDragLeave, handleDrop } = useUnifiedDrag({ treeChildren })
|
||||
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)
|
||||
|
||||
62
web/app/components/workflow/skill/hooks/use-unified-drag.ts
Normal file
62
web/app/components/workflow/skill/hooks/use-unified-drag.ts
Normal file
@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
// Unified drag handler that routes to file upload or node move based on drag type
|
||||
|
||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
||||
import { useCallback } from 'react'
|
||||
import { getDragActionType, isFileDrag, isNodeDrag } from '../utils/drag-utils'
|
||||
import { useFileDrop } from './use-file-drop'
|
||||
import { useNodeMove } from './use-node-move'
|
||||
|
||||
type DragTarget = {
|
||||
folderId: string | null
|
||||
isFolder: boolean
|
||||
}
|
||||
|
||||
type UseUnifiedDragOptions = {
|
||||
treeChildren: AppAssetTreeView[]
|
||||
}
|
||||
|
||||
export function useUnifiedDrag({ treeChildren }: UseUnifiedDragOptions) {
|
||||
const fileDrop = useFileDrop()
|
||||
const nodeMove = useNodeMove({ treeChildren })
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent, target: DragTarget) => {
|
||||
const actionType = getDragActionType(e)
|
||||
if (actionType === 'upload') {
|
||||
fileDrop.handleDragOver(e, target)
|
||||
}
|
||||
else if (actionType === 'move') {
|
||||
nodeMove.handleDragOver(e, target)
|
||||
}
|
||||
}, [fileDrop, nodeMove])
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
if (isFileDrag(e)) {
|
||||
fileDrop.handleDragLeave(e)
|
||||
}
|
||||
else if (isNodeDrag(e)) {
|
||||
nodeMove.handleDragLeave(e)
|
||||
}
|
||||
}, [fileDrop, nodeMove])
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent, targetFolderId: string | null) => {
|
||||
if (isFileDrag(e)) {
|
||||
return fileDrop.handleDrop(e, targetFolderId)
|
||||
}
|
||||
else if (isNodeDrag(e)) {
|
||||
return nodeMove.handleDrop(e, targetFolderId)
|
||||
}
|
||||
}, [fileDrop, nodeMove])
|
||||
|
||||
return {
|
||||
handleDragOver,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
// Expose individual handlers for specific needs
|
||||
handleNodeDragStart: nodeMove.handleDragStart,
|
||||
handleNodeDragEnd: nodeMove.handleDragEnd,
|
||||
isUploading: fileDrop.isUploading,
|
||||
isMoving: nodeMove.isMoving,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,27 @@
|
||||
import type * as React from 'react'
|
||||
import { INTERNAL_NODE_DRAG_TYPE } from '../constants'
|
||||
|
||||
// Check if dragging external files from OS
|
||||
export const isFileDrag = (e: React.DragEvent): boolean => {
|
||||
return e.dataTransfer.types.includes('Files')
|
||||
}
|
||||
|
||||
// Check if dragging internal tree node
|
||||
export const isNodeDrag = (e: React.DragEvent): boolean => {
|
||||
return e.dataTransfer.types.includes(INTERNAL_NODE_DRAG_TYPE)
|
||||
}
|
||||
|
||||
// Check if any supported drag type
|
||||
export const isDragEvent = (e: React.DragEvent): boolean => {
|
||||
return isFileDrag(e) || isNodeDrag(e)
|
||||
}
|
||||
|
||||
// Get drag action type for tooltip display
|
||||
export type DragActionType = 'upload' | 'move'
|
||||
export const getDragActionType = (e: React.DragEvent): DragActionType | null => {
|
||||
if (isFileDrag(e))
|
||||
return 'upload'
|
||||
if (isNodeDrag(e))
|
||||
return 'move'
|
||||
return null
|
||||
}
|
||||
|
||||
@ -117,6 +117,19 @@ export function getAllDescendantFileIds(
|
||||
return fileIds
|
||||
}
|
||||
|
||||
export function isDescendantOf(
|
||||
potentialDescendantId: string | null | undefined,
|
||||
ancestorId: string | null | undefined,
|
||||
nodes: AppAssetTreeView[],
|
||||
): boolean {
|
||||
if (!potentialDescendantId || !ancestorId)
|
||||
return false
|
||||
if (potentialDescendantId === ancestorId)
|
||||
return true
|
||||
const ancestors = getAncestorIds(potentialDescendantId, nodes)
|
||||
return ancestors.includes(ancestorId)
|
||||
}
|
||||
|
||||
export function getTargetFolderIdFromSelection(
|
||||
selectedId: string | null,
|
||||
nodes: AppAssetTreeView[],
|
||||
|
||||
@ -97,6 +97,12 @@ export const createFileTreeSlice: StateCreator<
|
||||
set({ dragOverFolderId: folderId })
|
||||
},
|
||||
|
||||
currentDragType: null,
|
||||
|
||||
setCurrentDragType: (type) => {
|
||||
set({ currentDragType: type })
|
||||
},
|
||||
|
||||
fileTreeSearchTerm: '',
|
||||
|
||||
setFileTreeSearchTerm: (term) => {
|
||||
|
||||
@ -23,6 +23,8 @@ export type PendingCreateNode = {
|
||||
nodeType: 'file' | 'folder'
|
||||
}
|
||||
|
||||
export type DragActionType = 'upload' | 'move'
|
||||
|
||||
export type FileTreeSliceShape = {
|
||||
expandedFolderIds: Set<string>
|
||||
setExpandedFolderIds: (ids: Set<string>) => void
|
||||
@ -40,6 +42,8 @@ export type FileTreeSliceShape = {
|
||||
clearCreateNode: () => void
|
||||
dragOverFolderId: string | null
|
||||
setDragOverFolderId: (folderId: string | null) => void
|
||||
currentDragType: DragActionType | null
|
||||
setCurrentDragType: (type: DragActionType | null) => void
|
||||
fileTreeSearchTerm: string
|
||||
setFileTreeSearchTerm: (term: string) => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user