feat(skill-editor): render flat search result list in file tree

Replace the tree-filtered search with a flat list that shows icon + name
on the left and parent path on the right, matching the Figma design.
Clicking a file opens its tab; clicking a folder clears the search and
reveals the folder in the tree.
This commit is contained in:
yyh
2026-02-06 15:31:17 +08:00
parent ad3a5ad473
commit f1100b82f9
3 changed files with 123 additions and 0 deletions

View File

@@ -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 (
<div className={cn('flex min-h-[150px] flex-1 flex-col overflow-y-auto', className)}>
<SearchResultList
searchTerm={searchTerm}
treeChildren={treeChildren as AppAssetTreeView[]}
/>
</div>
)
}
return (
<>
<div

View File

@@ -0,0 +1,80 @@
'use client'
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 { flattenMatchingNodes, getAncestorIds } from '../utils/tree-utils'
import { TreeNodeIcon } from './tree-node-icon'
type SearchResultListProps = {
searchTerm: string
treeChildren: AppAssetTreeView[]
}
const SearchResultList = ({ searchTerm, treeChildren }: SearchResultListProps) => {
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 (
<div className="flex flex-col gap-px p-1">
{results.map(({ node, parentPath }) => (
<div
key={node.id}
role="button"
tabIndex={0}
className={cn(
'flex h-6 w-full cursor-pointer items-center rounded-md px-2',
'hover:bg-state-base-hover',
activeTabId === node.id && 'bg-state-base-active',
)}
onClick={() => handleClick(node)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleClick(node)
}
}}
>
<div className="flex min-w-0 flex-1 items-center">
<div className="flex size-5 shrink-0 items-center justify-center">
<TreeNodeIcon
isFolder={node.node_type === 'folder'}
isOpen={false}
fileName={node.name}
extension={node.extension}
isDirty={false}
/>
</div>
<span className="min-w-0 truncate px-1 py-0.5 text-[13px] font-normal leading-4 text-text-secondary">
{node.name}
</span>
</div>
{parentPath && (
<span className="system-xs-regular shrink-0 text-text-tertiary">
{parentPath}
</span>
)}
</div>
))}
</div>
)
}
export default SearchResultList

View File

@@ -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,