import React, { useRef, useState, useLayoutEffect, useCallback } from 'react'; import { Tree, type NodeApi } from 'react-arborist'; import { IconFile, IconFolder, IconFolderOpen, IconUpload, } from '@tabler/icons-react'; import { Text, Box } from '@mantine/core'; import useResizeObserver from '@react-hook/resize-observer'; import { useFileOperations } from '../../hooks/useFileOperations'; import type { FileNode } from '@/types/models'; interface Size { width: number; height: number; } interface FileTreeProps { files: FileNode[]; handleFileSelect: (filePath: string | null) => Promise; showHiddenFiles: boolean; loadFileList: () => Promise; } const useSize = (target: React.RefObject): Size | undefined => { const [size, setSize] = useState(); useLayoutEffect(() => { if (target.current) { setSize(target.current.getBoundingClientRect()); } }, [target]); useResizeObserver(target, (entry) => setSize(entry.contentRect)); return size; }; const FileIcon = ({ node }: { node: NodeApi }) => { if (node.isLeaf) { return ; } return node.isOpen ? ( ) : ( ); }; // Enhanced Node component with drag handle function Node({ node, style, dragHandle, onNodeClick, }: { node: NodeApi; style: React.CSSProperties; dragHandle?: React.Ref; onNodeClick?: (node: NodeApi) => void; }) { const handleClick = () => { if (node.isInternal) { node.toggle(); } else if (typeof onNodeClick === 'function') { onNodeClick(node); } }; return (
{node.data.name}
); } // Utility function to recursively find file paths by IDs const findFilePathsById = (files: FileNode[], ids: string[]): string[] => { const paths: string[] = []; const searchFiles = (nodes: FileNode[]) => { for (const node of nodes) { if (ids.includes(node.id)) { paths.push(node.path); } if (node.children) { searchFiles(node.children); } } }; searchFiles(files); return paths; }; // Utility function to find parent path by ID const findParentPathById = ( files: FileNode[], parentId: string | null ): string => { if (!parentId) return ''; const searchFiles = (nodes: FileNode[]): string | null => { for (const node of nodes) { if (node.id === parentId) { return node.path; } if (node.children) { const result = searchFiles(node.children); if (result) return result; } } return null; }; return searchFiles(files) || ''; }; export const FileTree: React.FC = ({ files, handleFileSelect, showHiddenFiles, loadFileList, }) => { const target = useRef(null); const size = useSize(target); const { handleMove, handleUpload } = useFileOperations(); // State for drag and drop overlay const [isDragOver, setIsDragOver] = useState(false); const filteredFiles = files.filter((file) => { if (file.name.startsWith('.') && !showHiddenFiles) { return false; } return true; }); // Handler for node click const onNodeClick = (node: NodeApi) => { const fileNode = node.data; if (!node.isInternal) { void handleFileSelect(fileNode.path); } }; // Handle file movement within the tree const handleTreeMove = useCallback( async ({ dragIds, parentId, index, }: { dragIds: string[]; parentId: string | null; index: number; }) => { try { // Map dragged file IDs to their corresponding paths const dragPaths = findFilePathsById(filteredFiles, dragIds); // Find the parent path where files will be moved const targetParentPath = findParentPathById(filteredFiles, parentId); // Move files to the new location const success = await handleMove(dragPaths, targetParentPath, index); if (success) { await loadFileList(); } } catch (error) { console.error('Error moving files:', error); } }, [handleMove, loadFileList, filteredFiles] ); // External file drag and drop handlers const handleDragEnter = useCallback((e: React.DragEvent) => { // Check if drag contains files (not internal tree nodes) if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); e.stopPropagation(); setIsDragOver(true); } }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { // Only handle if it's an external file drag if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); e.stopPropagation(); // Only hide overlay when leaving the container itself if (e.currentTarget === e.target) { setIsDragOver(false); } } }, []); const handleDragOver = useCallback((e: React.DragEvent) => { // Only handle external file drags if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); e.stopPropagation(); // Set the drop effect to indicate this is a valid drop target e.dataTransfer.dropEffect = 'copy'; } }, []); const handleDrop = useCallback( (e: React.DragEvent) => { const { files } = e.dataTransfer; // Only handle if it's an external file drop if (files && files.length > 0) { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); const uploadFiles = async () => { try { const success = await handleUpload(files); if (success) { await loadFileList(); } } catch (error) { console.error('Error uploading files:', error); } }; void uploadFiles(); } }, [handleUpload, loadFileList] ); return (
{/* Drag overlay */} {isDragOver && ( Drop files here to upload )} {size && ( false} disableDrop={() => false} onActivate={(node) => { const fileNode = node.data; if (!node.isInternal) { void handleFileSelect(fileNode.path); } }} > {(props) => } )}
); }; export default FileTree;