diff --git a/web/app/components/workflow/skill/file-tree/index.tsx b/web/app/components/workflow/skill/file-tree/index.tsx index cefd5efe9b..cf2e60c9cf 100644 --- a/web/app/components/workflow/skill/file-tree/index.tsx +++ b/web/app/components/workflow/skill/file-tree/index.tsx @@ -28,6 +28,7 @@ import { useSkillTreeCollaboration } from '../hooks/use-skill-tree-collaboration import { useSyncTreeWithActiveTab } from '../hooks/use-sync-tree-with-active-tab' import { isDescendantOf } from '../utils/tree-utils' import DragActionTooltip from './drag-action-tooltip' +import SearchResultList from './search-result-list' import TreeContextMenu from './tree-context-menu' import TreeNode from './tree-node' import UploadStatusTooltip from './upload-status-tooltip' @@ -366,6 +367,17 @@ const FileTree = ({ className }: FileTreeProps) => { ) } + if (searchTerm) { + return ( +
+ +
+ ) + } + return ( <>
{ + const activeTabId = useStore(s => s.activeTabId) + const storeApi = useWorkflowStore() + + const results = useMemo( + () => flattenMatchingNodes(treeChildren, searchTerm), + [treeChildren, searchTerm], + ) + + const handleClick = useCallback((node: AppAssetTreeView) => { + if (node.node_type === 'file') { + storeApi.getState().openTab(node.id, { pinned: true }) + } + else { + const ancestors = getAncestorIds(node.id, treeChildren) + storeApi.getState().revealFile([...ancestors, node.id]) + storeApi.getState().setFileTreeSearchTerm('') + } + }, [storeApi, treeChildren]) + + return ( +
+ {results.map(({ node, parentPath }) => ( +
handleClick(node)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleClick(node) + } + }} + > +
+
+ +
+ + {node.name} + +
+ {parentPath && ( + + {parentPath} + + )} +
+ ))} +
+ ) +} + +export default SearchResultList diff --git a/web/app/components/workflow/skill/utils/tree-utils.ts b/web/app/components/workflow/skill/utils/tree-utils.ts index 59dd02425e..d1076119e9 100644 --- a/web/app/components/workflow/skill/utils/tree-utils.ts +++ b/web/app/components/workflow/skill/utils/tree-utils.ts @@ -199,6 +199,37 @@ function insertDraftNodeAtParent( return { nodes: inserted ? nextNodes : nodes, inserted } } +export type FlatSearchResult = { + node: AppAssetTreeView + parentPath: string +} + +export function flattenMatchingNodes( + nodes: AppAssetTreeView[], + searchTerm: string, +): FlatSearchResult[] { + if (!searchTerm) + return [] + + const results: FlatSearchResult[] = [] + const lowerTerm = searchTerm.toLowerCase() + + function traverse(nodeList: AppAssetTreeView[]): void { + for (const node of nodeList) { + if (node.name.toLowerCase().includes(lowerTerm)) { + const lastSlash = node.path.lastIndexOf('/') + const parentPath = lastSlash > 0 ? node.path.slice(1, lastSlash) : '' + results.push({ node, parentPath }) + } + if (node.children && node.children.length > 0) + traverse(node.children) + } + } + + traverse(nodes) + return results +} + export function insertDraftTreeNode( nodes: AppAssetTreeView[], parentId: string | null,