564 lines
22 KiB
TypeScript
564 lines
22 KiB
TypeScript
/**
|
||
* FilesPage
|
||
*
|
||
* Split-view file management: FolderTree on the left, FormGeneratorTable on the right.
|
||
* The tree is the master – it dictates which folder's files the table shows (paginated).
|
||
* Tree files are managed by FileContext (lazy-loaded per expanded folder).
|
||
*/
|
||
|
||
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 { useResizablePanels } from '../../hooks/useResizablePanels';
|
||
import { FaSync, FaUpload, FaDownload, FaFolderPlus } from 'react-icons/fa';
|
||
import { useToast } from '../../contexts/ToastContext';
|
||
import { usePrompt } from '../../hooks/usePrompt';
|
||
import styles from '../admin/Admin.module.css';
|
||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||
import { getUserDataCache } from '../../utils/userCache';
|
||
|
||
interface UserFile {
|
||
id: string;
|
||
fileName: string;
|
||
mimeType?: string;
|
||
fileSize?: number;
|
||
folderId?: string | null;
|
||
featureInstanceId?: string;
|
||
[key: string]: any;
|
||
}
|
||
|
||
export const FilesPage: React.FC = () => {
|
||
const { t } = useLanguage();
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const { showSuccess, showError } = useToast();
|
||
const { prompt: promptInput, PromptDialog } = usePrompt();
|
||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||
|
||
const {
|
||
leftWidth, isDragging, handleMouseDown, containerRef,
|
||
} = useResizablePanels({
|
||
storageKey: 'filesPage-panelWidth',
|
||
defaultLeftWidth: 22,
|
||
minLeftWidth: 15,
|
||
maxLeftWidth: 40,
|
||
});
|
||
|
||
// ── Table data (paginated, filtered by selectedFolderId) ──────────────
|
||
const {
|
||
data: tableFiles,
|
||
attributes,
|
||
permissions,
|
||
loading: tableLoading,
|
||
error,
|
||
refetch: tableRefetch,
|
||
pagination,
|
||
fetchFileById,
|
||
updateFileOptimistically,
|
||
} = useUserFiles();
|
||
|
||
const {
|
||
handleFileDownload,
|
||
handleFileDelete,
|
||
handleFileDeleteMultiple,
|
||
handleFileUpload,
|
||
handleFileUpdate,
|
||
handleInlineUpdate,
|
||
deletingFiles,
|
||
downloadingFiles,
|
||
uploadingFile,
|
||
previewingFiles,
|
||
} = useFileOperations();
|
||
|
||
// ── Tree data (from FileContext – lazy-loaded per expanded folder) ─────
|
||
const {
|
||
folders,
|
||
refreshFolders,
|
||
treeFileNodes,
|
||
refreshTreeFiles,
|
||
updateTreeFileNode,
|
||
expandedFolderIds,
|
||
toggleFolderExpanded,
|
||
handleCreateFolder,
|
||
handleRenameFolder,
|
||
handleDeleteFolder,
|
||
handleMoveFolder,
|
||
handleMoveFolders,
|
||
handleMoveFile: contextMoveFile,
|
||
handleMoveFiles: contextMoveFiles,
|
||
handleDownloadFolder,
|
||
} = useFileContext();
|
||
|
||
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
|
||
const [selectedFiles, setSelectedFiles] = useState<UserFile[]>([]);
|
||
const [treeSelectedIds, setTreeSelectedIds] = useState<Set<string>>(new Set());
|
||
const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null);
|
||
|
||
// ── Table refetch: filter by real folderId ───────────────────────────
|
||
const _tableRefetch = useCallback(async (params?: any) => {
|
||
const nextParams = { ...(params || {}) };
|
||
const nextFilters = { ...(nextParams.filters || {}) };
|
||
|
||
if (!selectedFolderId) {
|
||
nextFilters.folderId = null;
|
||
} else {
|
||
nextFilters.folderId = selectedFolderId;
|
||
}
|
||
|
||
nextParams.filters = nextFilters;
|
||
await tableRefetch(nextParams);
|
||
}, [tableRefetch, selectedFolderId]);
|
||
|
||
useEffect(() => {
|
||
_tableRefetch({ page: 1, pageSize: 25 });
|
||
}, [selectedFolderId, _tableRefetch]);
|
||
|
||
const _refreshAll = useCallback(async () => {
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
}, [_tableRefetch, refreshTreeFiles, refreshFolders]);
|
||
|
||
const _handleScopeChange = useCallback(async (fileId: string, newScope: string) => {
|
||
updateTreeFileNode(fileId, { scope: newScope });
|
||
try {
|
||
await api.patch(`/api/files/${fileId}/scope`, { scope: newScope });
|
||
_tableRefetch();
|
||
} catch (err) {
|
||
console.error('Failed to update scope:', err);
|
||
await Promise.all([refreshTreeFiles(), _tableRefetch()]);
|
||
}
|
||
}, [updateTreeFileNode, refreshTreeFiles, _tableRefetch]);
|
||
|
||
const _handleNeutralizeToggle = useCallback(async (fileId: string, newValue: boolean) => {
|
||
updateTreeFileNode(fileId, { neutralize: newValue });
|
||
try {
|
||
await api.patch(`/api/files/${fileId}/neutralize`, { neutralize: newValue });
|
||
_tableRefetch();
|
||
} catch (err) {
|
||
console.error('Failed to toggle neutralize:', err);
|
||
await Promise.all([refreshTreeFiles(), _tableRefetch()]);
|
||
}
|
||
}, [updateTreeFileNode, refreshTreeFiles, _tableRefetch]);
|
||
|
||
// ── Folder nodes for tree (real folders only) ────────────────────────
|
||
const folderNodes = useMemo(() => {
|
||
return folders.map(f => ({
|
||
id: f.id,
|
||
name: f.name,
|
||
parentId: f.parentId ?? null,
|
||
fileCount: f.fileCount ?? 0,
|
||
}));
|
||
}, [folders]);
|
||
|
||
const selectedFolderName = useMemo(() => {
|
||
if (!selectedFolderId) return null;
|
||
return folders.find(f => f.id === selectedFolderId)?.name ?? null;
|
||
}, [folders, selectedFolderId]);
|
||
|
||
const emptyTableMessage = useMemo(() => {
|
||
if (!selectedFolderId) {
|
||
return t('Keine Dateien gefunden');
|
||
}
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem', alignItems: 'center' }}>
|
||
<div style={{ fontWeight: 600 }}>
|
||
{selectedFolderName
|
||
? t('Der Ordner „{name}" ist leer.', { name: selectedFolderName })
|
||
: t('Dieser Ordner ist leer.')}
|
||
</div>
|
||
<div style={{ color: 'var(--text-muted, #64748b)' }}>
|
||
{t('Lade eine neue Datei hoch oder verschiebe bestehende Dateien hierher.')}
|
||
</div>
|
||
</div>
|
||
);
|
||
}, [selectedFolderId, selectedFolderName, t]);
|
||
|
||
// ── Columns ───────────────────────────────────────────────────────────
|
||
const columns = useMemo(() => {
|
||
const hiddenColumns = ['id', '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: 'sysCreatedBy',
|
||
label: t('Erstellt von'),
|
||
type: 'text' as any,
|
||
sortable: true,
|
||
filterable: true,
|
||
searchable: true,
|
||
width: 150,
|
||
minWidth: 100,
|
||
maxWidth: 250,
|
||
fkSource: '/api/users/',
|
||
fkDisplayField: 'username',
|
||
} as any);
|
||
return cols;
|
||
}, [attributes, t]);
|
||
|
||
const canCreate = permissions?.create !== 'n';
|
||
const canUpdate = permissions?.update !== 'n';
|
||
const canDelete = permissions?.delete !== 'n';
|
||
|
||
const currentUserId = useMemo(() => getUserDataCache()?.id || '', []);
|
||
const _isOwned = useCallback((row: UserFile) => row.sysCreatedBy === currentUserId, [currentUserId]);
|
||
|
||
// ── Tree event handlers ───────────────────────────────────────────────
|
||
const _handleTreeFileSelect = useCallback((fileId: string) => {
|
||
const file = treeFileNodes.find(f => 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);
|
||
}
|
||
}, [treeFileNodes]);
|
||
|
||
const _handleMoveFile = useCallback(async (fileId: string, targetFolderId: string | null) => {
|
||
await contextMoveFile(fileId, targetFolderId);
|
||
await _tableRefetch();
|
||
}, [contextMoveFile, _tableRefetch]);
|
||
|
||
const _handleMoveFiles = useCallback(async (fileIds: string[], targetFolderId: string | null) => {
|
||
await contextMoveFiles(fileIds, targetFolderId);
|
||
await _tableRefetch();
|
||
}, [contextMoveFiles, _tableRefetch]);
|
||
|
||
const _handleRenameFile = useCallback(async (fileId: string, newName: string) => {
|
||
await handleFileUpdate(fileId, { fileName: newName });
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles()]);
|
||
}, [handleFileUpdate, _tableRefetch, refreshTreeFiles]);
|
||
|
||
const _handleDeleteTreeFile = useCallback(async (fileId: string) => {
|
||
await handleFileDelete(fileId);
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
}, [handleFileDelete, _tableRefetch, refreshTreeFiles, refreshFolders]);
|
||
|
||
const _handleDeleteTreeFiles = useCallback(async (fileIds: string[]) => {
|
||
await handleFileDeleteMultiple(fileIds);
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
}, [handleFileDeleteMultiple, _tableRefetch, refreshTreeFiles, refreshFolders]);
|
||
|
||
const _handleDeleteTreeFolders = useCallback(async (folderIds: string[]) => {
|
||
await api.post('/api/files/batch-delete', { folderIds, recursiveFolders: true });
|
||
await Promise.all([refreshFolders(), refreshTreeFiles(), _tableRefetch()]);
|
||
}, [refreshFolders, refreshTreeFiles, _tableRefetch]);
|
||
|
||
// ── Table event handlers ──────────────────────────────────────────────
|
||
const handleEditClick = async (file: UserFile) => {
|
||
const fullFile = await fetchFileById(file.id);
|
||
if (fullFile) setEditingFile(fullFile as UserFile);
|
||
};
|
||
|
||
const handleEditSubmit = async (data: Partial<UserFile>) => {
|
||
if (!editingFile) return;
|
||
const changes: Record<string, any> = {};
|
||
const editableFields = ['fileName', 'scope', 'tags', 'description', 'folderId', 'neutralize'] as const;
|
||
for (const field of editableFields) {
|
||
if (data[field] !== undefined && data[field] !== editingFile[field]) {
|
||
changes[field] = data[field];
|
||
}
|
||
}
|
||
if (Object.keys(changes).length === 0 && data.fileName) {
|
||
changes.fileName = data.fileName;
|
||
}
|
||
const result = await handleFileUpdate(editingFile.id, changes);
|
||
if (result.success) {
|
||
setEditingFile(null);
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles()]);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (file: UserFile) => {
|
||
const success = await handleFileDelete(file.id);
|
||
if (success) await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
};
|
||
|
||
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
|
||
const ids = filesToDelete.map(f => f.id);
|
||
const success = await handleFileDeleteMultiple(ids);
|
||
if (success) await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
};
|
||
|
||
const handleDownload = async (file: UserFile) => {
|
||
await handleFileDownload(file.id, file.fileName);
|
||
};
|
||
|
||
const handleUploadClick = () => { fileInputRef.current?.click(); };
|
||
|
||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const picked = e.target.files;
|
||
if (picked && picked.length > 0) {
|
||
let successCount = 0;
|
||
let errorCount = 0;
|
||
for (const file of Array.from(picked)) {
|
||
const result = await handleFileUpload(file, undefined, undefined, selectedFolderId);
|
||
if (result?.success) successCount++; else errorCount++;
|
||
}
|
||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||
await Promise.all([_tableRefetch(), refreshTreeFiles(), refreshFolders()]);
|
||
if (successCount > 0) {
|
||
showSuccess(
|
||
t('Upload erfolgreich'),
|
||
errorCount > 0
|
||
? t('{successCount} Datei(en) hochgeladen, {errorCount} fehlgeschlagen', { successCount, errorCount })
|
||
: t('{successCount} Datei(en) hochgeladen', { successCount }),
|
||
);
|
||
} else if (errorCount > 0) {
|
||
showError(t('Upload fehlgeschlagen'), t('{errorCount} Datei(en) konnten nicht hochgeladen werden', { errorCount }));
|
||
}
|
||
}
|
||
};
|
||
|
||
const _handleNewFolder = useCallback(async () => {
|
||
const name = await promptInput(t('Neuer Ordnername:'), { title: t('Neuer Ordner'), placeholder: t('Ordnername') });
|
||
if (name?.trim()) {
|
||
await handleCreateFolder(name.trim(), selectedFolderId);
|
||
}
|
||
}, [handleCreateFolder, selectedFolderId, promptInput, t]);
|
||
|
||
const _onRowDragStart = useCallback((e: React.DragEvent<HTMLTableRowElement>, row: UserFile) => {
|
||
const isInSelection = selectedFiles.some(f => f.id === row.id);
|
||
if (isInSelection && selectedFiles.length > 1) {
|
||
e.dataTransfer.setData('application/file-ids', JSON.stringify(selectedFiles.map(f => f.id)));
|
||
} else {
|
||
e.dataTransfer.setData('application/file-id', row.id);
|
||
}
|
||
e.dataTransfer.effectAllowed = 'move';
|
||
}, [selectedFiles]);
|
||
|
||
const formAttributes = useMemo(() => {
|
||
const excludedFields = ['id', 'mandateId', 'fileHash', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'creationDate', 'source'];
|
||
return (attributes || []).filter(attr => !excludedFields.includes(attr.name));
|
||
}, [attributes]);
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}</p>
|
||
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||
<FaSync /> {t('Erneut versuchen')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
multiple
|
||
style={{ display: 'none' }}
|
||
onChange={handleFileSelect}
|
||
/>
|
||
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>{t('Dateien')}</h1>
|
||
<p className={styles.pageSubtitle}>{t('Dateiverwaltung')}</p>
|
||
</div>
|
||
<div className={styles.headerActions}>
|
||
<button className={styles.secondaryButton} onClick={() => _refreshAll()} disabled={tableLoading}>
|
||
<FaSync className={tableLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
ref={containerRef as React.RefObject<HTMLDivElement>}
|
||
style={{ display: 'flex', flex: 1, overflow: 'hidden', minHeight: 0, position: 'relative' }}
|
||
>
|
||
{/* Left panel: FolderTree */}
|
||
<div style={{
|
||
width: `${leftWidth}%`,
|
||
minWidth: 0,
|
||
overflow: 'auto',
|
||
borderRight: '1px solid var(--color-border, #e0e0e0)',
|
||
padding: '8px 4px',
|
||
}}>
|
||
<FolderTree
|
||
folders={folderNodes}
|
||
files={treeFileNodes}
|
||
showFiles={true}
|
||
selectedFolderId={selectedFolderId}
|
||
onSelect={setSelectedFolderId}
|
||
onFileSelect={_handleTreeFileSelect}
|
||
selectedItemIds={treeSelectedIds}
|
||
onSelectionChange={setTreeSelectedIds}
|
||
expandedIds={expandedFolderIds}
|
||
onToggleExpand={toggleFolderExpanded}
|
||
onRefresh={_refreshAll}
|
||
onCreateFolder={handleCreateFolder}
|
||
onRenameFolder={handleRenameFolder}
|
||
onDeleteFolder={async (folderId) => {
|
||
await handleDeleteFolder(folderId);
|
||
if (selectedFolderId === folderId) setSelectedFolderId(null);
|
||
await _tableRefetch();
|
||
}}
|
||
onMoveFolder={handleMoveFolder}
|
||
onMoveFolders={handleMoveFolders}
|
||
onMoveFile={_handleMoveFile}
|
||
onMoveFiles={_handleMoveFiles}
|
||
onRenameFile={_handleRenameFile}
|
||
onDeleteFile={_handleDeleteTreeFile}
|
||
onDeleteFiles={_handleDeleteTreeFiles}
|
||
onDeleteFolders={_handleDeleteTreeFolders}
|
||
onDownloadFolder={handleDownloadFolder}
|
||
onScopeChange={_handleScopeChange}
|
||
onNeutralizeToggle={_handleNeutralizeToggle}
|
||
/>
|
||
</div>
|
||
|
||
{/* Resizable divider */}
|
||
<div
|
||
onMouseDown={handleMouseDown}
|
||
style={{
|
||
width: 6,
|
||
cursor: 'col-resize',
|
||
background: isDragging ? 'var(--primary-dark-bg, rgba(242, 88, 67, 0.2))' : 'transparent',
|
||
transition: isDragging ? 'none' : 'background 0.15s',
|
||
flexShrink: 0,
|
||
zIndex: 10,
|
||
}}
|
||
onMouseEnter={(e) => { (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 */}
|
||
<div style={{ flex: 1, minWidth: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||
<div style={{
|
||
display: 'flex', gap: 8, padding: '8px 12px',
|
||
borderBottom: '1px solid var(--color-border, #e0e0e0)',
|
||
flexShrink: 0, alignItems: 'center', flexWrap: 'wrap',
|
||
}}>
|
||
<button className={styles.secondaryButton} onClick={_handleNewFolder}>
|
||
<FaFolderPlus /> {t('Neuer Ordner')}
|
||
</button>
|
||
{canCreate && (
|
||
<button className={styles.primaryButton} onClick={handleUploadClick} disabled={uploadingFile}>
|
||
<FaUpload /> {uploadingFile ? t('Wird hochgeladen…') : t('Datei hochladen')}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||
<FormGeneratorTable
|
||
data={tableFiles || []}
|
||
columns={columns}
|
||
apiEndpoint="/api/files/list"
|
||
loading={tableLoading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={true}
|
||
onRowSelect={(rows) => setSelectedFiles(rows as UserFile[])}
|
||
rowDraggable={true}
|
||
onRowDragStart={_onRowDragStart}
|
||
getRowDataAttributes={(row: UserFile) =>
|
||
({ highlighted: row.id === highlightedFileId ? 'true' : 'false' })
|
||
}
|
||
actionButtons={[
|
||
{
|
||
type: 'view' as const,
|
||
onAction: () => { /* ContentPreview fetches the file itself once the popup opens */ },
|
||
title: t('Vorschau'),
|
||
idField: 'id',
|
||
nameField: 'fileName',
|
||
typeField: 'mimeType',
|
||
loadingStateName: 'previewingFiles',
|
||
},
|
||
...(canUpdate ? [{
|
||
type: 'edit' as const,
|
||
onAction: handleEditClick,
|
||
title: t('Bearbeiten'),
|
||
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann bearbeiten') } : false,
|
||
}] : []),
|
||
...(canDelete ? [{
|
||
type: 'delete' as const,
|
||
title: t('Löschen'),
|
||
loading: (row: UserFile) => deletingFiles.has(row.id),
|
||
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentümer kann löschen') } : false,
|
||
}] : []),
|
||
]}
|
||
customActions={[
|
||
{
|
||
id: 'download',
|
||
icon: <FaDownload />,
|
||
onClick: handleDownload,
|
||
title: t('Herunterladen'),
|
||
loading: (row: UserFile) => downloadingFiles.has(row.id),
|
||
},
|
||
]}
|
||
onDelete={handleDelete}
|
||
onDeleteMultiple={handleDeleteMultiple}
|
||
hookData={{
|
||
refetch: _tableRefetch,
|
||
pagination,
|
||
permissions,
|
||
handleDelete: handleFileDelete,
|
||
handleInlineUpdate,
|
||
updateOptimistically: updateFileOptimistically,
|
||
previewingFiles,
|
||
}}
|
||
emptyMessage={emptyTableMessage}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{editingFile && (
|
||
<div className={styles.modalOverlay}>
|
||
<div className={styles.modal}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>{t('Datei bearbeiten')}</h2>
|
||
<button className={styles.modalClose} onClick={() => setEditingFile(null)}>✕</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{formAttributes.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>{t('Formular laden')}</span>
|
||
</div>
|
||
) : (
|
||
<FormGeneratorForm
|
||
attributes={formAttributes}
|
||
data={editingFile}
|
||
mode="edit"
|
||
onSubmit={handleEditSubmit}
|
||
onCancel={() => setEditingFile(null)}
|
||
submitButtonText={t('Speichern')}
|
||
cancelButtonText={t('Abbrechen')}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<PromptDialog />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default FilesPage;
|