/** * FilesPage * * Split-view file management: FolderTree on the left, FormGeneratorTable on the right. * Uses useResizablePanels for the divider. */ import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react'; import api from '../../api'; import { useUserFiles, useFileOperations } from '../../hooks/useFiles'; import { useFileContext } from '../../contexts/FileContext'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import FolderTree from '../../components/FolderTree/FolderTree'; import type { FileNode } from '../../components/FolderTree/FolderTree'; import { useResizablePanels } from '../../hooks/useResizablePanels'; import { FaSync, FaFolder, FaUpload, FaDownload, FaEye, FaFolderPlus } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import styles from '../admin/Admin.module.css'; interface UserFile { id: string; fileName: string; mimeType?: string; fileSize?: number; folderId?: string | null; featureInstanceId?: string; [key: string]: any; } export const FilesPage: React.FC = () => { const fileInputRef = useRef(null); const { showSuccess, showError } = useToast(); const [selectedFolderId, setSelectedFolderId] = useState(null); const { leftWidth, isDragging, handleMouseDown, containerRef, } = useResizablePanels({ storageKey: 'filesPage-panelWidth', defaultLeftWidth: 22, minLeftWidth: 15, maxLeftWidth: 40, }); const { data: files, attributes, permissions, loading, error, refetch, pagination, fetchFileById, updateFileOptimistically, } = useUserFiles(); const { handleFileDownload, handleFileDelete, handleFileDeleteMultiple, handleFileUpload, handleFileUpdate, handleFilePreview, handleInlineUpdate, deletingFiles, downloadingFiles, uploadingFile, previewingFiles, } = useFileOperations(); const { folders, refreshFolders, handleCreateFolder, handleRenameFolder, handleDeleteFolder, handleMoveFolder, handleMoveFolders, handleMoveFile, handleMoveFiles: contextMoveFiles, handleDownloadFolder, expandedFolderIds, toggleFolderExpanded, } = useFileContext(); const [editingFile, setEditingFile] = useState(null); const [selectedFiles, setSelectedFiles] = useState([]); const [treeSelectedIds, setTreeSelectedIds] = useState>(new Set()); const [highlightedFileId, setHighlightedFileId] = useState(null); useEffect(() => { refetch(); }, []); const treeFileNodes: FileNode[] = useMemo(() => { if (!files) return []; return files.map((f: UserFile) => ({ id: f.id, fileName: f.fileName, mimeType: f.mimeType, fileSize: f.fileSize, folderId: f.folderId ?? null, })); }, [files]); const _handleTreeFileSelect = useCallback((fileId: string) => { const file = files?.find((f: UserFile) => f.id === fileId); if (file) { setSelectedFolderId(file.folderId ?? null); setHighlightedFileId(fileId); requestAnimationFrame(() => { const row = document.querySelector('tr[data-highlighted="true"]'); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); setTimeout(() => setHighlightedFileId(null), 2500); } }, [files]); const filteredFiles = useMemo(() => { if (!files) return []; if (selectedFolderId === null) { return files.filter((f: UserFile) => !f.folderId); } return files.filter((f: UserFile) => f.folderId === selectedFolderId); }, [files, selectedFolderId]); const columns = useMemo(() => { const hiddenColumns = ['id', 'mandateId', 'fileHash', 'folderId']; const cols = (attributes || []) .filter(attr => !hiddenColumns.includes(attr.name)) .map(attr => ({ key: attr.name, label: attr.label || attr.name, type: attr.type as any, sortable: attr.sortable !== false, filterable: attr.filterable !== false, searchable: attr.searchable !== false, width: attr.width || 150, minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, fkSource: (attr as any).fkSource, fkDisplayField: (attr as any).fkDisplayField, })); cols.push({ key: '_createdBy', label: 'Created By', type: 'text' as any, sortable: true, filterable: false, searchable: false, width: 150, minWidth: 100, maxWidth: 250, } as any); return cols; }, [attributes]); const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; const handleEditClick = async (file: UserFile) => { const fullFile = await fetchFileById(file.id); if (fullFile) setEditingFile(fullFile as UserFile); }; const handleEditSubmit = async (data: Partial) => { if (!editingFile) return; const result = await handleFileUpdate(editingFile.id, { fileName: data.fileName || editingFile.fileName }, editingFile); if (result.success) { setEditingFile(null); refetch(); } }; const handleDelete = async (file: UserFile) => { const success = await handleFileDelete(file.id); if (success) refetch(); }; const handleDeleteMultiple = async (filesToDelete: UserFile[]) => { const ids = filesToDelete.map(f => f.id); const success = await handleFileDeleteMultiple(ids); if (success) refetch(); }; const handleDownload = async (file: UserFile) => { await handleFileDownload(file.id, file.fileName); }; const handlePreview = async (file: UserFile) => { const result = await handleFilePreview(file.id, file.fileName, file.mimeType); if (result.success && result.previewUrl) { window.open(result.previewUrl, '_blank'); } }; const handleUploadClick = () => { fileInputRef.current?.click(); }; const handleFileSelect = async (e: React.ChangeEvent) => { const selectedFiles = e.target.files; if (selectedFiles && selectedFiles.length > 0) { let successCount = 0; let errorCount = 0; for (const file of Array.from(selectedFiles)) { const result = await handleFileUpload(file); if (result?.success) successCount++; else errorCount++; } if (fileInputRef.current) fileInputRef.current.value = ''; await refetch(); if (successCount > 0) { showSuccess( 'Upload erfolgreich', `${successCount} Datei(en) hochgeladen${errorCount > 0 ? `, ${errorCount} fehlgeschlagen` : ''}` ); } else if (errorCount > 0) { showError('Upload fehlgeschlagen', `${errorCount} Datei(en) konnten nicht hochgeladen werden`); } } }; const _handleNewFolder = useCallback(async () => { const name = prompt('Neuer Ordnername:'); if (name?.trim()) { await handleCreateFolder(name.trim(), selectedFolderId); } }, [handleCreateFolder, selectedFolderId]); const _onRowDragStart = useCallback((e: React.DragEvent, row: UserFile) => { const isInSelection = selectedFiles.some(f => f.id === row.id); if (isInSelection && selectedFiles.length > 1) { const ids = selectedFiles.map(f => f.id); e.dataTransfer.setData('application/file-ids', JSON.stringify(ids)); } else { e.dataTransfer.setData('application/file-id', row.id); } e.dataTransfer.effectAllowed = 'move'; }, [selectedFiles]); const _handleMoveFilePage = useCallback(async (fileId: string, targetFolderId: string | null) => { await handleMoveFile(fileId, targetFolderId); await refetch(); }, [handleMoveFile, refetch]); const _handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => { await contextMoveFiles(fileIds, targetFolderId); await refetch(); }, [contextMoveFiles, refetch]); const _handleRenameFile = useCallback(async (fileId: string, newName: string) => { await handleFileUpdate(fileId, { fileName: newName }); await refetch(); }, [handleFileUpdate, refetch]); const _handleDeleteTreeFile = useCallback(async (fileId: string) => { await handleFileDelete(fileId); await refetch(); }, [handleFileDelete, refetch]); const _handleDeleteTreeFiles = useCallback(async (fileIds: string[]) => { await handleFileDeleteMultiple(fileIds); await refetch(); }, [handleFileDeleteMultiple, refetch]); const _handleDeleteTreeFolders = useCallback(async (folderIds: string[]) => { await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true }); await refreshFolders(); await refetch(); }, [refreshFolders, refetch]); const _handleTreeRefresh = useCallback(async () => { await refetch(); await refreshFolders(); }, [refetch, refreshFolders]); const _tableRefetch = useCallback(async (params?: any) => { const nextParams = { ...(params || {}) }; const nextFilters = { ...(nextParams.filters || {}) }; nextFilters.folderId = selectedFolderId; nextParams.filters = nextFilters; await refetch(nextParams); }, [refetch, selectedFolderId]); useEffect(() => { _tableRefetch({ page: 1, pageSize: 25 }); }, [selectedFolderId, _tableRefetch]); const formAttributes = useMemo(() => { const excludedFields = ['id', 'mandateId', 'fileHash', '_createdBy', '_createdAt', '_modifiedAt', 'creationDate', 'source']; return (attributes || []).filter(attr => !excludedFields.includes(attr.name)); }, [attributes]); if (error) { return (
⚠️

