mirror of
https://github.com/langgenius/dify.git
synced 2026-03-11 02:07:50 +08:00
Introduce a dedicated Zustand ArtifactSlice to manage artifact selection state with mutual exclusion against the main file tree. Artifact files from the sandbox can now be opened as tabs in the skill editor, rendered via a lightweight ArtifactContentPanel that reuses ReadOnlyFilePreview.
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import type { NodeApi } from 'react-arborist'
|
|
import type { TreeNodeData } from '../type'
|
|
import { throttle } from 'es-toolkit/function'
|
|
import { useCallback, useMemo, useRef } from 'react'
|
|
import { useWorkflowStore } from '@/app/components/workflow/store'
|
|
import { useDelayedClick } from './use-delayed-click'
|
|
|
|
type UseTreeNodeHandlersOptions = {
|
|
node: NodeApi<TreeNodeData>
|
|
}
|
|
|
|
type UseTreeNodeHandlersReturn = {
|
|
handleClick: (e: React.MouseEvent) => void
|
|
handleDoubleClick: (e: React.MouseEvent) => void
|
|
handleToggle: (e: React.MouseEvent) => void
|
|
handleContextMenu: (e: React.MouseEvent) => void
|
|
handleKeyDown: (e: React.KeyboardEvent) => void
|
|
}
|
|
|
|
export function useTreeNodeHandlers({
|
|
node,
|
|
}: UseTreeNodeHandlersOptions): UseTreeNodeHandlersReturn {
|
|
const storeApi = useWorkflowStore()
|
|
const isFolder = node.data.node_type === 'folder'
|
|
const nodeRef = useRef(node)
|
|
nodeRef.current = node
|
|
|
|
const throttledToggle = useMemo(
|
|
() => throttle(() => nodeRef.current.toggle(), 300, { edges: ['leading'] }),
|
|
[],
|
|
)
|
|
|
|
const openFilePreview = useCallback(() => {
|
|
storeApi.getState().clearArtifactSelection()
|
|
storeApi.getState().openTab(node.data.id, { pinned: false })
|
|
}, [node.data.id, storeApi])
|
|
|
|
const openFilePinned = useCallback(() => {
|
|
storeApi.getState().clearArtifactSelection()
|
|
storeApi.getState().openTab(node.data.id, { pinned: true })
|
|
}, [node.data.id, storeApi])
|
|
|
|
const { handleClick: handleFileClick, handleDoubleClick: handleFileDoubleClick } = useDelayedClick({
|
|
onSingleClick: openFilePreview,
|
|
onDoubleClick: openFilePinned,
|
|
})
|
|
|
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
if (e.shiftKey)
|
|
node.selectContiguous()
|
|
else if (e.metaKey || e.ctrlKey)
|
|
node.selectMulti()
|
|
else
|
|
node.select()
|
|
|
|
if (isFolder)
|
|
throttledToggle()
|
|
else if (!e.metaKey && !e.ctrlKey && !e.shiftKey)
|
|
handleFileClick()
|
|
}, [handleFileClick, isFolder, node, throttledToggle])
|
|
|
|
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
if (isFolder)
|
|
throttledToggle()
|
|
else
|
|
handleFileDoubleClick()
|
|
}, [isFolder, throttledToggle, handleFileDoubleClick])
|
|
|
|
const handleToggle = useCallback((e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
throttledToggle()
|
|
}, [throttledToggle])
|
|
|
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
node.select()
|
|
storeApi.getState().setContextMenu({
|
|
top: e.clientY,
|
|
left: e.clientX,
|
|
type: 'node',
|
|
nodeId: node.data.id,
|
|
isFolder,
|
|
})
|
|
}, [isFolder, node, storeApi])
|
|
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
if (isFolder) {
|
|
node.toggle()
|
|
}
|
|
else {
|
|
storeApi.getState().clearArtifactSelection()
|
|
storeApi.getState().openTab(node.data.id, { pinned: true })
|
|
}
|
|
}
|
|
}, [isFolder, node, storeApi])
|
|
|
|
return {
|
|
handleClick,
|
|
handleDoubleClick,
|
|
handleToggle,
|
|
handleContextMenu,
|
|
handleKeyDown,
|
|
}
|
|
}
|