diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-upload-modal.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-upload-modal.tsx index ce9815efbd..83307cf842 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-upload-modal.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-upload-modal.tsx @@ -1,21 +1,27 @@ -import type { Item } from '@/app/components/base/select' +import type { NodeRendererProps } from 'react-arborist' import type { TreeNodeData } from '@/app/components/workflow/skill/type' import { noop } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' +import { Tree } from 'react-arborist' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import { SimpleSelect } from '@/app/components/base/select' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' import Toast from '@/app/components/base/toast' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import { ROOT_ID } from '@/app/components/workflow/skill/constants' +import TreeGuideLines from '@/app/components/workflow/skill/file-tree/tree/tree-guide-lines' import { useSkillAssetTreeData } from '@/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree' import { useSkillTreeUpdateEmitter } from '@/app/components/workflow/skill/hooks/file-tree/data/use-skill-tree-collaboration' import { useCreateOperations } from '@/app/components/workflow/skill/hooks/file-tree/operations/use-create-operations' -import { toApiParentId } from '@/app/components/workflow/skill/utils/tree-utils' +import { findNodeById, toApiParentId } from '@/app/components/workflow/skill/utils/tree-utils' import { useWorkflowStore } from '@/app/components/workflow/store' import { useUploadFileWithPresignedUrl } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' @@ -27,12 +33,102 @@ type FilePickerUploadModalProps = { } type AddFileMode = 'create' | 'upload' -type FolderOption = Item & { - pathLabel: string - depth: number - hasChildren: boolean + +const buildFolderOnlyTree = (nodes: TreeNodeData[]): TreeNodeData[] => { + return nodes + .filter(node => node.node_type === 'folder') + .map((node) => { + const children = buildFolderOnlyTree(node.children) + return { + ...node, + children, + } + }) } +type FolderPickerTreeNodeProps = NodeRendererProps & { + onSelectNode: (node: TreeNodeData) => void +} + +const FolderPickerTreeNode = ({ node, style, dragHandle, onSelectNode }: FolderPickerTreeNodeProps) => { + const isSelected = node.isSelected + const hasChildren = node.data.children.length > 0 + + const handleClick = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + node.select() + onSelectNode(node.data) + }, [node, onSelectNode]) + + const handleToggle = useCallback((e: React.MouseEvent) => { + e.stopPropagation() + node.toggle() + }, [node]) + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSelectNode(node.data) + } + }, [node.data, onSelectNode]) + + return ( +
+ +
+
+ {node.isOpen + ?
+ + {node.data.name} + +
+ {hasChildren && ( + + )} +
+ ) +} + +FolderPickerTreeNode.displayName = 'FolderPickerTreeNode' + const FilePickerUploadModal = ({ isOpen, onClose, @@ -47,49 +143,44 @@ const FilePickerUploadModal = ({ const uploadFile = useUploadFileWithPresignedUrl() const [mode, setMode] = useState('create') const [uploadFolderId, setUploadFolderId] = useState(defaultFolderId || ROOT_ID) + const [isFolderPickerOpen, setIsFolderPickerOpen] = useState(false) + const [folderPickerVersion, setFolderPickerVersion] = useState(0) const [fileName, setFileName] = useState('') const [isDragOver, setIsDragOver] = useState(false) const treeNodes = useMemo(() => treeData?.children || [], [treeData?.children]) - const folderOptions = useMemo(() => { - const options: FolderOption[] = [{ - value: ROOT_ID, - name: t('skillSidebar.rootFolder'), - pathLabel: t('skillSidebar.rootFolder'), - depth: 0, - hasChildren: true, - }] - - const travelFolders = (nodes: TreeNodeData[]) => { - nodes.forEach((node) => { - if (node.node_type !== 'folder') - return - - const folderPath = node.path.replace(/^\//, '') || node.name - const depth = Math.max(0, folderPath.split('/').length - 1) - options.push({ - value: node.id, - name: node.name, - pathLabel: folderPath, - depth, - hasChildren: node.children.some(child => child.node_type === 'folder'), - }) - if (node.children.length > 0) - travelFolders(node.children) - }) + const folderTreeNodes = useMemo(() => buildFolderOnlyTree(treeNodes), [treeNodes]) + const uploadInTreeData = useMemo(() => { + const workRoot: TreeNodeData = { + id: ROOT_ID, + node_type: 'folder', + name: 'work', + path: '/work', + extension: '', + size: 0, + children: folderTreeNodes, } - travelFolders(treeNodes) - return options - }, [t, treeNodes]) + return [workRoot] + }, [folderTreeNodes]) + const selectedFolderPath = useMemo(() => { + if (uploadFolderId === ROOT_ID) + return 'work' + const selectedNode = findNodeById(uploadInTreeData, uploadFolderId) + const selectedPath = selectedNode?.path.replace(/^\//, '') + return selectedPath ? `work/${selectedPath}` : 'work' + }, [uploadFolderId, uploadInTreeData]) const effectiveUploadFolderId = useMemo(() => { - return folderOptions.some(item => item.value === uploadFolderId) + if (uploadFolderId === ROOT_ID) + return ROOT_ID + const selectedNode = findNodeById(uploadInTreeData, uploadFolderId) + return selectedNode ? uploadFolderId : ROOT_ID - }, [folderOptions, uploadFolderId]) - const selectedFolderOption = useMemo(() => { - return folderOptions.find(item => item.value === effectiveUploadFolderId) || folderOptions[0] - }, [effectiveUploadFolderId, folderOptions]) + }, [uploadFolderId, uploadInTreeData]) + const folderPickerOpenState = useMemo(() => { + return { [ROOT_ID]: true } + }, []) const { fileInputRef, @@ -111,6 +202,25 @@ const FilePickerUploadModal = ({ return onClose() }, [isBusy, onClose]) + const handleFolderPickerOpenChange = useCallback((open: boolean) => { + setIsFolderPickerOpen(open) + if (open) + setFolderPickerVersion(version => version + 1) + }, []) + const handleToggleFolderPicker = useCallback(() => { + if (isBusy) + return + setIsFolderPickerOpen((open) => { + const nextOpen = !open + if (nextOpen) + setFolderPickerVersion(version => version + 1) + return nextOpen + }) + }, [isBusy]) + const handleSelectUploadFolder = useCallback((node: TreeNodeData) => { + setUploadFolderId(node.id) + setIsFolderPickerOpen(false) + }, []) const handleUploadFilesChange = useCallback(async (e: React.ChangeEvent) => { const hasSelectedFiles = (e.target.files?.length ?? 0) > 0 @@ -188,41 +298,66 @@ const FilePickerUploadModal = ({
{modeLabel}
- { - const currentOption = selectedItem as FolderOption | null - const label = currentOption?.pathLabel || selectedFolderOption?.pathLabel || t('skillSidebar.rootFolder') - return ( -
-
+ +
{mode === 'create' && (