+
{t('Output')}
+ {outputData !== undefined && outputData !== null && (
+
+ )}
<_DataBlock data={outputData} />
<_FileLinkList files={outputFiles} />
@@ -1349,12 +1426,12 @@ export const AutomationsDashboardPage: React.FC = () => {
},
{
id: 'dashboard',
- label: t('Dashboard'),
+ label: t('Workflow-Durchläufe'),
content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
},
{
id: 'workspace',
- label: t('Workspace'),
+ label: t('Durchlauf-Details'),
content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
},
], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
diff --git a/src/pages/basedata/FilesPage.tsx b/src/pages/basedata/FilesPage.tsx
index 36c7802..d1d019e 100644
--- a/src/pages/basedata/FilesPage.tsx
+++ b/src/pages/basedata/FilesPage.tsx
@@ -1,23 +1,25 @@
/**
* FilesPage
*
- * Full-width file management using FormGeneratorTable with persistent grouping.
- * Organisation exclusively via groupTree/groupId — no physical folder navigation.
+ * 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 } from 'react';
+import React, { useState, useMemo, useEffect, useRef, useCallback, type PointerEvent as RPointerEvent } from 'react';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
-import { FaSync, FaUpload, FaDownload, FaLock, FaLockOpen, FaFileArchive, FaTrash } from 'react-icons/fa';
+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 { useApiRequest } from '../../hooks/useApi';
-import { patchGroupScope, downloadGroupZip, deleteGroup } from '../../api/fileApi';
import styles from '../admin/Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { getUserDataCache } from '../../utils/userCache';
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
-import type { GroupBulkAction } from '../../components/FormGenerator/GroupingManager/GroupRow';
interface UserFile {
id: string;
@@ -28,11 +30,16 @@ interface UserFile {
[key: string]: any;
}
+type ViewMode = 'folder' | 'all';
+
export const FilesPage: React.FC = () => {
const { t } = useLanguage();
const fileInputRef = useRef
(null);
const { showSuccess, showError } = useToast();
- const { request } = useApiRequest();
+
+ const [viewMode, setViewMode] = useState('folder');
+ const provider = useMemo(() => createFolderFileProvider(), []);
+ const [treeKey, setTreeKey] = useState(0);
// ── Table data ────────────────────────────────────────────────────────
const {
@@ -43,7 +50,6 @@ export const FilesPage: React.FC = () => {
error,
refetch: tableRefetch,
pagination,
- groupTree,
fetchFileById,
updateFileOptimistically,
} = useUserFiles();
@@ -63,20 +69,68 @@ export const FilesPage: React.FC = () => {
const [editingFile, setEditingFile] = useState(null);
const [selectedFiles, setSelectedFiles] = useState([]);
+ const [selectedFolderId, setSelectedFolderId] = useState(null);
+ const [highlightedFileId, setHighlightedFileId] = useState(null);
- // ── Table refetch wrapper ──────────────────────────────────────────────
+ const [treeWidth, setTreeWidth] = useState(300);
+ const [treeVisible, setTreeVisible] = useState(true);
+ const [tableVisible, setTableVisible] = useState(true);
+ const draggingRef = useRef(false);
+ const splitContainerRef = useRef(null);
+
+ const _handleDividerPointerDown = useCallback((e: RPointerEvent) => {
+ e.preventDefault();
+ draggingRef.current = true;
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
+ }, []);
+
+ const _handleDividerPointerMove = useCallback((e: RPointerEvent) => {
+ 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) => {
- await tableRefetch(params);
- }, [tableRefetch]);
+ const nextParams = { ...(params || {}) };
+ const nextFilters = { ...(nextParams.filters || {}) };
+ if (viewMode === 'folder' && selectedFolderId) {
+ nextFilters.folderId = selectedFolderId;
+ } else {
+ delete nextFilters.folderId;
+ }
+ nextParams.filters = nextFilters;
+ await tableRefetch(nextParams);
+ }, [tableRefetch, selectedFolderId, viewMode]);
const _refreshAll = useCallback(async () => {
await _tableRefetch({ page: 1, pageSize: 25 });
+ setTreeKey(k => k + 1);
}, [_tableRefetch]);
- // Initial fetch
useEffect(() => {
_tableRefetch({ page: 1, pageSize: 25 });
- }, [_tableRefetch]);
+ }, [selectedFolderId, viewMode, _tableRefetch]);
+
+ // ── Tree interaction ──────────────────────────────────────────────────
+ const _handleTreeNodeClick = useCallback((node: TreeNode) => {
+ if (node.type === 'folder') {
+ setSelectedFolderId(node.id);
+ } else if (node.type === 'file') {
+ setSelectedFolderId(node.parentId);
+ 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(() => {
@@ -181,6 +235,7 @@ export const FilesPage: React.FC = () => {
}
if (fileInputRef.current) fileInputRef.current.value = '';
await _tableRefetch();
+ setTreeKey(k => k + 1);
if (successCount > 0) {
showSuccess(
t('Upload erfolgreich'),
@@ -194,55 +249,6 @@ export const FilesPage: React.FC = () => {
}
};
- const _groupBulkActionsProvider = useCallback((groupId: string, itemIds: string[]): GroupBulkAction[] => {
- return [
- {
- icon: ,
- title: t('Scope: personal'),
- onClick: async () => {
- try {
- await patchGroupScope(request, groupId, 'personal');
- showSuccess(t('Scope aktualisiert'), t('{n} Dateien auf personal gesetzt', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- },
- {
- icon: ,
- title: t('Scope: mandate'),
- onClick: async () => {
- try {
- await patchGroupScope(request, groupId, 'mandate');
- showSuccess(t('Scope aktualisiert'), t('{n} Dateien auf mandate gesetzt', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- },
- {
- icon: ,
- title: t('ZIP herunterladen'),
- onClick: async () => {
- try { await downloadGroupZip(groupId); }
- catch (e) { showError(t('Fehler'), String(e)); }
- },
- disabled: itemIds.length === 0,
- },
- {
- icon: ,
- title: t('Gruppe + Dateien löschen'),
- variant: 'danger' as const,
- onClick: async () => {
- try {
- await deleteGroup(request, groupId, true);
- showSuccess(t('Gelöscht'), t('Gruppe und {n} Dateien gelöscht', { n: String(itemIds.length) }));
- await _tableRefetch();
- } catch (e) { showError(t('Fehler'), String(e)); }
- },
- disabled: itemIds.length === 0,
- },
- ];
- }, [request, showSuccess, showError, _tableRefetch, t]);
-
const _onRowDragStart = useCallback((e: React.DragEvent, row: UserFile) => {
const isInSelection = selectedFiles.some(f => f.id === row.id);
if (isInSelection && selectedFiles.length > 1) {
@@ -262,7 +268,7 @@ export const FilesPage: React.FC = () => {
return (
-
⚠️
+
⚠️
{t('Fehler beim Laden der Dateien: {detail}', { detail: String(error) })}
+
+
+
+
-
-
- {canCreate && (
-
- )}
-
+
+ {/* Left panel: Tree */}
+ {treeVisible && (
+
+ _tableRefetch()}
+ />
+
+
+ )}
-
-
setSelectedFiles(rows as UserFile[])}
- rowDraggable={true}
- onRowDragStart={_onRowDragStart}
- 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 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: ,
- 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,
- groupTree,
+ {/* Resizable divider */}
+ {treeVisible && tableVisible && (
+
+ >
+
+
+ )}
+
+ {/* Right panel: Table with view-mode toggle */}
+ {tableVisible && (
+
+
+
+
+
+
+
+ {canCreate && (
+
+ )}
+
+
+
+ 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: ,
+ 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={t('Keine Dateien gefunden')}
+ />
+
+ )}
{editingFile && (
@@ -378,7 +477,7 @@ export const FilesPage: React.FC = () => {
{t('Datei bearbeiten')}
-
+
{formAttributes.length === 0 ? (