mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
- Extract useFileOperations hook to hooks/use-file-operations.ts - Move tree utilities to utils/tree-utils.ts - Move file utilities to utils/file-utils.ts (renamed from utils.ts) - Remove unnecessary JSDoc comments throughout components - Simplify type.ts to only contain local type definitions - Clean up store/index.ts by removing verbose comments
167 lines
4.9 KiB
TypeScript
167 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import type { NodeApi, TreeApi } from 'react-arborist'
|
|
import type { TreeNodeData } from './type'
|
|
import { RiDragDropLine } from '@remixicon/react'
|
|
import * as React from 'react'
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
import { Tree } from 'react-arborist'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
|
import Loading from '@/app/components/base/loading'
|
|
import Toast from '@/app/components/base/toast'
|
|
import { useGetAppAssetTree, useRenameAppAssetNode } from '@/service/use-app-asset'
|
|
import { cn } from '@/utils/classnames'
|
|
import FileTreeContextMenu from './file-tree-context-menu'
|
|
import FileTreeNode from './file-tree-node'
|
|
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
|
import { getAncestorIds, toOpensObject } from './utils/tree-utils'
|
|
|
|
type FilesProps = {
|
|
className?: string
|
|
}
|
|
|
|
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 Files: React.FC<FilesProps> = ({ className }) => {
|
|
const { t } = useTranslation('workflow')
|
|
const treeRef = useRef<TreeApi<TreeNodeData>>(null)
|
|
|
|
const appDetail = useAppStore(s => s.appDetail)
|
|
const appId = appDetail?.id || ''
|
|
|
|
const { data: treeData, isLoading, error } = useGetAppAssetTree(appId)
|
|
|
|
const expandedFolderIds = useSkillEditorStore(s => s.expandedFolderIds)
|
|
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
|
const storeApi = useSkillEditorStoreApi()
|
|
|
|
const renameNode = useRenameAppAssetNode()
|
|
|
|
const initialOpenState = useMemo(() => toOpensObject(expandedFolderIds), [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)
|
|
else
|
|
node.toggle()
|
|
}, [storeApi])
|
|
|
|
const handleRename = useCallback(({ id, name }: { id: string, name: string }) => {
|
|
renameNode.mutateAsync({
|
|
appId,
|
|
nodeId: id,
|
|
payload: { name },
|
|
}).catch(() => {
|
|
Toast.notify({
|
|
type: 'error',
|
|
message: t('skillSidebar.menu.renameError'),
|
|
})
|
|
})
|
|
}, [appId, renameNode, t])
|
|
|
|
useEffect(() => {
|
|
if (!activeTabId || !treeData?.children || !treeRef.current)
|
|
return
|
|
|
|
const ancestors = getAncestorIds(activeTabId, treeData.children)
|
|
|
|
if (ancestors.length > 0)
|
|
storeApi.getState().revealFile(ancestors)
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
const tree = treeRef.current
|
|
if (!tree)
|
|
return
|
|
|
|
for (const ancestorId of ancestors) {
|
|
const ancestorNode = tree.get(ancestorId)
|
|
if (ancestorNode && !ancestorNode.isOpen)
|
|
ancestorNode.open()
|
|
}
|
|
|
|
const node = tree.get(activeTabId)
|
|
if (node)
|
|
node.select()
|
|
}, 0)
|
|
|
|
return () => clearTimeout(timeoutId)
|
|
}, [activeTabId, treeData?.children, storeApi])
|
|
|
|
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 (!treeData?.children || treeData.children.length === 0) {
|
|
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', className)}>
|
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-1 pt-1">
|
|
<Tree<TreeNodeData>
|
|
ref={treeRef}
|
|
data={treeData.children}
|
|
idAccessor="id"
|
|
childrenAccessor="children"
|
|
width="100%"
|
|
height={1000}
|
|
rowHeight={24}
|
|
indent={20}
|
|
overscanCount={5}
|
|
initialOpenState={initialOpenState}
|
|
selection={activeTabId ?? undefined}
|
|
onToggle={handleToggle}
|
|
onActivate={handleActivate}
|
|
onRename={handleRename}
|
|
disableDrag
|
|
disableDrop
|
|
>
|
|
{FileTreeNode}
|
|
</Tree>
|
|
</div>
|
|
<DropTip />
|
|
<FileTreeContextMenu treeRef={treeRef} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(Files)
|