'use client' import type { NodeRendererProps } from 'react-arborist' import type { TreeNodeData } from '../type' import type { FileAppearanceType } from '@/app/components/base/file-uploader/types' import { RiFolderLine, RiFolderOpenLine, RiMoreFill } from '@remixicon/react' import { throttle } from 'es-toolkit/function' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { cn } from '@/utils/classnames' import { useDelayedClick } from '../hooks/use-delayed-click' import { useSkillEditorStore, useSkillEditorStoreApi } from '../store' import { getFileIconType } from '../utils/file-utils' import FileNodeMenu from './file-node-menu' import FolderNodeMenu from './folder-node-menu' import TreeEditInput from './tree-edit-input' import TreeGuideLines from './tree-guide-lines' const TreeNode = ({ node, style, dragHandle }: NodeRendererProps) => { const { t } = useTranslation('workflow') const isFolder = node.data.node_type === 'folder' const isSelected = node.isSelected const isDirty = useSkillEditorStore(s => s.dirtyContents.has(node.data.id)) const contextMenuNodeId = useSkillEditorStore(s => s.contextMenu?.nodeId) const hasContextMenu = contextMenuNodeId === node.data.id const storeApi = useSkillEditorStoreApi() const [showDropdown, setShowDropdown] = useState(false) const fileIconType = !isFolder ? getFileIconType(node.data.name) : null const throttledToggle = useMemo( () => throttle(() => node.toggle(), 300, { edges: ['leading'] }), [node], ) const openFilePreview = useCallback(() => { storeApi.getState().openTab(node.data.id, { pinned: false }) }, [node.data.id, storeApi]) const openFilePinned = useCallback(() => { storeApi.getState().openTab(node.data.id, { pinned: true }) }, [node.data.id, storeApi]) const { handleClick: handleFileClick, handleDoubleClick: handleFileDoubleClick } = useDelayedClick({ onSingleClick: openFilePreview, onDoubleClick: openFilePinned, }) const handleClick = (e: React.MouseEvent) => { e.stopPropagation() node.select() if (isFolder) throttledToggle() else handleFileClick() } const handleDoubleClick = (e: React.MouseEvent) => { e.stopPropagation() if (isFolder) throttledToggle() else handleFileDoubleClick() } const handleToggle = (e: React.MouseEvent) => { e.stopPropagation() throttledToggle() } const handleContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() storeApi.getState().setContextMenu({ top: e.clientY, left: e.clientX, nodeId: node.data.id, }) }, [node.data.id, storeApi]) const handleMoreClick = useCallback((e: React.MouseEvent) => { e.stopPropagation() setShowDropdown(prev => !prev) }, []) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() if (isFolder) node.toggle() else storeApi.getState().openTab(node.data.id, { pinned: true }) } }, [isFolder, node, storeApi]) return (
{isFolder ? ( ) : (
{isDirty && ( )}
)}
{node.isEditing ? ( ) : ( {node.data.name} )} {isFolder ? ( setShowDropdown(false)} node={node} /> ) : ( setShowDropdown(false)} node={node} /> )}
) } export default React.memo(TreeNode)