Fehler beim Laden der Dateien: {error}

); } return (

Dateien

Dateiverwaltung

{/* Split-view container */}
} style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0, position: 'relative' }} > {/* Left panel: FolderTree */}
{ await handleDeleteFolder(folderId); if (selectedFolderId === folderId) setSelectedFolderId(null); await refetch(); }} onMoveFolder={handleMoveFolder} onMoveFolders={handleMoveFolders} onMoveFile={_handleMoveFilePage} onMoveFiles={_handleMoveFiles} onRenameFile={_handleRenameFile} onDeleteFile={_handleDeleteTreeFile} onDeleteFiles={_handleDeleteTreeFiles} onDeleteFolders={_handleDeleteTreeFolders} onDownloadFolder={handleDownloadFolder} />
{/* Resizable divider */}
{ (e.target as HTMLElement).style.background = 'var(--color-border-hover, #bbb)'; }} onMouseLeave={(e) => { if (!isDragging) (e.target as HTMLElement).style.background = 'transparent'; }} /> {/* Right panel: File table */}
{/* Toolbar above table */}
{canCreate && ( )}
{/* Table content */}
{loading && (!files || files.length === 0) ? (
Lade Dateien...
) : filteredFiles.length === 0 ? (

{selectedFolderId ? 'Ordner ist leer' : 'Keine Dateien vorhanden'}

{selectedFolderId ? 'Verschieben Sie Dateien hierher oder laden Sie neue hoch.' : 'Laden Sie eine Datei hoch, um loszulegen.'}

{canCreate && ( )}
) : ( setSelectedFiles(rows as UserFile[])} rowDraggable={true} onRowDragStart={_onRowDragStart} getRowDataAttributes={(row: UserFile) => ({ highlighted: row.id === highlightedFileId ? 'true' : 'false' }) } actionButtons={[ ...(canUpdate ? [{ type: 'edit' as const, onAction: handleEditClick, title: 'Bearbeiten', }] : []), ...(canDelete ? [{ type: 'delete' as const, title: 'Löschen', loading: (row: UserFile) => deletingFiles.has(row.id), }] : []), ]} customActions={[ { id: 'download', icon: , onClick: handleDownload, title: 'Herunterladen', loading: (row: UserFile) => downloadingFiles.has(row.id), }, { id: 'preview', icon: , onClick: handlePreview, title: 'Vorschau', loading: (row: UserFile) => previewingFiles.has(row.id), }, ]} onDelete={handleDelete} onDeleteMultiple={handleDeleteMultiple} hookData={{ refetch: _tableRefetch, pagination, permissions, handleDelete: handleFileDelete, handleInlineUpdate, updateOptimistically: updateFileOptimistically, }} emptyMessage="Keine Dateien gefunden" /> )}
{/* Edit Modal */} {editingFile && (
setEditingFile(null)}>
e.stopPropagation()}>

Datei bearbeiten

{formAttributes.length === 0 ? (
Lade Formular...
) : ( setEditingFile(null)} submitButtonText="Speichern" cancelButtonText="Abbrechen" /> )}
)}
); }; export default FilesPage;