From 3bfa4957954697fa50c28bd212014da272bc829c Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 6 Feb 2026 15:38:37 +0800 Subject: [PATCH] refactor(skill-editor): add single/double click and optimize re-renders in search results Extract SearchResultRow component with useDelayedClick to match file tree behavior (single-click preview, double-click pin). Subscribe to derived boolean instead of raw activeTabId to avoid unnecessary re-renders across all rows. --- .../skill/file-tree/search-result-list.tsx | 135 +++++++++++------- 1 file changed, 85 insertions(+), 50 deletions(-) diff --git a/web/app/components/workflow/skill/file-tree/search-result-list.tsx b/web/app/components/workflow/skill/file-tree/search-result-list.tsx index 277082c2a2..945cae0c49 100644 --- a/web/app/components/workflow/skill/file-tree/search-result-list.tsx +++ b/web/app/components/workflow/skill/file-tree/search-result-list.tsx @@ -4,6 +4,7 @@ import type { AppAssetTreeView } from '@/types/app-asset' import { useCallback, useMemo } from 'react' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { cn } from '@/utils/classnames' +import { useDelayedClick } from '../hooks/use-delayed-click' import { flattenMatchingNodes, getAncestorIds } from '../utils/tree-utils' import { TreeNodeIcon } from './tree-node-icon' @@ -12,66 +13,100 @@ type SearchResultListProps = { treeChildren: AppAssetTreeView[] } -const SearchResultList = ({ searchTerm, treeChildren }: SearchResultListProps) => { - const activeTabId = useStore(s => s.activeTabId) - const storeApi = useWorkflowStore() +type SearchResultRowProps = { + node: AppAssetTreeView + parentPath: string + treeChildren: AppAssetTreeView[] +} +const SearchResultRow = ({ node, parentPath, treeChildren }: SearchResultRowProps) => { + const isActive = useStore(s => s.activeTabId === node.id) + const storeApi = useWorkflowStore() + const isFile = node.node_type === 'file' + + const openFilePreview = useCallback(() => { + storeApi.getState().clearArtifactSelection() + storeApi.getState().openTab(node.id, { pinned: false }) + }, [node.id, storeApi]) + + const openFilePinned = useCallback(() => { + storeApi.getState().clearArtifactSelection() + storeApi.getState().openTab(node.id, { pinned: true }) + }, [node.id, storeApi]) + + const { handleClick: handleFileClick, handleDoubleClick: handleFileDoubleClick } = useDelayedClick({ + onSingleClick: openFilePreview, + onDoubleClick: openFilePinned, + }) + + const handleFolderClick = useCallback(() => { + const ancestors = getAncestorIds(node.id, treeChildren) + storeApi.getState().revealFile([...ancestors, node.id]) + storeApi.getState().setFileTreeSearchTerm('') + }, [node.id, storeApi, treeChildren]) + + const handleClick = isFile ? handleFileClick : handleFolderClick + const handleDoubleClick = isFile ? handleFileDoubleClick : undefined + + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + if (isFile) + openFilePinned() + else + handleFolderClick() + } + }} + > +
+
+ +
+ + {node.name} + +
+ {parentPath && ( + + {parentPath} + + )} +
+ ) +} + +const SearchResultList = ({ searchTerm, treeChildren }: SearchResultListProps) => { 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} - - )} -
+ node={node} + parentPath={parentPath} + treeChildren={treeChildren} + /> ))}
)