From 783cdb1357b0c08ce1b1bbee43b4aa501266f129 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 15 Jan 2026 21:21:58 +0800 Subject: [PATCH] feat(skill): add inline rename and guide lines to file tree Add TreeEditInput component for inline file/folder renaming with keyboard support (Enter to submit, Escape to cancel). Add TreeGuideLines component to render vertical indent lines based on node depth for better visual hierarchy in the tree view. Reorganize file tree components into dedicated `file-tree` subdirectory for better code organization. --- .../skill/{ => file-tree}/file-node-menu.tsx | 4 +- .../{ => file-tree}/folder-node-menu.tsx | 4 +- .../{file-tree.tsx => file-tree/index.tsx} | 10 ++-- .../{ => file-tree}/tree-context-menu.tsx | 8 ++-- .../skill/file-tree/tree-edit-input.tsx | 48 +++++++++++++++++++ .../skill/file-tree/tree-guide-lines.tsx | 28 +++++++++++ .../skill/{ => file-tree}/tree-node.tsx | 37 ++++++++------ 7 files changed, 112 insertions(+), 27 deletions(-) rename web/app/components/workflow/skill/{ => file-tree}/file-node-menu.tsx (96%) rename web/app/components/workflow/skill/{ => file-tree}/folder-node-menu.tsx (97%) rename web/app/components/workflow/skill/{file-tree.tsx => file-tree/index.tsx} (94%) rename web/app/components/workflow/skill/{ => file-tree}/tree-context-menu.tsx (87%) create mode 100644 web/app/components/workflow/skill/file-tree/tree-edit-input.tsx create mode 100644 web/app/components/workflow/skill/file-tree/tree-guide-lines.tsx rename web/app/components/workflow/skill/{ => file-tree}/tree-node.tsx (87%) diff --git a/web/app/components/workflow/skill/file-node-menu.tsx b/web/app/components/workflow/skill/file-tree/file-node-menu.tsx similarity index 96% rename from web/app/components/workflow/skill/file-node-menu.tsx rename to web/app/components/workflow/skill/file-tree/file-node-menu.tsx index d31be13a27..d0ce4fee3a 100644 --- a/web/app/components/workflow/skill/file-node-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/file-node-menu.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { NodeApi, TreeApi } from 'react-arborist' -import type { TreeNodeData } from './type' +import type { TreeNodeData } from '../type' import { RiDeleteBinLine, RiEdit2Line, @@ -11,7 +11,7 @@ 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' +import { useFileOperations } from '../hooks/use-file-operations' type MenuItemProps = { icon: React.ElementType diff --git a/web/app/components/workflow/skill/folder-node-menu.tsx b/web/app/components/workflow/skill/file-tree/folder-node-menu.tsx similarity index 97% rename from web/app/components/workflow/skill/folder-node-menu.tsx rename to web/app/components/workflow/skill/file-tree/folder-node-menu.tsx index f702e5d06e..e6c5d076d5 100644 --- a/web/app/components/workflow/skill/folder-node-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/folder-node-menu.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { NodeApi, TreeApi } from 'react-arborist' -import type { TreeNodeData } from './type' +import type { TreeNodeData } from '../type' import { RiDeleteBinLine, RiEdit2Line, @@ -15,7 +15,7 @@ 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' +import { useFileOperations } from '../hooks/use-file-operations' type MenuItemProps = { icon: React.ElementType diff --git a/web/app/components/workflow/skill/file-tree.tsx b/web/app/components/workflow/skill/file-tree/index.tsx similarity index 94% rename from web/app/components/workflow/skill/file-tree.tsx rename to web/app/components/workflow/skill/file-tree/index.tsx index dd3c48d76d..7c825d1fd0 100644 --- a/web/app/components/workflow/skill/file-tree.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -1,8 +1,8 @@ 'use client' import type { NodeApi, TreeApi } from 'react-arborist' -import type { OpensObject } from './store' -import type { TreeNodeData } from './type' +import type { OpensObject } from '../store' +import type { TreeNodeData } from '../type' import { RiDragDropLine } from '@remixicon/react' import { useIsMutating } from '@tanstack/react-query' import { useSize } from 'ahooks' @@ -15,11 +15,11 @@ import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' import { useRenameAppAssetNode } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' -import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' -import { useSkillEditorStore, useSkillEditorStoreApi } from './store' +import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree' +import { useSkillEditorStore, useSkillEditorStoreApi } from '../store' +import { getAncestorIds } from '../utils/tree-utils' import TreeContextMenu from './tree-context-menu' import TreeNode from './tree-node' -import { getAncestorIds } from './utils/tree-utils' type FileTreeProps = { className?: string diff --git a/web/app/components/workflow/skill/tree-context-menu.tsx b/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx similarity index 87% rename from web/app/components/workflow/skill/tree-context-menu.tsx rename to web/app/components/workflow/skill/file-tree/tree-context-menu.tsx index e1b4913895..5d4233650e 100644 --- a/web/app/components/workflow/skill/tree-context-menu.tsx +++ b/web/app/components/workflow/skill/file-tree/tree-context-menu.tsx @@ -2,15 +2,15 @@ import type { FC } from 'react' import type { TreeApi } from 'react-arborist' -import type { TreeNodeData } from './type' +import type { TreeNodeData } from '../type' import { useClickAway } from 'ahooks' import * as React from 'react' import { useCallback, useMemo, useRef } from 'react' +import { useSkillAssetTreeData } from '../hooks/use-skill-asset-tree' +import { useSkillEditorStore, useSkillEditorStoreApi } from '../store' +import { findNodeById } from '../utils/tree-utils' import FileNodeMenu from './file-node-menu' import FolderNodeMenu from './folder-node-menu' -import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' -import { useSkillEditorStore, useSkillEditorStoreApi } from './store' -import { findNodeById } from './utils/tree-utils' type TreeContextMenuProps = { treeRef: React.RefObject | null> diff --git a/web/app/components/workflow/skill/file-tree/tree-edit-input.tsx b/web/app/components/workflow/skill/file-tree/tree-edit-input.tsx new file mode 100644 index 0000000000..1c8d3cacb3 --- /dev/null +++ b/web/app/components/workflow/skill/file-tree/tree-edit-input.tsx @@ -0,0 +1,48 @@ +'use client' + +import type { NodeApi } from 'react-arborist' +import type { TreeNodeData } from '../type' +import * as React from 'react' +import { useEffect, useRef } from 'react' + +type TreeEditInputProps = { + node: NodeApi +} + +const TreeEditInput: React.FC = ({ node }) => { + const inputRef = useRef(null) + + useEffect(() => { + inputRef.current?.focus() + inputRef.current?.select() + }, []) + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation() + if (e.key === 'Escape') { + node.reset() + } + else if (e.key === 'Enter') { + e.preventDefault() + node.submit(inputRef.current?.value || '') + } + } + + const handleBlur = () => { + node.reset() + } + + return ( + e.stopPropagation()} + className="min-w-0 flex-1 rounded border border-components-input-border-active bg-transparent px-1 text-[13px] font-normal leading-4 text-text-primary outline-none" + /> + ) +} + +export default React.memo(TreeEditInput) diff --git a/web/app/components/workflow/skill/file-tree/tree-guide-lines.tsx b/web/app/components/workflow/skill/file-tree/tree-guide-lines.tsx new file mode 100644 index 0000000000..35da9ab509 --- /dev/null +++ b/web/app/components/workflow/skill/file-tree/tree-guide-lines.tsx @@ -0,0 +1,28 @@ +'use client' + +import * as React from 'react' + +const INDENT_SIZE = 20 + +type TreeGuideLinesProps = { + level: number +} + +const TreeGuideLines: React.FC = ({ level }) => { + if (level === 0) + return null + + return ( + <> + {Array.from({ length: level }).map((_, i) => ( +
+ ))} + + ) +} + +export default React.memo(TreeGuideLines) diff --git a/web/app/components/workflow/skill/tree-node.tsx b/web/app/components/workflow/skill/file-tree/tree-node.tsx similarity index 87% rename from web/app/components/workflow/skill/tree-node.tsx rename to web/app/components/workflow/skill/file-tree/tree-node.tsx index 48b64b5fc8..ddd52614bf 100644 --- a/web/app/components/workflow/skill/tree-node.tsx +++ b/web/app/components/workflow/skill/file-tree/tree-node.tsx @@ -1,7 +1,7 @@ 'use client' import type { NodeRendererProps } from 'react-arborist' -import type { TreeNodeData } from './type' +import type { TreeNodeData } from '../type' import type { FileAppearanceType } from '@/app/components/base/file-uploader/types' import { RiFolderLine, RiFolderOpenLine, RiMoreFill } from '@remixicon/react' import * as React from 'react' @@ -14,10 +14,12 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { cn } from '@/utils/classnames' +import { useSkillEditorStore, useSkillEditorStoreApi } from '../store' +import { getFileIconType } from '../utils/file-utils' import FileNodeMenu from './file-node-menu' import FolderNodeMenu from './folder-node-menu' -import { useSkillEditorStore, useSkillEditorStoreApi } from './store' -import { getFileIconType } from './utils/file-utils' +import TreeEditInput from './tree-edit-input' +import TreeGuideLines from './tree-guide-lines' const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) => { const { t } = useTranslation('workflow') @@ -83,7 +85,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) aria-selected={isSelected} aria-expanded={isFolder ? node.isOpen : undefined} className={cn( - 'group flex h-6 cursor-pointer items-center gap-2 rounded-md px-2', + 'group relative flex h-6 cursor-pointer items-center gap-2 rounded-md px-2', 'hover:bg-state-base-hover', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active', isSelected && 'bg-state-base-active', @@ -94,6 +96,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) onKeyDown={handleKeyDown} onContextMenu={handleContextMenu} > +
{isFolder ? ( @@ -122,16 +125,22 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) )}
- - {node.data.name} - + {node.isEditing + ? ( + + ) + : ( + + {node.data.name} + + )}