diff --git a/web/app/components/workflow/skill/file-tree/blank-area-menu.tsx b/web/app/components/workflow/skill/file-tree/blank-area-menu.tsx new file mode 100644 index 0000000000..20eee5a6a4 --- /dev/null +++ b/web/app/components/workflow/skill/file-tree/blank-area-menu.tsx @@ -0,0 +1,74 @@ +'use client' + +import type { FC } from 'react' +import { + RiFileAddLine, + RiFolderAddLine, + RiUploadLine, +} from '@remixicon/react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/utils/classnames' +import { useFileOperations } from '../hooks/use-file-operations' +import MenuItem from './menu-item' + +type BlankAreaMenuProps = { + onClose: () => void + className?: string +} + +const BlankAreaMenu: FC = ({ + onClose, + className, +}) => { + const { t } = useTranslation('workflow') + + const { + fileInputRef, + isLoading, + handleNewFile, + handleNewFolder, + handleFileChange, + } = useFileOperations({ nodeId: 'root', onClose }) + + return ( +
+ + + + + +
+ + fileInputRef.current?.click()} + disabled={isLoading} + /> +
+ ) +} + +export default React.memo(BlankAreaMenu) diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx index 687db348f3..f393a840e6 100644 --- a/web/app/components/workflow/skill/file-tree/index.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -91,6 +91,15 @@ const FileTree: React.FC = ({ className, searchTerm = '' }) => { }) }, [appId, renameNode, t]) + const handleBlankAreaContextMenu = useCallback((e: React.MouseEvent) => { + e.preventDefault() + storeApi.getState().setContextMenu({ + top: e.clientY, + left: e.clientX, + type: 'blank', + }) + }, [storeApi]) + const searchMatch = useCallback( (node: NodeApi, term: string) => { return node.data.name.toLowerCase().includes(term.toLowerCase()) @@ -146,6 +155,7 @@ const FileTree: React.FC = ({ className, searchTerm = '' }) => {
ref={treeRef} diff --git a/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx b/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx index 0de88de0f7..aa463c1761 100644 --- a/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx @@ -9,6 +9,7 @@ import { useCallback, useMemo, useRef } from 'react' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree' import { findNodeById } from '../utils/tree-utils' +import BlankAreaMenu from './blank-area-menu' import NodeMenu from './node-menu' type TreeContextMenuProps = { @@ -29,13 +30,17 @@ const TreeContextMenu: FC = ({ treeRef }) => { handleClose() }, ref) + const nodeId = contextMenu?.nodeId + const treeChildren = treeData?.children + const targetNode = useMemo(() => { - if (!contextMenu?.nodeId || !treeData?.children) + if (!nodeId || !treeChildren) return null - return findNodeById(treeData.children, contextMenu.nodeId) - }, [contextMenu?.nodeId, treeData?.children]) + return findNodeById(treeChildren, nodeId) + }, [nodeId, treeChildren]) const isFolder = targetNode?.node_type === 'folder' + const isBlankArea = contextMenu?.type === 'blank' if (!contextMenu) return null @@ -49,12 +54,18 @@ const TreeContextMenu: FC = ({ treeRef }) => { left: contextMenu.left, }} > - + {isBlankArea + ? ( + + ) + : ( + + )}
) } diff --git a/web/app/components/workflow/skill/hooks/use-tree-node-handlers.ts b/web/app/components/workflow/skill/hooks/use-tree-node-handlers.ts index e1f4c848f8..a1265d6a36 100644 --- a/web/app/components/workflow/skill/hooks/use-tree-node-handlers.ts +++ b/web/app/components/workflow/skill/hooks/use-tree-node-handlers.ts @@ -76,6 +76,7 @@ export function useTreeNodeHandlers({ storeApi.getState().setContextMenu({ top: e.clientY, left: e.clientX, + type: 'node', nodeId: node.data.id, }) }, [node.data.id, storeApi]) diff --git a/web/app/components/workflow/skill/sidebar-search-add.tsx b/web/app/components/workflow/skill/sidebar-search-add.tsx index fcf9fa781a..0cd1057f9c 100644 --- a/web/app/components/workflow/skill/sidebar-search-add.tsx +++ b/web/app/components/workflow/skill/sidebar-search-add.tsx @@ -66,12 +66,13 @@ const SidebarSearchAdd: FC = ({ onSearchChange }) => { const { data: treeData } = useSkillAssetTreeData() const activeTabId = useStore(s => s.activeTabId) + const treeChildren = treeData?.children const targetFolderId = useMemo(() => { - if (!treeData?.children) + if (!treeChildren) return 'root' - return getTargetFolderIdFromSelection(activeTabId, treeData.children) - }, [activeTabId, treeData?.children]) + return getTargetFolderIdFromSelection(activeTabId, treeChildren) + }, [activeTabId, treeChildren]) const menuOffset = useMemo(() => ({ mainAxis: 4 }), []) const { @@ -88,11 +89,11 @@ const SidebarSearchAdd: FC = ({ onSearchChange }) => { }) return ( -
+
= ({ onSearchChange }) => { setShowMenu(!showMenu)}> diff --git a/web/app/components/workflow/store/workflow/skill-editor/types.ts b/web/app/components/workflow/store/workflow/skill-editor/types.ts index 791010fff1..d1678a3b9c 100644 --- a/web/app/components/workflow/store/workflow/skill-editor/types.ts +++ b/web/app/components/workflow/store/workflow/skill-editor/types.ts @@ -43,13 +43,18 @@ export type MetadataSliceShape = { getFileMetadata: (fileId: string) => Record | undefined } +export type ContextMenuType = 'node' | 'blank' + +export type ContextMenuState = { + top: number + left: number + type: ContextMenuType + nodeId?: string +} + export type FileOperationsMenuSliceShape = { - contextMenu: { - top: number - left: number - nodeId: string - } | null - setContextMenu: (menu: FileOperationsMenuSliceShape['contextMenu']) => void + contextMenu: ContextMenuState | null + setContextMenu: (menu: ContextMenuState | null) => void } export type SkillEditorSliceShape