import { useCallback, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { AlertTriangle, Check, X, Loader2, Folder, Upload } from 'lucide-react'; import { cn } from '../../../lib/utils'; import { ICON_SIZE_CLASS, getFileIconData } from '../constants/fileIcons'; import { useExpandedDirectories } from '../hooks/useExpandedDirectories'; import { useFileTreeData } from '../hooks/useFileTreeData'; import { useFileTreeOperations } from '../hooks/useFileTreeOperations'; import { useFileTreeSearch } from '../hooks/useFileTreeSearch'; import { useFileTreeViewMode } from '../hooks/useFileTreeViewMode'; import { useFileTreeUpload } from '../hooks/useFileTreeUpload'; import type { FileTreeImageSelection, FileTreeNode } from '../types/types'; import { formatFileSize, formatRelativeTime, isImageFile } from '../utils/fileTreeUtils'; import { Project } from '../../../types/app'; import { ScrollArea, Input } from '../../../shared/view/ui'; import FileTreeBody from './FileTreeBody'; import FileTreeDetailedColumns from './FileTreeDetailedColumns'; import FileTreeHeader from './FileTreeHeader'; import FileTreeLoadingState from './FileTreeLoadingState'; import ImageViewer from './ImageViewer'; type FileTreeProps = { selectedProject: Project | null; onFileOpen?: (filePath: string) => void; }; export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps) { const { t } = useTranslation(); const [selectedImage, setSelectedImage] = useState(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const newItemInputRef = useRef(null); const renameInputRef = useRef(null); // Show toast notification const showToast = useCallback((message: string, type: 'success' | 'error') => { setToast({ message, type }); }, []); // Auto-hide toast useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); const { files, loading, refreshFiles } = useFileTreeData(selectedProject); const { viewMode, changeViewMode } = useFileTreeViewMode(); const { expandedDirs, toggleDirectory, expandDirectories, collapseAll } = useExpandedDirectories(); const { searchQuery, setSearchQuery, filteredFiles } = useFileTreeSearch({ files, expandDirectories, }); // File operations const operations = useFileTreeOperations({ selectedProject, onRefresh: refreshFiles, showToast, }); // File upload (drag and drop) const upload = useFileTreeUpload({ selectedProject, onRefresh: refreshFiles, showToast, }); // Focus input when creating new item useEffect(() => { if (operations.isCreating && newItemInputRef.current) { newItemInputRef.current.focus(); newItemInputRef.current.select(); } }, [operations.isCreating]); // Focus input when renaming useEffect(() => { if (operations.renamingItem && renameInputRef.current) { renameInputRef.current.focus(); renameInputRef.current.select(); } }, [operations.renamingItem]); const renderFileIcon = useCallback((filename: string) => { const { icon: Icon, color } = getFileIconData(filename); return ; }, []); // Centralized click behavior keeps file actions identical across all presentation modes. const handleItemClick = useCallback( (item: FileTreeNode) => { if (item.type === 'directory') { toggleDirectory(item.path); return; } if (isImageFile(item.name) && selectedProject) { setSelectedImage({ name: item.name, path: item.path, projectPath: selectedProject.path, // Image URL uses the DB projectId so ImageViewer can hit the // /api/projects/:projectId/files/content endpoint directly. projectId: selectedProject.projectId, }); return; } onFileOpen?.(item.path); }, [onFileOpen, selectedProject, toggleDirectory], ); const formatRelativeTimeLabel = useCallback( (date?: string) => formatRelativeTime(date, t), [t], ); if (loading) { return ; } return (
{/* Drag overlay */} {upload.isDragOver && (
{t('fileTree.dropToUpload', 'Drop files to upload')}
)} operations.handleStartCreate('', 'file')} onNewFolder={() => operations.handleStartCreate('', 'directory')} onRefresh={refreshFiles} onCollapseAll={collapseAll} loading={loading} operationLoading={operations.operationLoading} /> {viewMode === 'detailed' && filteredFiles.length > 0 && } {/* New item input */} {operations.isCreating && (
{operations.newItemType === 'directory' ? ( ) : ( {renderFileIcon(operations.newItemName)} )} operations.setNewItemName(e.target.value)} onKeyDown={(e) => { e.stopPropagation(); if (e.key === 'Enter') operations.handleConfirmCreate(); if (e.key === 'Escape') operations.handleCancelCreate(); }} onBlur={() => { setTimeout(() => { if (operations.isCreating) operations.handleConfirmCreate(); }, 100); }} className="h-6 flex-1 text-sm" disabled={operations.operationLoading} />
)} operations.handleStartCreate(path, 'file')} onNewFolder={(path) => operations.handleStartCreate(path, 'directory')} onCopyPath={operations.handleCopyPath} onDownload={operations.handleDownload} onRefresh={refreshFiles} // Pass rename state and handlers for inline editing renamingItem={operations.renamingItem} renameValue={operations.renameValue} setRenameValue={operations.setRenameValue} handleConfirmRename={operations.handleConfirmRename} handleCancelRename={operations.handleCancelRename} renameInputRef={renameInputRef} operationLoading={operations.operationLoading} />
{selectedImage && ( setSelectedImage(null)} /> )} {/* Delete Confirmation Dialog */} {operations.deleteConfirmation.isOpen && operations.deleteConfirmation.item && (

{t('fileTree.delete.title', 'Delete {{type}}', { type: operations.deleteConfirmation.item.type === 'directory' ? 'Folder' : 'File' })}

{operations.deleteConfirmation.item.name}

{operations.deleteConfirmation.item.type === 'directory' ? t('fileTree.delete.folderWarning', 'This folder and all its contents will be permanently deleted.') : t('fileTree.delete.fileWarning', 'This file will be permanently deleted.')}

)} {/* Toast Notification */} {toast && (
{toast.type === 'success' ? ( ) : ( )} {toast.message}
)}
); }