/** * 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'; import { Archive, ChevronDown, ChevronUp, Eye, FileText, Image, Loader2, Share2, Trash2 } from 'lucide-react'; export function getFileIcon(mimeType: string): { icon: React.ElementType; color: string } { if (mimeType === 'application/pdf') { return { icon: FileText, color: 'text-danger-500' }; } if (mimeType.startsWith('image/')) { return { icon: Image, color: 'text-accent-500' }; } if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) { return { icon: FileText, color: 'text-success-500' }; } if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) { return { icon: FileText, color: 'text-warning-500' }; } if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) { return { icon: FileText, color: 'text-primary-500' }; } if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) { return { icon: Archive, color: 'text-secondary-500' }; } return { icon: FileText, 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`; } import { formatDateShort } from '@/utils/date'; function getFileSize(file: DmsFile): number { return file.size_bytes ?? file.size ?? 0; } function formatDate(dateStr: string | null | undefined): string { if (!dateStr) return ''; return formatDateShort(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 (
); } if (files.length === 0) { return (
); } // ─── 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')} />
{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 minColWidth = viewMode === 'icons-sm' ? '80px' : viewMode === 'icons-md' ? '120px' : '180px'; const iconSize = viewMode === 'icons-sm' ? 'w-6 h-6' : viewMode === 'icons-md' ? 'w-8 h-8' : 'w-16 h-16'; const isSmall = viewMode === 'icons-sm'; 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'; }} />
) : (
); })}
); }