mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user