Files
dify/web/app/components/workflow/skill/file-tree/index.tsx

185 lines
5.5 KiB
TypeScript

'use client'
import type { NodeApi, TreeApi } from 'react-arborist'
import type { TreeNodeData } from '../type'
import type { OpensObject } from '@/app/components/workflow/store/workflow/skill-editor/file-tree-slice'
import { RiDragDropLine } from '@remixicon/react'
import { useIsMutating } from '@tanstack/react-query'
import { useSize } from 'ahooks'
import * as React from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { Tree } from 'react-arborist'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { cn } from '@/utils/classnames'
import { useInlineCreateNode } from '../hooks/use-inline-create-node'
import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree'
import { useSyncTreeWithActiveTab } from '../hooks/use-sync-tree-with-active-tab'
import TreeContextMenu from './tree-context-menu'
import TreeNode from './tree-node'
type FileTreeProps = {
className?: string
searchTerm?: string
}
const emptyTreeNodes: TreeNodeData[] = []
const DropTip = () => {
const { t } = useTranslation('workflow')
return (
<div className="flex shrink-0 items-center justify-center gap-2 py-4 text-text-quaternary">
<RiDragDropLine className="size-4" />
<span className="system-xs-regular">
{t('skillSidebar.dropTip')}
</span>
</div>
)
}
const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
const { t } = useTranslation('workflow')
const treeRef = useRef<TreeApi<TreeNodeData>>(null)
const containerRef = useRef<HTMLDivElement>(null)
const containerSize = useSize(containerRef)
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
const isMutating = useIsMutating() > 0
const expandedFolderIds = useStore(s => s.expandedFolderIds)
const activeTabId = useStore(s => s.activeTabId)
const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId)
const storeApi = useWorkflowStore()
const treeChildren = treeData?.children ?? emptyTreeNodes
const {
treeNodes,
handleRename,
searchMatch,
hasPendingCreate,
} = useInlineCreateNode({
treeRef,
treeChildren,
})
const initialOpensObject = useMemo<OpensObject>(() => {
return Object.fromEntries(
[...expandedFolderIds].map(id => [id, true]),
)
}, [expandedFolderIds])
const handleToggle = useCallback((id: string) => {
storeApi.getState().toggleFolder(id)
}, [storeApi])
const handleActivate = useCallback((node: NodeApi<TreeNodeData>) => {
if (node.data.node_type === 'file')
storeApi.getState().openTab(node.data.id, { pinned: true })
else
node.toggle()
}, [storeApi])
const handleSelect = useCallback((nodes: NodeApi<TreeNodeData>[]) => {
if (activeTabId) {
storeApi.getState().setSelectedTreeNodeId(activeTabId)
return
}
const selectedId = nodes[0]?.id ?? null
storeApi.getState().setSelectedTreeNodeId(selectedId)
storeApi.getState().setCreateTargetNodeId(selectedId)
}, [activeTabId, storeApi])
const handleBlankAreaContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault()
storeApi.getState().setContextMenu({
top: e.clientY,
left: e.clientX,
type: 'blank',
})
}, [storeApi])
useSyncTreeWithActiveTab({
treeRef,
activeTabId,
})
if (isLoading) {
return (
<div className={cn('flex min-h-0 flex-1 items-center justify-center', className)}>
<Loading type="area" />
</div>
)
}
if (error) {
return (
<div className={cn('flex min-h-0 flex-1 flex-col items-center justify-center gap-2 text-text-tertiary', className)}>
<span className="system-xs-regular">
{t('skillSidebar.loadError')}
</span>
</div>
)
}
if (treeChildren.length === 0 && !hasPendingCreate) {
return (
<div className={cn('flex min-h-0 flex-1 flex-col', className)}>
<div className="flex flex-1 flex-col items-center justify-center gap-2 px-4 text-center">
<span className="system-xs-regular text-text-tertiary">
{t('skillSidebar.empty')}
</span>
</div>
<DropTip />
</div>
)
}
return (
<>
<div
className={cn(
'flex min-h-0 flex-1 flex-col',
isMutating && 'pointer-events-none opacity-50',
className,
)}
>
<div
ref={containerRef}
className="flex min-h-0 flex-1 flex-col overflow-hidden px-1 pt-1"
onContextMenu={handleBlankAreaContextMenu}
>
<Tree<TreeNodeData>
ref={treeRef}
data={treeNodes}
idAccessor="id"
childrenAccessor="children"
width="100%"
height={containerSize?.height ?? 400}
rowHeight={24}
indent={20}
overscanCount={5}
openByDefault={false}
selection={activeTabId ?? selectedTreeNodeId ?? undefined}
initialOpenState={initialOpensObject}
onToggle={handleToggle}
onSelect={handleSelect}
onActivate={handleActivate}
onRename={handleRename}
searchTerm={searchTerm}
searchMatch={searchMatch}
disableDrag
disableDrop
>
{TreeNode}
</Tree>
</div>
</div>
<DropTip />
<TreeContextMenu treeRef={treeRef} />
</>
)
}
export default React.memo(FileTree)