From f58f36fc8f0db9fd6e9f68b0bba5ce0119ccf495 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 15 Jan 2026 17:24:58 +0800 Subject: [PATCH] feat(skill): add file right-click/more menu and refactor naming - Add right-click context menu and '...' more button for files - Files now support Rename and Delete operations - Created file-node-menu.tsx for file-specific menu - Refactor component naming for consistency - file-item-menu.tsx -> file-node-menu.tsx (unify 'node' terminology) - file-operations-menu.tsx -> folder-node-menu.tsx (clarify folder menu) - file-tree-context-menu.tsx -> tree-context-menu.tsx (simplify) - file-tree-node.tsx -> tree-node.tsx (simplify) - files.tsx -> file-tree.tsx (more descriptive) - Renamed internal components: FileTreeNode -> TreeNode, Files -> FileTree - Add context menu node highlight - When right-clicking a node, it now shows hover highlight - Subscribed to contextMenu.nodeId in TreeNode component --- .../workflow/skill/file-node-menu.tsx | 109 ++++++++++++++++++ .../workflow/skill/file-tree-context-menu.tsx | 50 -------- .../skill/{files.tsx => file-tree.tsx} | 14 +-- ...erations-menu.tsx => folder-node-menu.tsx} | 0 web/app/components/workflow/skill/main.tsx | 4 +- .../workflow/skill/tree-context-menu.tsx | 76 ++++++++++++ .../{file-tree-node.tsx => tree-node.tsx} | 83 +++++++------ 7 files changed, 240 insertions(+), 96 deletions(-) create mode 100644 web/app/components/workflow/skill/file-node-menu.tsx delete mode 100644 web/app/components/workflow/skill/file-tree-context-menu.tsx rename web/app/components/workflow/skill/{files.tsx => file-tree.tsx} (94%) rename web/app/components/workflow/skill/{file-operations-menu.tsx => folder-node-menu.tsx} (100%) create mode 100644 web/app/components/workflow/skill/tree-context-menu.tsx rename web/app/components/workflow/skill/{file-tree-node.tsx => tree-node.tsx} (66%) diff --git a/web/app/components/workflow/skill/file-node-menu.tsx b/web/app/components/workflow/skill/file-node-menu.tsx new file mode 100644 index 0000000000..9eb71fc0e4 --- /dev/null +++ b/web/app/components/workflow/skill/file-node-menu.tsx @@ -0,0 +1,109 @@ +'use client' + +import type { FC } from 'react' +import type { NodeApi, TreeApi } from 'react-arborist' +import type { TreeNodeData } from './type' +import { + RiDeleteBinLine, + RiEdit2Line, +} from '@remixicon/react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import Confirm from '@/app/components/base/confirm' +import { cn } from '@/utils/classnames' +import { useFileOperations } from './hooks/use-file-operations' + +type MenuItemProps = { + icon: React.ElementType + label: string + onClick: () => void + disabled?: boolean +} + +const MenuItem: React.FC = ({ icon: Icon, label, onClick, disabled }) => ( + +) + +type FileItemMenuProps = { + nodeId: string + onClose: () => void + className?: string + treeRef?: React.RefObject | null> + node?: NodeApi +} + +const FileItemMenu: FC = ({ + nodeId, + onClose, + className, + treeRef, + node, +}) => { + const { t } = useTranslation('workflow') + + const { + showDeleteConfirm, + isLoading, + isDeleting, + handleRename, + handleDeleteClick, + handleDeleteConfirm, + handleDeleteCancel, + } = useFileOperations({ nodeId, onClose, treeRef, node }) + + return ( +
+ + + + +
+ ) +} + +export default React.memo(FileItemMenu) diff --git a/web/app/components/workflow/skill/file-tree-context-menu.tsx b/web/app/components/workflow/skill/file-tree-context-menu.tsx deleted file mode 100644 index 9b8c48a8e7..0000000000 --- a/web/app/components/workflow/skill/file-tree-context-menu.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { TreeApi } from 'react-arborist' -import type { TreeNodeData } from './type' -import { useClickAway } from 'ahooks' -import * as React from 'react' -import { useCallback, useRef } from 'react' -import FileOperationsMenu from './file-operations-menu' -import { useSkillEditorStore, useSkillEditorStoreApi } from './store' - -type FileTreeContextMenuProps = { - treeRef: React.RefObject | null> -} - -const FileTreeContextMenu: FC = ({ treeRef }) => { - const ref = useRef(null) - const contextMenu = useSkillEditorStore(s => s.contextMenu) - const storeApi = useSkillEditorStoreApi() - - const handleClose = useCallback(() => { - storeApi.getState().setContextMenu(null) - }, [storeApi]) - - useClickAway(() => { - handleClose() - }, ref) - - if (!contextMenu) - return null - - return ( -
- -
- ) -} - -export default React.memo(FileTreeContextMenu) diff --git a/web/app/components/workflow/skill/files.tsx b/web/app/components/workflow/skill/file-tree.tsx similarity index 94% rename from web/app/components/workflow/skill/files.tsx rename to web/app/components/workflow/skill/file-tree.tsx index 936da07c1c..3a7e6f4e89 100644 --- a/web/app/components/workflow/skill/files.tsx +++ b/web/app/components/workflow/skill/file-tree.tsx @@ -13,12 +13,12 @@ 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 TreeContextMenu from './tree-context-menu' +import TreeNode from './tree-node' import { getAncestorIds } from './utils/tree-utils' -type FilesProps = { +type FileTreeProps = { className?: string } @@ -34,7 +34,7 @@ const DropTip = () => { ) } -const Files: React.FC = ({ className }) => { +const FileTree: React.FC = ({ className }) => { const { t } = useTranslation('workflow') const treeRef = useRef>(null) @@ -151,13 +151,13 @@ const Files: React.FC = ({ className }) => { disableDrag disableDrop > - {FileTreeNode} + {TreeNode} - + ) } -export default React.memo(Files) +export default React.memo(FileTree) diff --git a/web/app/components/workflow/skill/file-operations-menu.tsx b/web/app/components/workflow/skill/folder-node-menu.tsx similarity index 100% rename from web/app/components/workflow/skill/file-operations-menu.tsx rename to web/app/components/workflow/skill/folder-node-menu.tsx diff --git a/web/app/components/workflow/skill/main.tsx b/web/app/components/workflow/skill/main.tsx index de3cb719ae..aa0ac2a6b4 100644 --- a/web/app/components/workflow/skill/main.tsx +++ b/web/app/components/workflow/skill/main.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import EditorArea from './editor-area' import EditorBody from './editor-body' import EditorTabs from './editor-tabs' -import Files from './files' +import FileTree from './file-tree' import Sidebar from './sidebar' import SidebarSearchAdd from './sidebar-search-add' import SkillDocEditor from './skill-doc-editor' @@ -17,7 +17,7 @@ const SkillMain: FC = () => { - + diff --git a/web/app/components/workflow/skill/tree-context-menu.tsx b/web/app/components/workflow/skill/tree-context-menu.tsx new file mode 100644 index 0000000000..fda25ca348 --- /dev/null +++ b/web/app/components/workflow/skill/tree-context-menu.tsx @@ -0,0 +1,76 @@ +'use client' + +import type { FC } from 'react' +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 { useStore as useAppStore } from '@/app/components/app/store' +import { useGetAppAssetTree } from '@/service/use-app-asset' +import FileNodeMenu from './file-node-menu' +import FolderNodeMenu from './folder-node-menu' +import { useSkillEditorStore, useSkillEditorStoreApi } from './store' +import { findNodeById } from './utils/tree-utils' + +type TreeContextMenuProps = { + treeRef: React.RefObject | null> +} + +const TreeContextMenu: FC = ({ treeRef }) => { + const ref = useRef(null) + const contextMenu = useSkillEditorStore(s => s.contextMenu) + const storeApi = useSkillEditorStoreApi() + + const appDetail = useAppStore(s => s.appDetail) + const appId = appDetail?.id || '' + const { data: treeData } = useGetAppAssetTree(appId) + + const handleClose = useCallback(() => { + storeApi.getState().setContextMenu(null) + }, [storeApi]) + + useClickAway(() => { + handleClose() + }, ref) + + const targetNode = useMemo(() => { + if (!contextMenu?.nodeId || !treeData?.children) + return null + return findNodeById(treeData.children, contextMenu.nodeId) + }, [contextMenu?.nodeId, treeData?.children]) + + const isFolder = targetNode?.node_type === 'folder' + + if (!contextMenu) + return null + + return ( +
+ {isFolder + ? ( + + ) + : ( + + )} +
+ ) +} + +export default React.memo(TreeContextMenu) diff --git a/web/app/components/workflow/skill/file-tree-node.tsx b/web/app/components/workflow/skill/tree-node.tsx similarity index 66% rename from web/app/components/workflow/skill/file-tree-node.tsx rename to web/app/components/workflow/skill/tree-node.tsx index 9f4c681c1d..bd5bc3a6c5 100644 --- a/web/app/components/workflow/skill/file-tree-node.tsx +++ b/web/app/components/workflow/skill/tree-node.tsx @@ -13,14 +13,17 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { cn } from '@/utils/classnames' -import FileOperationsMenu from './file-operations-menu' +import FileNodeMenu from './file-node-menu' +import FolderNodeMenu from './folder-node-menu' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' import { getFileIconType } from './utils/file-utils' -const FileTreeNode = ({ node, style, dragHandle }: NodeRendererProps) => { +const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) => { const isFolder = node.data.node_type === 'folder' const isSelected = node.isSelected const isDirty = useSkillEditorStore(s => s.dirtyContents.has(node.data.id)) + const contextMenuNodeId = useSkillEditorStore(s => s.contextMenu?.nodeId) + const hasContextMenu = contextMenuNodeId === node.data.id const storeApi = useSkillEditorStoreApi() const [showDropdown, setShowDropdown] = useState(false) @@ -44,9 +47,6 @@ const FileTreeNode = ({ node, style, dragHandle }: NodeRendererProps { - if (!isFolder) - return - e.preventDefault() e.stopPropagation() @@ -55,7 +55,7 @@ const FileTreeNode = ({ node, style, dragHandle }: NodeRendererProps { e.stopPropagation() @@ -70,6 +70,7 @@ const FileTreeNode = ({ node, style, dragHandle }: NodeRendererProps - {isFolder && ( - - - + + + {isFolder + ? ( + setShowDropdown(false)} + node={node} + /> + ) + : ( + setShowDropdown(false)} + node={node} + /> )} - aria-label="File operations" - > - - - - - setShowDropdown(false)} - node={node} - /> - - - )} + + ) } -export default React.memo(FileTreeNode) +export default React.memo(TreeNode)