From 021f055c364a1867dd4ba461aca8adc8aec99669 Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 19 Jan 2026 11:38:59 +0800 Subject: [PATCH] feat(skill-editor): add blank area context menu and align search/add styles Add right-click context menu for file tree blank area with New File, New Folder, and Upload Files options. Also align search input and add button styles to match Figma design specs (24px height, 6px radius). --- .../skill/file-tree/blank-area-menu.tsx | 74 +++++++++++++++++++ .../workflow/skill/file-tree/index.tsx | 10 +++ .../skill/file-tree/tree-context-menu.tsx | 29 +++++--- .../skill/hooks/use-tree-node-handlers.ts | 1 + .../workflow/skill/sidebar-search-add.tsx | 17 +++-- .../store/workflow/skill-editor/types.ts | 17 +++-- 6 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 web/app/components/workflow/skill/file-tree/blank-area-menu.tsx 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