All checks were successful
Deploy Nyla Frontend INT / build-and-deploy (push) Successful in 10m27s
590 lines
22 KiB
TypeScript
590 lines
22 KiB
TypeScript
/**
|
|
* FilesPage
|
|
*
|
|
* Split-view file management: tree panel on the left (FormGeneratorTree),
|
|
* FormGeneratorTable on the right. Two modes:
|
|
* - "Ordner-Sicht": table filtered by selected folder in the tree
|
|
* - "Alle Dateien": table shows all files without folder filter
|
|
*/
|
|
|
|
import React, { useState, useMemo, useEffect, useRef, useCallback, type PointerEvent as RPointerEvent } from 'react';
|
|
import { useUserFiles, useFileOperations, type PaginationParams } from '../../hooks/useFiles';
|
|
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
|
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
|
import { FormGeneratorTree } from '../../components/FormGenerator/FormGeneratorTree';
|
|
import { createFolderFileProvider } from '../../components/FormGenerator/FormGeneratorTree/providers/FolderFileProvider';
|
|
import type { TreeNode } from '../../components/FormGenerator/FormGeneratorTree';
|
|
import { FaSync, FaUpload, FaDownload, FaTree, FaTable } from 'react-icons/fa';
|
|
import { useToast } from '../../contexts/ToastContext';
|
|
import styles from '../admin/Admin.module.css';
|
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
|
import { getUserDataCache } from '../../utils/userCache';
|
|
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
|
|
|
|
interface UserFile {
|
|
id: string;
|
|
fileName: string;
|
|
mimeType?: string;
|
|
fileSize?: number;
|
|
featureInstanceId?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
type ViewMode = 'folder' | 'all';
|
|
type FileOwnerScope = 'all' | 'me' | 'shared';
|
|
|
|
function normalizeFolderFilterId(folderId: string | null): string | null {
|
|
if (!folderId) return null;
|
|
if (folderId.startsWith('__filesRoot:')) return null;
|
|
return folderId;
|
|
}
|
|
|
|
function isSyntheticRootFolderId(folderId: string | null): boolean {
|
|
return Boolean(folderId && folderId.startsWith('__filesRoot:'));
|
|
}
|
|
|
|
export const FilesPage: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const { showSuccess, showError } = useToast();
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>('folder');
|
|
const provider = useMemo(() => createFolderFileProvider(), []);
|
|
const [treeKey, setTreeKey] = useState(0);
|
|
|
|
// ── Table data ────────────────────────────────────────────────────────
|
|
const {
|
|
data: tableFiles,
|
|
attributes,
|
|
permissions,
|
|
loading: tableLoading,
|
|
error,
|
|
refetch: tableRefetch,
|
|
pagination,
|
|
groupLayout,
|
|
appliedView,
|
|
fetchFileById,
|
|
updateFileOptimistically,
|
|
fetchGroupSectionSummaries: fetchGroupSectionSummariesFromHook,
|
|
refetchForSection: refetchForSectionFromHook,
|
|
} = useUserFiles();
|
|
|
|
const {
|
|
handleFileDownload,
|
|
handleFileDelete,
|
|
handleFileDeleteMultiple,
|
|
handleFileUpload,
|
|
handleFileUpdate,
|
|
handleInlineUpdate,
|
|
deletingFiles,
|
|
downloadingFiles,
|
|
uploadingFile,
|
|
previewingFiles,
|
|
} = useFileOperations();
|
|
|
|
const [editingFile, setEditingFile] = useState<UserFile | null>(null);
|
|
const [selectedFiles, setSelectedFiles] = useState<UserFile[]>([]);
|
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
|
const [selectedOwnership, setSelectedOwnership] = useState<'own' | 'shared' | null>('own');
|
|
const [highlightedFileId, setHighlightedFileId] = useState<string | null>(null);
|
|
|
|
const [treeWidth, setTreeWidth] = useState(300);
|
|
const [treeVisible, setTreeVisible] = useState(true);
|
|
const [tableVisible, setTableVisible] = useState(true);
|
|
const draggingRef = useRef(false);
|
|
const splitContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const _handleDividerPointerDown = useCallback((e: RPointerEvent<HTMLDivElement>) => {
|
|
e.preventDefault();
|
|
draggingRef.current = true;
|
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
}, []);
|
|
|
|
const _handleDividerPointerMove = useCallback((e: RPointerEvent<HTMLDivElement>) => {
|
|
if (!draggingRef.current || !splitContainerRef.current) return;
|
|
const rect = splitContainerRef.current.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
setTreeWidth(Math.max(180, Math.min(x, rect.width - 200)));
|
|
}, []);
|
|
|
|
const _handleDividerPointerUp = useCallback(() => {
|
|
draggingRef.current = false;
|
|
}, []);
|
|
|
|
// ── Table refetch wrapper (filters by selectedFolderId in folder mode) ──
|
|
const _tableRefetch = useCallback(async (params?: any) => {
|
|
const nextParams = { ...(params || {}) };
|
|
const nextFilters = { ...(nextParams.filters || {}) };
|
|
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
|
const rootSelected = isSyntheticRootFolderId(selectedFolderId);
|
|
const owner: FileOwnerScope =
|
|
selectedOwnership === 'own'
|
|
? 'me'
|
|
: selectedOwnership === 'shared'
|
|
? 'shared'
|
|
: 'all';
|
|
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
|
|
nextFilters.folderId = normalizedFolderId;
|
|
} else {
|
|
delete nextFilters.folderId;
|
|
}
|
|
nextParams.filters = nextFilters;
|
|
if (owner !== 'all') nextParams.owner = owner;
|
|
else delete nextParams.owner;
|
|
await tableRefetch(nextParams);
|
|
}, [tableRefetch, selectedFolderId, selectedOwnership, viewMode]);
|
|
|
|
const fetchGroupSectionSummaries = useCallback(
|
|
async (base: {
|
|
search?: string;
|
|
filters?: Record<string, unknown>;
|
|
sort?: Array<{ field: string; direction: string }>;
|
|
viewKey?: string | null;
|
|
groupField: string;
|
|
groupDirection?: 'asc' | 'desc';
|
|
}) => {
|
|
const filters = { ...(base.filters || {}) };
|
|
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
|
const rootSelected = isSyntheticRootFolderId(selectedFolderId);
|
|
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
|
|
filters.folderId = normalizedFolderId;
|
|
}
|
|
const owner: FileOwnerScope =
|
|
selectedOwnership === 'own'
|
|
? 'me'
|
|
: selectedOwnership === 'shared'
|
|
? 'shared'
|
|
: 'all';
|
|
return fetchGroupSectionSummariesFromHook({ ...base, filters, owner });
|
|
},
|
|
[fetchGroupSectionSummariesFromHook, viewMode, selectedFolderId, selectedOwnership],
|
|
);
|
|
|
|
const refetchForSection = useCallback(
|
|
async (
|
|
paginationParams: PaginationParams & { page: number; pageSize: number },
|
|
sectionFilter: Record<string, unknown>,
|
|
parentColumnFilters?: Record<string, unknown>,
|
|
) => {
|
|
const merged = { ...(parentColumnFilters || {}) };
|
|
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
|
const rootSelected = isSyntheticRootFolderId(selectedFolderId);
|
|
if (viewMode === 'folder' && selectedFolderId && !rootSelected) {
|
|
merged.folderId = normalizedFolderId;
|
|
}
|
|
const owner: FileOwnerScope =
|
|
selectedOwnership === 'own'
|
|
? 'me'
|
|
: selectedOwnership === 'shared'
|
|
? 'shared'
|
|
: 'all';
|
|
return refetchForSectionFromHook({ ...paginationParams, owner }, sectionFilter, merged);
|
|
},
|
|
[refetchForSectionFromHook, viewMode, selectedFolderId, selectedOwnership],
|
|
);
|
|
|
|
const _refreshAll = useCallback(async () => {
|
|
await _tableRefetch({ page: 1, pageSize: 25 });
|
|
setTreeKey(k => k + 1);
|
|
}, [_tableRefetch]);
|
|
|
|
useEffect(() => {
|
|
_tableRefetch({ page: 1, pageSize: 25 });
|
|
}, [selectedFolderId, selectedOwnership, viewMode, _tableRefetch]);
|
|
|
|
// ── Tree interaction ──────────────────────────────────────────────────
|
|
const _handleTreeNodeClick = useCallback((node: TreeNode) => {
|
|
setSelectedOwnership(node.ownership);
|
|
if (node.type === 'folder') {
|
|
setSelectedFolderId(node.id);
|
|
} else if (node.type === 'file') {
|
|
setSelectedFolderId(node.parentId ?? null);
|
|
setHighlightedFileId(node.id);
|
|
requestAnimationFrame(() => {
|
|
const row = document.querySelector('tr[data-highlighted="true"]');
|
|
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
});
|
|
setTimeout(() => setHighlightedFileId(null), 2500);
|
|
}
|
|
}, []);
|
|
|
|
// ── 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,
|
|
sortable: attr.sortable !== false,
|
|
filterable: attr.filterable !== false,
|
|
searchable: attr.searchable !== false,
|
|
width: attr.width || 150,
|
|
minWidth: attr.minWidth || 100,
|
|
maxWidth: attr.maxWidth || 400,
|
|
displayField: (attr as any).displayField,
|
|
frontendFormat: (attr as any).frontendFormat,
|
|
frontendFormatLabels: (attr as any).frontendFormatLabels,
|
|
}));
|
|
cols.push({
|
|
key: 'sysCreatedBy',
|
|
label: t('Erstellt von'),
|
|
sortable: true,
|
|
filterable: true,
|
|
searchable: true,
|
|
width: 150,
|
|
minWidth: 100,
|
|
maxWidth: 250,
|
|
displayField: 'sysCreatedByLabel',
|
|
} as any);
|
|
cols.push({
|
|
key: 'sysModifiedAt',
|
|
label: t('Geaendert am'),
|
|
type: 'timestamp',
|
|
sortable: true,
|
|
filterable: true,
|
|
searchable: false,
|
|
width: 170,
|
|
minWidth: 130,
|
|
maxWidth: 220,
|
|
} as any);
|
|
return resolveColumnTypes(cols, attributes || []);
|
|
}, [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]);
|
|
|
|
// ── 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', '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 _tableRefetch();
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (file: UserFile) => {
|
|
const success = await handleFileDelete(file.id);
|
|
if (success) await _tableRefetch();
|
|
};
|
|
|
|
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
|
|
const ids = filesToDelete.map(f => f.id);
|
|
const success = await handleFileDeleteMultiple(ids);
|
|
if (success) await _tableRefetch();
|
|
};
|
|
|
|
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);
|
|
if (result?.success) successCount++; else errorCount++;
|
|
}
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
await _tableRefetch();
|
|
setTreeKey(k => k + 1);
|
|
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 _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 = 'copyMove';
|
|
}, [selectedFiles]);
|
|
|
|
const formAttributes = useMemo(() => {
|
|
const excludedFields = ['id', 'mandateId', 'fileHash', 'folderId', '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}>
|
|
<div style={{ display: 'flex', gap: 4, marginRight: 8 }}>
|
|
<button
|
|
className={treeVisible ? styles.primaryButton : styles.secondaryButton}
|
|
onClick={() => { if (!treeVisible || tableVisible) setTreeVisible(v => !v); }}
|
|
style={{ fontSize: 12, padding: '4px 8px' }}
|
|
title={treeVisible ? t('Ordnerstruktur ausblenden') : t('Ordnerstruktur einblenden')}
|
|
>
|
|
<FaTree />
|
|
</button>
|
|
<button
|
|
className={tableVisible ? styles.primaryButton : styles.secondaryButton}
|
|
onClick={() => { if (!tableVisible || treeVisible) setTableVisible(v => !v); }}
|
|
style={{ fontSize: 12, padding: '4px 8px' }}
|
|
title={tableVisible ? t('Tabelle ausblenden') : t('Tabelle einblenden')}
|
|
>
|
|
<FaTable />
|
|
</button>
|
|
</div>
|
|
<button className={styles.secondaryButton} onClick={_refreshAll} disabled={tableLoading}>
|
|
<FaSync className={tableLoading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div ref={splitContainerRef} style={{ flex: 1, overflow: 'hidden', display: 'flex', minHeight: 0 }}>
|
|
{/* Left panel: Tree */}
|
|
{treeVisible && (
|
|
<div style={{
|
|
width: tableVisible ? treeWidth : '100%',
|
|
flexShrink: 0,
|
|
overflow: 'auto',
|
|
display: 'flex', flexDirection: 'column',
|
|
}}>
|
|
<FormGeneratorTree
|
|
key={`own-${treeKey}`}
|
|
provider={provider}
|
|
ownership="own"
|
|
title={t('Eigene')}
|
|
showFilter={true}
|
|
allowCreateFolder={canCreate}
|
|
onNodeClick={_handleTreeNodeClick}
|
|
onRefresh={() => _tableRefetch()}
|
|
/>
|
|
<FormGeneratorTree
|
|
key={`shared-${treeKey}`}
|
|
provider={provider}
|
|
ownership="shared"
|
|
title={t('Geteilt mit mir')}
|
|
collapsible={true}
|
|
defaultCollapsed={true}
|
|
emptyMessage={t('Keine geteilten Dateien')}
|
|
onNodeClick={_handleTreeNodeClick}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Resizable divider */}
|
|
{treeVisible && tableVisible && (
|
|
<div
|
|
onPointerDown={_handleDividerPointerDown}
|
|
onPointerMove={_handleDividerPointerMove}
|
|
onPointerUp={_handleDividerPointerUp}
|
|
style={{
|
|
width: 6, cursor: 'col-resize', flexShrink: 0,
|
|
background: 'var(--color-border, #e0e0e0)',
|
|
position: 'relative', zIndex: 2,
|
|
touchAction: 'none',
|
|
}}
|
|
>
|
|
<div style={{
|
|
position: 'absolute', top: '50%', left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: 4, height: 32, borderRadius: 2,
|
|
background: 'var(--color-text-muted, #94a3b8)',
|
|
opacity: 0.4,
|
|
}} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Right panel: Table with view-mode toggle */}
|
|
{tableVisible && (
|
|
<div style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
<div style={{
|
|
display: 'flex', gap: 8, padding: '8px 12px',
|
|
borderBottom: '1px solid var(--color-border, #e0e0e0)',
|
|
flexShrink: 0, alignItems: 'center', flexWrap: 'wrap',
|
|
}}>
|
|
<button
|
|
className={viewMode === 'folder' ? styles.primaryButton : styles.secondaryButton}
|
|
onClick={() => setViewMode('folder')}
|
|
style={{ fontSize: 12, padding: '4px 10px' }}
|
|
>
|
|
{t('Ordner-Sicht')}
|
|
</button>
|
|
<button
|
|
className={viewMode === 'all' ? styles.primaryButton : styles.secondaryButton}
|
|
onClick={() => setViewMode('all')}
|
|
style={{ fontSize: 12, padding: '4px 10px' }}
|
|
>
|
|
{t('Alle Dateien')}
|
|
</button>
|
|
|
|
<div style={{ flex: 1 }} />
|
|
|
|
{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"
|
|
tableContextKey="files/list"
|
|
tableGroupLayoutMode="inline"
|
|
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: () => {},
|
|
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 Eigentuemer kann bearbeiten') } : false,
|
|
}] : []),
|
|
...(canDelete ? [{
|
|
type: 'delete' as const,
|
|
title: t('Loeschen'),
|
|
loading: (row: UserFile) => deletingFiles.has(row.id),
|
|
disabled: (row: UserFile) => !_isOwned(row) ? { disabled: true, message: t('Nur Eigentuemer kann loeschen') } : 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,
|
|
groupLayout,
|
|
appliedView,
|
|
permissions,
|
|
handleDelete: handleFileDelete,
|
|
handleInlineUpdate,
|
|
updateOptimistically: updateFileOptimistically,
|
|
previewingFiles,
|
|
fetchGroupSectionSummaries,
|
|
refetchForSection,
|
|
}}
|
|
emptyMessage={t('Keine Dateien gefunden')}
|
|
/>
|
|
</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>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FilesPage;
|