import React from 'react'; import { ChevronRight, CornerUpLeft, Folder, FolderOpen, Loader2 } from 'lucide-react'; import type { SftpFileEntry } from '../../types'; import { formatBytes, formatDate, getFileIcon, isNavigableDirectory, type SftpColumnVisibility } from './utils'; import { useI18n } from '../../application/i18n/I18nProvider'; import { cn } from '../../lib/utils'; export type NodeDescriptor = | { type: 'node'; entry: SftpFileEntry; entryPath: string; depth: number; isExpanded: boolean; isLoading: boolean } | { type: 'loading' | 'error'; key: string; depth: number }; // ── Simplified TreeNode (no per-node ContextMenu) ──────────────────── interface TreeNodeProps { entry: SftpFileEntry; entryPath: string; depth: number; columnTemplate: string; visibleColumns: SftpColumnVisibility; isSelected: boolean; isExpanded: boolean; isLoading: boolean; isDragOver: boolean; onToggleExpand: (entry: SftpFileEntry, entryPath: string) => void; onNodeClick: (entry: SftpFileEntry, entryPath: string, e: React.MouseEvent) => void; onOpenEntry: (entry: SftpFileEntry, entryPath: string) => void; onDragStart: (entry: SftpFileEntry, entryPath: string, isDir: boolean, e: React.DragEvent) => void; onDragEnd: () => void; onDragOverEntry: (entryPath: string, e: React.DragEvent) => void; onDropEntry: (entryPath: string, e: React.DragEvent) => void; onDragLeaveEntry: () => void; onContextMenu: (entry: SftpFileEntry, entryPath: string, e: React.MouseEvent) => void; } export const TREE_ROW_HEIGHT = 28; export const TreeNode = React.memo(({ entry, entryPath, depth, columnTemplate, visibleColumns, isSelected, isExpanded, isLoading, isDragOver, onToggleExpand, onNodeClick, onOpenEntry, onDragStart, onDragEnd, onDragOverEntry, onDropEntry, onDragLeaveEntry, onContextMenu, }) => { const { t } = useI18n(); const isParentEntry = entry.name === '..'; const isDir = isNavigableDirectory(entry); const icon = isDir ? (isExpanded ? : ) : getFileIcon(entry); return (
onNodeClick(entry, entryPath, e)} onDoubleClick={() => onOpenEntry(entry, entryPath)} onContextMenu={e => { if (!isParentEntry) { onContextMenu(entry, entryPath, e); } }} draggable={!isParentEntry} onDragStart={e => { if (!isParentEntry) onDragStart(entry, entryPath, isDir, e); }} onDragEnd={onDragEnd} onDragOver={e => onDragOverEntry(entryPath, e)} onDrop={e => onDropEntry(entryPath, e)} onDragLeave={onDragLeaveEntry} >
{isParentEntry ? ( ) : isDir ? ( isLoading ? ( ) : ( { e.stopPropagation(); void onToggleExpand(entry, entryPath); }} /> ) ) : null} {!isParentEntry && {icon}} {entry.name}
{visibleColumns.modified && ( {isParentEntry ? '' : formatDate(entry.lastModified)} )} {visibleColumns.size && ( {isParentEntry ? '' : (isDir ? '--' : formatBytes(entry.size ?? 0))} )} {visibleColumns.type && ( {isParentEntry ? '' : (isDir ? t('sftp.kind.folder') : (entry.name.split('.').pop()?.toUpperCase() ?? '--'))} )} {visibleColumns.owner && ( {isParentEntry ? '' : (entry.owner || '--')} )}
); }); TreeNode.displayName = 'TreeNode'; // ── Tree paths reducer (unchanged) ──────────────────────────────────