/** * FileExplorer - middle column file browser with multiple view modes. * Replaces FileGrid. Supports: list, table, icons-sm, icons-md, icons-lg. */ import React, { useMemo } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import type { DmsFile } from '@/api/dms'; export function getFileIcon(mimeType: string): { path: string; color: string } { if (mimeType === 'application/pdf') { return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-danger-500' }; } if (mimeType.startsWith('image/')) { return { path: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-accent-500' }; } if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) { return { path: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-success-500' }; } if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) { return { path: 'M7 8h10M7 16h10M7 12h6m-7 8h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-warning-500' }; } if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) { return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-primary-500' }; } if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) { return { path: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4', color: 'text-secondary-500' }; } return { path: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', color: 'text-secondary-400' }; } export function formatFileSize(bytes: number): string { if (!bytes || bytes < 0) return '0 B'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } function getFileSize(file: DmsFile): number { return file.size_bytes ?? file.size ?? 0; } function formatDate(dateStr: string | null | undefined): string { if (!dateStr) return ''; try { const d = new Date(dateStr); return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return dateStr; } } export type ViewMode = 'list' | 'table' | 'icons-sm' | 'icons-md' | 'icons-lg'; export type SortBy = 'name' | 'size' | 'date' | 'type'; export type SortOrder = 'asc' | 'desc'; export interface FileExplorerProps { files: DmsFile[]; selectedFileIds: Set; selectedFile: DmsFile | null; viewMode: ViewMode; onToggleSelect: (fileId: string) => void; onFileClick: (file: DmsFile) => void; onFileDoubleClick: (file: DmsFile) => void; onFileShare: (file: DmsFile) => void; onFileDelete: (file: DmsFile) => void; onFilePreview: (file: DmsFile) => void; loading?: boolean; sortBy: SortBy; sortOrder: SortOrder; onSortChange: (by: string, order: string) => void; } function SortHeader({ label, field, sortBy, sortOrder, onSortChange, className, }: { label: string; field: SortBy; sortBy: SortBy; sortOrder: SortOrder; onSortChange: (by: string, order: string) => void; className?: string; }) { const isActive = sortBy === field; return ( ); } function ActionButtons({ file, onFilePreview, onFileShare, onFileDelete, t, }: { file: DmsFile; onFilePreview: (file: DmsFile) => void; onFileShare: (file: DmsFile) => void; onFileDelete: (file: DmsFile) => void; t: (key: string) => string; }) { return (
); } export function FileExplorer({ files, selectedFileIds, selectedFile, viewMode, onToggleSelect, onFileClick, onFileDoubleClick, onFileShare, onFileDelete, onFilePreview, loading = false, sortBy, sortOrder, onSortChange, }: FileExplorerProps) { const { t } = useTranslation(); const sortedFiles = useMemo(() => { const sorted = [...files]; sorted.sort((a, b) => { let cmp = 0; if (sortBy === 'name') { cmp = a.name.localeCompare(b.name); } else if (sortBy === 'size') { cmp = getFileSize(a) - getFileSize(b); } else if (sortBy === 'date') { cmp = (a.updated_at || a.created_at || '').localeCompare(b.updated_at || b.created_at || ''); } else if (sortBy === 'type') { cmp = a.mime_type.localeCompare(b.mime_type); } return sortOrder === 'asc' ? cmp : -cmp; }); return sorted; }, [files, sortBy, sortOrder]); if (loading) { return (
{t('dms.loading')}
); } if (files.length === 0) { return (

{t('dms.noFiles')}

); } // ─── List View ─── if (viewMode === 'list') { return (
{/* Sort header */}
| |
); } // ─── Table View ─── if (viewMode === 'table') { return (
{sortedFiles.map((file) => { const icon = getFileIcon(file.mime_type); const isSelected = selectedFileIds.has(file.id); const isActive = selectedFile?.id === file.id; return ( onFileClick(file)} onDoubleClick={() => onFileDoubleClick(file)} data-testid={`file-row-${file.id}`} > ); })}
{}} /> {t('common.actions')}
e.stopPropagation()}> onToggleSelect(file.id)} className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500" aria-label={t('dms.bulkSelect')} />
{file.name}
{formatFileSize(getFileSize(file))} {file.mime_type.split('/')[1]?.toUpperCase() || t('dms.icon.other')} {formatDate(file.updated_at || file.created_at)} e.stopPropagation()}>
); } // ─── Icon Views ─── const gridClass = viewMode === 'icons-sm' ? 'grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8' : viewMode === 'icons-md' ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'; const iconSize = viewMode === 'icons-sm' ? 'w-8 h-8' : viewMode === 'icons-md' ? 'w-10 h-10' : 'w-16 h-16'; return (
{sortedFiles.map((file) => { const icon = getFileIcon(file.mime_type); const isSelected = selectedFileIds.has(file.id); const isActive = selectedFile?.id === file.id; return (
onFileClick(file)} onDoubleClick={() => onFileDoubleClick(file)} data-testid={`file-card-${file.id}`} >
onToggleSelect(file.id)} onClick={(e) => e.stopPropagation()} className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500" aria-label={t('dms.bulkSelect')} />
{viewMode === 'icons-lg' && file.mime_type.startsWith('image/') ? (
{file.name} { (e.target as HTMLImageElement).style.display = 'none'; }} />
) : ( )}

{file.name}

{formatFileSize(getFileSize(file))}
); })}
); }