From 06b6625c014b882415fcfecb55d87c6e972d288a Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 16 Jan 2026 14:40:55 +0800 Subject: [PATCH] feat(skill): implement file tree search with debounced filtering Add search functionality to skill sidebar using react-arborist's built-in searchTerm and searchMatch props. Search input is debounced at 300ms and filters tree nodes by name (case-insensitive). Also add success toast for rename operations. --- .../workflow/skill/file-tree/index.tsx | 17 ++++++++++++++++- web/app/components/workflow/skill/main.tsx | 11 +++++++++-- .../workflow/skill/sidebar-search-add.tsx | 14 ++++++++++++-- web/i18n/en-US/workflow.json | 1 + web/i18n/zh-Hans/workflow.json | 1 + 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx index d21c1adb43..35aea9f5b8 100644 --- a/web/app/components/workflow/skill/file-tree/index.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -23,6 +23,7 @@ import TreeNode from './tree-node' type FileTreeProps = { className?: string + searchTerm?: string } const DropTip = () => { @@ -37,7 +38,7 @@ const DropTip = () => { ) } -const FileTree: React.FC = ({ className }) => { +const FileTree: React.FC = ({ className, searchTerm = '' }) => { const { t } = useTranslation('workflow') const treeRef = useRef>(null) const containerRef = useRef(null) @@ -77,6 +78,11 @@ const FileTree: React.FC = ({ className }) => { appId, nodeId: id, payload: { name }, + }).then(() => { + Toast.notify({ + type: 'success', + message: t('skillSidebar.menu.renamed'), + }) }).catch(() => { Toast.notify({ type: 'error', @@ -85,6 +91,13 @@ const FileTree: React.FC = ({ className }) => { }) }, [appId, renameNode, t]) + const searchMatch = useCallback( + (node: NodeApi, term: string) => { + return node.data.name.toLowerCase().includes(term.toLowerCase()) + }, + [], + ) + useSyncTreeWithActiveTab({ treeRef, activeTabId, @@ -150,6 +163,8 @@ const FileTree: React.FC = ({ className }) => { onToggle={handleToggle} onActivate={handleActivate} onRename={handleRename} + searchTerm={searchTerm} + searchMatch={searchMatch} disableDrag disableDrop > diff --git a/web/app/components/workflow/skill/main.tsx b/web/app/components/workflow/skill/main.tsx index aa0ac2a6b4..385eab3d40 100644 --- a/web/app/components/workflow/skill/main.tsx +++ b/web/app/components/workflow/skill/main.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import * as React from 'react' +import { useCallback, useState } from 'react' import EditorArea from './editor-area' import EditorBody from './editor-body' import EditorTabs from './editor-tabs' @@ -12,12 +13,18 @@ import SkillDocEditor from './skill-doc-editor' import SkillPageLayout from './skill-page-layout' const SkillMain: FC = () => { + const [searchTerm, setSearchTerm] = useState('') + + const handleSearchChange = useCallback((term: string) => { + setSearchTerm(term) + }, []) + return (
- - + + diff --git a/web/app/components/workflow/skill/sidebar-search-add.tsx b/web/app/components/workflow/skill/sidebar-search-add.tsx index 6064eadbf0..fcf9fa781a 100644 --- a/web/app/components/workflow/skill/sidebar-search-add.tsx +++ b/web/app/components/workflow/skill/sidebar-search-add.tsx @@ -8,8 +8,9 @@ import { RiFolderUploadLine, RiUploadLine, } from '@remixicon/react' +import { useDebounce } from 'ahooks' import * as React from 'react' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { @@ -24,6 +25,10 @@ import { useFileOperations } from './hooks/use-file-operations' import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' import { getTargetFolderIdFromSelection } from './utils/tree-utils' +type SidebarSearchAddProps = { + onSearchChange?: (searchTerm: string) => void +} + type MenuItemProps = { icon: React.ElementType label: string @@ -49,11 +54,16 @@ const MenuItem: React.FC = ({ icon: Icon, label, onClick, disable ) -const SidebarSearchAdd: FC = () => { +const SidebarSearchAdd: FC = ({ onSearchChange }) => { const { t } = useTranslation('workflow') const [searchValue, setSearchValue] = useState('') + const debouncedSearchValue = useDebounce(searchValue, { wait: 300 }) const [showMenu, setShowMenu] = useState(false) + useEffect(() => { + onSearchChange?.(debouncedSearchValue) + }, [debouncedSearchValue, onSearchChange]) + const { data: treeData } = useSkillAssetTreeData() const activeTabId = useStore(s => s.activeTabId) diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 8cadeac391..9a36ea0c16 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -1025,6 +1025,7 @@ "skillSidebar.menu.newFolderPrompt": "Enter folder name:", "skillSidebar.menu.rename": "Rename", "skillSidebar.menu.renameError": "Failed to rename", + "skillSidebar.menu.renamed": "Renamed successfully", "skillSidebar.menu.uploadError": "Failed to upload", "skillSidebar.menu.uploadFile": "Upload File", "skillSidebar.menu.uploadFolder": "Upload Folder", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index ae6bc458f4..d94443a23a 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -1018,6 +1018,7 @@ "skillSidebar.menu.newFolderPrompt": "请输入文件夹名称:", "skillSidebar.menu.rename": "重命名", "skillSidebar.menu.renameError": "重命名失败", + "skillSidebar.menu.renamed": "重命名成功", "skillSidebar.menu.uploadError": "上传失败", "skillSidebar.menu.uploadFile": "上传文件", "skillSidebar.menu.uploadFolder": "上传文件夹",