From 552f9a8989e597aa315d3bef4a260669bb69f0da Mon Sep 17 00:00:00 2001 From: yyh Date: Tue, 20 Jan 2026 12:43:56 +0800 Subject: [PATCH] refactor(skill): simplify file tree search state management Move searchTerm from props drilling to zustand store for cleaner architecture. Remove unnecessary controlled/uncontrolled pattern and unused debounce logic since search is pure frontend filtering. - Add fileTreeSearchTerm state to file-tree-slice - Remove useState and props from main.tsx - Simplify sidebar-search-add.tsx to read/write store directly - Add empty state UI with reset filter button --- .../workflow/skill/file-tree/index.tsx | 46 ++++++++++++++++++- web/app/components/workflow/skill/main.tsx | 11 +---- .../workflow/skill/sidebar-search-add.tsx | 21 +++------ .../workflow/skill-editor/file-tree-slice.ts | 6 +++ .../store/workflow/skill-editor/index.ts | 1 + .../store/workflow/skill-editor/types.ts | 2 + 6 files changed, 61 insertions(+), 26 deletions(-) diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx index 62d8b315bc..cb46875c5f 100644 --- a/web/app/components/workflow/skill/file-tree/index.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -10,6 +10,8 @@ import * as React from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' import { Tree } from 'react-arborist' import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import SearchMenu from '@/app/components/base/icons/src/vender/knowledge/SearchMenu' import Loading from '@/app/components/base/loading' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { cn } from '@/utils/classnames' @@ -23,7 +25,6 @@ import TreeNode from './tree-node' type FileTreeProps = { className?: string - searchTerm?: string } const emptyTreeNodes: TreeNodeData[] = [] @@ -40,7 +41,7 @@ const DropTip = () => { ) } -const FileTree: React.FC = ({ className, searchTerm = '' }) => { +const FileTree: React.FC = ({ className }) => { const { t } = useTranslation('workflow') const treeRef = useRef>(null) const containerRef = useRef(null) @@ -61,6 +62,7 @@ const FileTree: React.FC = ({ className, searchTerm = '' }) => { const activeTabId = useStore(s => s.activeTabId) const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId) const dragOverFolderId = useStore(s => s.dragOverFolderId) + const searchTerm = useStore(s => s.fileTreeSearchTerm) const storeApi = useWorkflowStore() // Root dropzone highlight (when dragging to root, not to a specific folder) @@ -88,6 +90,25 @@ const FileTree: React.FC = ({ className, searchTerm = '' }) => { ) }, [expandedFolderIds]) + // Check if search has no results (has search term but no matches) + const hasSearchNoResults = useMemo(() => { + if (!searchTerm || treeChildren.length === 0) + return false + + const lowerSearchTerm = searchTerm.toLowerCase() + + const checkMatch = (nodes: TreeNodeData[]): boolean => { + for (const node of nodes) { + if (node.name.toLowerCase().includes(lowerSearchTerm)) + return true + if (node.children && checkMatch(node.children)) + return true + } + return false + } + return !checkMatch(treeChildren) + }, [searchTerm, treeChildren]) + const handleToggle = useCallback((id: string) => { storeApi.getState().toggleFolder(id) }, [storeApi]) @@ -155,6 +176,27 @@ const FileTree: React.FC = ({ className, searchTerm = '' }) => { ) } + // Search has no matching results + if (hasSearchNoResults) { + return ( +
+
+ + + {t('skillSidebar.searchNoResults')} + + +
+
+ ) + } + return ( <>
{ - 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 a7680b3956..08838e1013 100644 --- a/web/app/components/workflow/skill/sidebar-search-add.tsx +++ b/web/app/components/workflow/skill/sidebar-search-add.tsx @@ -8,9 +8,8 @@ import { RiFolderUploadLine, RiUploadLine, } from '@remixicon/react' -import { useDebounce } from 'ahooks' import * as React from 'react' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { @@ -19,17 +18,13 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import SearchInput from '@/app/components/base/search-input' -import { useStore } from '@/app/components/workflow/store' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { cn } from '@/utils/classnames' import { ROOT_ID } from './constants' 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 @@ -55,16 +50,12 @@ const MenuItem: React.FC = ({ icon: Icon, label, onClick, disable ) -const SidebarSearchAdd: FC = ({ onSearchChange }) => { +const SidebarSearchAdd: FC = () => { const { t } = useTranslation('workflow') - const [searchValue, setSearchValue] = useState('') - const debouncedSearchValue = useDebounce(searchValue, { wait: 300 }) + const searchValue = useStore(s => s.fileTreeSearchTerm) + const storeApi = useWorkflowStore() const [showMenu, setShowMenu] = useState(false) - useEffect(() => { - onSearchChange?.(debouncedSearchValue) - }, [debouncedSearchValue, onSearchChange]) - const { data: treeData } = useSkillAssetTreeData() const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId) const treeChildren = treeData?.children @@ -93,7 +84,7 @@ const SidebarSearchAdd: FC = ({ onSearchChange }) => {
storeApi.getState().setFileTreeSearchTerm(v)} className="!h-6 flex-1 !rounded-md" placeholder={t('skillSidebar.searchPlaceholder')} /> diff --git a/web/app/components/workflow/store/workflow/skill-editor/file-tree-slice.ts b/web/app/components/workflow/store/workflow/skill-editor/file-tree-slice.ts index c2aac992ac..e4cdc24331 100644 --- a/web/app/components/workflow/store/workflow/skill-editor/file-tree-slice.ts +++ b/web/app/components/workflow/store/workflow/skill-editor/file-tree-slice.ts @@ -80,4 +80,10 @@ export const createFileTreeSlice: StateCreator< setDragOverFolderId: (folderId) => { set({ dragOverFolderId: folderId }) }, + + fileTreeSearchTerm: '', + + setFileTreeSearchTerm: (term) => { + set({ fileTreeSearchTerm: term }) + }, }) diff --git a/web/app/components/workflow/store/workflow/skill-editor/index.ts b/web/app/components/workflow/store/workflow/skill-editor/index.ts index 909f71dc64..8b4649f9c0 100644 --- a/web/app/components/workflow/store/workflow/skill-editor/index.ts +++ b/web/app/components/workflow/store/workflow/skill-editor/index.ts @@ -33,6 +33,7 @@ export const createSkillEditorSlice: StateCreator = (...a fileMetadata: new Map>(), dirtyMetadataIds: new Set(), contextMenu: null, + fileTreeSearchTerm: '', }) }, }) 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 dcc6e0bb94..891a9b2f66 100644 --- a/web/app/components/workflow/store/workflow/skill-editor/types.ts +++ b/web/app/components/workflow/store/workflow/skill-editor/types.ts @@ -37,6 +37,8 @@ export type FileTreeSliceShape = { clearCreateNode: () => void dragOverFolderId: string | null setDragOverFolderId: (folderId: string | null) => void + fileTreeSearchTerm: string + setFileTreeSearchTerm: (term: string) => void } export type DirtySliceShape = {