'use client' import type { NodeRendererProps } from 'react-arborist' import type { TreeNodeData } from '../type' import { RiMoreFill } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { cn } from '@/utils/classnames' import { useFolderFileDrop } from '../hooks/use-folder-file-drop' import { useTreeNodeHandlers } from '../hooks/use-tree-node-handlers' import NodeMenu from './node-menu' import TreeEditInput from './tree-edit-input' import TreeGuideLines from './tree-guide-lines' import { TreeNodeIcon } from './tree-node-icon' type TreeNodeProps = NodeRendererProps & { treeChildren: TreeNodeData[] } const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => { const { t } = useTranslation('workflow') const isFolder = node.data.node_type === 'folder' const isSelected = node.isSelected const isDirty = useStore(s => s.dirtyContents.has(node.data.id)) const isCut = useStore(s => s.isCutNode(node.data.id)) const contextMenuNodeId = useStore(s => s.contextMenu?.nodeId) const hasContextMenu = contextMenuNodeId === node.data.id const storeApi = useWorkflowStore() const [showDropdown, setShowDropdown] = useState(false) // Sync react-arborist drag state to Zustand for DragActionTooltip const prevIsDragging = useRef(node.isDragging) useEffect(() => { // When drag starts if (node.isDragging && !prevIsDragging.current) storeApi.getState().setCurrentDragType('move') // When drag ends if (!node.isDragging && prevIsDragging.current) { storeApi.getState().setCurrentDragType(null) storeApi.getState().setDragOverFolderId(null) } prevIsDragging.current = node.isDragging }, [node.isDragging, storeApi]) // Sync react-arborist willReceiveDrop to Zustand for DragActionTooltip const prevWillReceiveDrop = useRef(node.willReceiveDrop) useEffect(() => { // When willReceiveDrop becomes true, set dragOverFolderId if (isFolder && node.willReceiveDrop && !prevWillReceiveDrop.current) storeApi.getState().setDragOverFolderId(node.data.id) // When willReceiveDrop becomes false, clear if this node was the target if (isFolder && !node.willReceiveDrop && prevWillReceiveDrop.current) { const currentDragOverId = storeApi.getState().dragOverFolderId if (currentDragOverId === node.data.id) storeApi.getState().setDragOverFolderId(null) } prevWillReceiveDrop.current = node.willReceiveDrop }, [isFolder, node.willReceiveDrop, node.data.id, storeApi]) const { handleClick, handleDoubleClick, handleToggle, handleContextMenu, handleKeyDown, } = useTreeNodeHandlers({ node }) // Get file drop visual state (for external file uploads) const { isDragOver: isFileDragOver, isBlinking, dragHandlers } = useFolderFileDrop({ node, treeChildren }) // Combine internal drag target (willReceiveDrop) with external file drag (isFileDragOver) const isDragOver = isFileDragOver || (isFolder && node.willReceiveDrop) const handleMoreClick = useCallback((e: React.MouseEvent) => { e.stopPropagation() setShowDropdown(prev => !prev) }, []) return (
{/* Main content area - isolated click/double-click handling */}
{node.isEditing ? ( ) : ( {node.data.name} )}
{/* More button - separate from main content click handling */} setShowDropdown(false)} node={node} />
) } export default React.memo(TreeNode)