mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
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.
This commit is contained in:
@ -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 (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'flex h-6 w-full cursor-pointer items-center rounded-md px-2',
|
||||
'hover:bg-state-base-hover',
|
||||
isActive && 'bg-state-base-active',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
if (isFile)
|
||||
openFilePinned()
|
||||
else
|
||||
handleFolderClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<div className="flex size-5 shrink-0 items-center justify-center">
|
||||
<TreeNodeIcon
|
||||
isFolder={!isFile}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-px p-1">
|
||||
{results.map(({ node, parentPath }) => (
|
||||
<div
|
||||
<SearchResultRow
|
||||
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>
|
||||
node={node}
|
||||
parentPath={parentPath}
|
||||
treeChildren={treeChildren}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user