From 9aed4f830f49d1f09c454e672e61dc0616166c56 Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 19 Jan 2026 14:22:25 +0800 Subject: [PATCH] refactor(skill): merge BlankAreaMenu into NodeMenu Consolidate menu components by extending NodeMenu to support a 'root' type, eliminating the redundant BlankAreaMenu component. This reduces code duplication and simplifies the context menu logic by storing isFolder in the context menu state instead of re-querying tree data. --- .../skill/file-tree/blank-area-menu.tsx | 74 ------------------- .../workflow/skill/file-tree/node-menu.tsx | 51 +++++++------ .../skill/file-tree/tree-context-menu.tsx | 42 ++++------- .../skill/hooks/use-tree-node-handlers.ts | 3 +- .../store/workflow/skill-editor/types.ts | 1 + 5 files changed, 43 insertions(+), 128 deletions(-) delete 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 deleted file mode 100644 index 20eee5a6a4..0000000000 --- a/web/app/components/workflow/skill/file-tree/blank-area-menu.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'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/node-menu.tsx b/web/app/components/workflow/skill/file-tree/node-menu.tsx index 518a4c98b4..6c91285a1a 100644 --- a/web/app/components/workflow/skill/file-tree/node-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/node-menu.tsx @@ -18,8 +18,13 @@ import { cn } from '@/utils/classnames' import { useFileOperations } from '../hooks/use-file-operations' import MenuItem from './menu-item' +export const MENU_CONTAINER_STYLES = [ + 'min-w-[180px] rounded-xl border-[0.5px] border-components-panel-border', + 'bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]', +] as const + type NodeMenuProps = { - type: 'file' | 'folder' + type: 'file' | 'folder' | 'root' nodeId?: string onClose: () => void className?: string @@ -36,9 +41,8 @@ const NodeMenu: FC = ({ node, }) => { const { t } = useTranslation('workflow') - const isFolder = type === 'folder' - const derivedNodeId = node?.data.id ?? nodeId ?? '' - const isRoot = derivedNodeId === 'root' + const isRoot = type === 'root' + const isFolder = type === 'folder' || isRoot const { fileInputRef, @@ -65,12 +69,7 @@ const NodeMenu: FC = ({ : t('skillSidebar.menu.fileDeleteConfirmContent') return ( -
+
{isFolder && ( <> = ({ className="hidden" onChange={handleFileChange} /> - + {!isRoot && ( + + )} = ({ onClick={() => fileInputRef.current?.click()} disabled={isLoading} /> - folderInputRef.current?.click()} - disabled={isLoading} - /> + {!isRoot && ( + folderInputRef.current?.click()} + disabled={isLoading} + /> + )} {showRenameDelete &&
} 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 aa463c1761..c878d8fbe9 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 @@ -5,22 +5,24 @@ import type { TreeApi } from 'react-arborist' import type { TreeNodeData } from '../type' import { useClickAway } from 'ahooks' import * as React from 'react' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, 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 = { treeRef: React.RefObject | null> } +function getMenuType(contextMenu: { type: string, isFolder?: boolean }): 'root' | 'folder' | 'file' { + if (contextMenu.type === 'blank') + return 'root' + return contextMenu.isFolder ? 'folder' : 'file' +} + const TreeContextMenu: FC = ({ treeRef }) => { const ref = useRef(null) const contextMenu = useStore(s => s.contextMenu) const storeApi = useWorkflowStore() - const { data: treeData } = useSkillAssetTreeData() const handleClose = useCallback(() => { storeApi.getState().setContextMenu(null) @@ -30,18 +32,6 @@ const TreeContextMenu: FC = ({ treeRef }) => { handleClose() }, ref) - const nodeId = contextMenu?.nodeId - const treeChildren = treeData?.children - - const targetNode = useMemo(() => { - if (!nodeId || !treeChildren) - return null - return findNodeById(treeChildren, nodeId) - }, [nodeId, treeChildren]) - - const isFolder = targetNode?.node_type === 'folder' - const isBlankArea = contextMenu?.type === 'blank' - if (!contextMenu) return null @@ -54,18 +44,12 @@ 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 74eaae857c..5ceed83f8b 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 @@ -80,8 +80,9 @@ export function useTreeNodeHandlers({ left: e.clientX, type: 'node', nodeId: node.data.id, + isFolder, }) - }, [node.data.id, storeApi]) + }, [isFolder, node.data.id, storeApi]) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { 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 b5b85a7395..ad02f0267f 100644 --- a/web/app/components/workflow/store/workflow/skill-editor/types.ts +++ b/web/app/components/workflow/store/workflow/skill-editor/types.ts @@ -63,6 +63,7 @@ export type ContextMenuState = { left: number type: ContextMenuType nodeId?: string + isFolder?: boolean } export type FileOperationsMenuSliceShape = {