+
{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 ? (
From 0055b8ce44bba10baba012762160f025362f1663 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 3 May 2026 22:19:20 +0200
Subject: [PATCH 02/11] fixed ux for expand object scrolling
---
.../FormGeneratorTree/FormGeneratorTree.tsx | 20 +++++++++++++-
.../TreeNavigation/TreeNavigation.tsx | 26 ++++++++++++++-----
2 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
index aa1bee9..e480963 100644
--- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
+++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx
@@ -452,6 +452,7 @@ export function FormGeneratorTree({
const _handleToggleExpand = useCallback(
async (id: string) => {
+ const wasExpanded = expandedIds.has(id);
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
@@ -463,7 +464,7 @@ export function FormGeneratorTree({
});
const node = nodes.find((n) => n.id === id);
- if (node && !expandedIds.has(id)) {
+ if (node && !wasExpanded) {
const childMap = _buildChildMap(nodes);
const existingChildren = childMap.get(id);
if (!existingChildren || existingChildren.length === 0) {
@@ -472,11 +473,28 @@ export function FormGeneratorTree({
setNodes((prev) => [...prev, ...childNodes]);
}
}
+ setTimeout(() => {
+ _scrollExpandedNodeToCenter(id);
+ }, 50);
}
},
[nodes, expandedIds, provider, ownership],
);
+ const _scrollExpandedNodeToCenter = useCallback((nodeId: string) => {
+ const container = treeContentRef.current;
+ if (!container) return;
+ const el = container.querySelector(`[data-node-id="${nodeId}"]`) as HTMLElement | null;
+ if (!el) return;
+ const containerRect = container.getBoundingClientRect();
+ const elRect = el.getBoundingClientRect();
+ const midpoint = containerRect.top + containerRect.height / 2;
+ if (elRect.top > midpoint) {
+ const scrollTarget = container.scrollTop + (elRect.top - midpoint);
+ container.scrollTo({ top: scrollTarget, behavior: 'smooth' });
+ }
+ }, []);
+
const _handleToggleSelect = useCallback(
(id: string, e: React.MouseEvent) => {
const newSelection = new Set(selectedIds);
diff --git a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
index 4b9beee..2f81d1b 100644
--- a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
+++ b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx
@@ -10,7 +10,7 @@
* - NavLink integration with React Router
*/
-import React, { useState, useEffect, ReactNode } from 'react';
+import React, { useState, useEffect, useRef, useCallback, ReactNode } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import styles from './TreeNavigation.module.css';
@@ -151,6 +151,7 @@ const TreeNode: React.FC = ({
const [isExpanded, setIsExpanded] = useState(
node.defaultExpanded ?? shouldAutoExpand ?? false
);
+ const containerRef = useRef(null);
// Auto-expand when path becomes active
useEffect(() => {
@@ -159,6 +160,16 @@ const TreeNode: React.FC = ({
}
}, [currentPath, autoExpandActive, node]);
+ const _scrollAfterExpand = useCallback(() => {
+ const el = containerRef.current;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ const viewportMid = window.innerHeight / 2;
+ if (rect.top > viewportMid) {
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ }
+ }, []);
+
// Check if this node is active (exact match or ancestor of active path)
const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false;
// Differentiate: leaf active (strong highlight) vs group active (subtle text only)
@@ -179,12 +190,13 @@ const TreeNode: React.FC = ({
}
if (isExpandable && !node.path) {
- // If only expandable (no path), toggle expand
- setIsExpanded(!isExpanded);
+ const willExpand = !isExpanded;
+ setIsExpanded(willExpand);
+ if (willExpand) setTimeout(_scrollAfterExpand, 50);
} else if (isExpandable && node.path) {
- // If both expandable and has path, expand on click but allow navigation
if (!isExpanded) {
setIsExpanded(true);
+ setTimeout(_scrollAfterExpand, 50);
}
}
@@ -197,7 +209,9 @@ const TreeNode: React.FC = ({
const handleToggleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
- setIsExpanded(!isExpanded);
+ const willExpand = !isExpanded;
+ setIsExpanded(willExpand);
+ if (willExpand) setTimeout(_scrollAfterExpand, 50);
};
// Render the node content (actions are rendered outside to avoid button-in-button nesting)
@@ -255,7 +269,7 @@ const TreeNode: React.FC = ({
const canRenderChildren = maxDepth === 0 || level < maxDepth;
return (
-
+
{nodeElement}
{node.actions && (
e.stopPropagation()}>
From 1219d616d427dc2f94991fa7adc7ed3db369f451 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 3 May 2026 22:24:47 +0200
Subject: [PATCH 03/11] fix import
---
src/components/UnifiedDataBar/FilesTab.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx
index 388ce93..43a20e4 100644
--- a/src/components/UnifiedDataBar/FilesTab.tsx
+++ b/src/components/UnifiedDataBar/FilesTab.tsx
@@ -1,5 +1,4 @@
import React, { useCallback, useRef, useMemo, useState } from 'react';
-import { FaFileImport } from 'react-icons/fa';
import type { UdbContext } from './UnifiedDataBar';
import api from '../../api';
import { useApiRequest } from '../../hooks/useApi';
From 930a34662da5d70f14709cba6282437fe4edf255 Mon Sep 17 00:00:00 2001
From: Ida
Date: Mon, 4 May 2026 17:29:11 +0200
Subject: [PATCH 04/11] fix: falsche gruppierung entfernt, gruppierung richtig
implementiert
---
src/api/billingApi.ts | 49 +-
src/api/connectionApi.ts | 33 +-
src/api/fileApi.ts | 37 +-
src/api/mandateApi.ts | 6 +-
src/api/promptApi.ts | 12 +-
src/api/tableViewApi.ts | 59 +
src/api/userApi.ts | 6 +-
.../FormGeneratorControls.tsx | 30 +-
.../FormGeneratorTable.module.css | 159 ++
.../FormGeneratorTable/FormGeneratorTable.tsx | 1589 +++++++++--------
.../GroupingManager/GroupRow.tsx | 7 +-
.../TableViewsBar/TableViewsBar.module.css | 286 +++
.../TableViewsBar/TableViewsBar.tsx | 337 ++++
.../FormGenerator/TableViewsBar/index.ts | 1 +
src/hooks/useBilling.ts | 44 +-
src/hooks/useConnections.ts | 85 +-
src/hooks/useFiles.ts | 95 +-
src/hooks/usePrompts.ts | 83 +-
src/pages/basedata/ConnectionsPage.tsx | 13 +-
src/pages/basedata/FilesPage.tsx | 45 +-
src/pages/basedata/PromptsPage.tsx | 13 +-
src/pages/billing/Billing.module.css | 25 +-
src/pages/billing/BillingDataView.tsx | 125 +-
src/pages/billing/BillingTransactions.tsx | 277 +--
src/pages/views/workspace/WorkspacePage.tsx | 36 +-
25 files changed, 2398 insertions(+), 1054 deletions(-)
create mode 100644 src/api/tableViewApi.ts
create mode 100644 src/components/FormGenerator/TableViewsBar/TableViewsBar.module.css
create mode 100644 src/components/FormGenerator/TableViewsBar/TableViewsBar.tsx
create mode 100644 src/components/FormGenerator/TableViewsBar/index.ts
diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts
index cc05e44..8094658 100644
--- a/src/api/billingApi.ts
+++ b/src/api/billingApi.ts
@@ -36,6 +36,29 @@ export interface BillingTransaction {
userName?: string;
}
+/** Pagination request for GET /api/billing/transactions with `pagination` JSON (table + grouping). */
+export interface BillingTransactionsPaginationParams {
+ page?: number;
+ pageSize?: number;
+ sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
+ filters?: Record;
+ search?: string;
+ viewKey?: string;
+ groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
+}
+
+export interface BillingTransactionsPaginatedResponse {
+ items: BillingTransaction[];
+ pagination?: {
+ currentPage: number;
+ pageSize: number;
+ totalItems: number;
+ totalPages: number;
+ };
+ groupLayout?: import('./connectionApi').GroupLayout;
+ appliedView?: { viewKey?: string; displayName?: string };
+}
+
export interface BillingSettings {
id: string;
mandateId: string;
@@ -135,7 +158,31 @@ export async function fetchBalanceForMandate(
}
/**
- * Fetch transaction history
+ * Fetch transaction history (table UI: pagination, filters, sort, saved views, grouping).
+ * Endpoint: GET /api/billing/transactions?pagination=...
+ */
+export async function fetchTransactionsPaginated(
+ request: ApiRequestFunction,
+ params?: BillingTransactionsPaginationParams
+): Promise {
+ const paginationObj: Record = {};
+ if (params?.page !== undefined) paginationObj.page = params.page;
+ if (params?.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
+ if (params?.sort?.length) paginationObj.sort = params.sort;
+ if (params?.filters && Object.keys(params.filters).length > 0) paginationObj.filters = params.filters;
+ if (params?.search) paginationObj.search = params.search;
+ if (params?.viewKey) paginationObj.viewKey = params.viewKey;
+ if (params?.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
+
+ return await request({
+ url: '/api/billing/transactions',
+ method: 'get',
+ params: { pagination: JSON.stringify(paginationObj) },
+ });
+}
+
+/**
+ * Fetch transaction history (legacy array window)
* Endpoint: GET /api/billing/transactions
*/
export async function fetchTransactions(
diff --git a/src/api/connectionApi.ts b/src/api/connectionApi.ts
index 41a79e4..8c47c6d 100644
--- a/src/api/connectionApi.ts
+++ b/src/api/connectionApi.ts
@@ -55,19 +55,22 @@ export interface PaginationParams {
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record;
search?: string;
- /** Scope request to items of this group (resolved server-side to itemIds IN-filter). */
- groupId?: string;
- /** If set, persist this group tree on the backend before fetching (optimistic save). */
- saveGroupTree?: TableGroupNode[];
+ /** Key of a saved view to apply (server loads groupByLevels, filters, sort from DB). */
+ viewKey?: string;
+ /** Explicit grouping levels; when sent (incl. []), overrides the view for this request. */
+ groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
}
-export interface TableGroupNode {
- id: string;
- name: string;
- itemIds: string[];
- subGroups: TableGroupNode[];
- order: number;
- isExpanded: boolean;
+export interface GroupBand {
+ path: string[];
+ label: string;
+ startRowIndex: number;
+ rowCount: number;
+}
+
+export interface GroupLayout {
+ levels: string[];
+ bands: GroupBand[];
}
export interface PaginatedResponse {
@@ -78,8 +81,8 @@ export interface PaginatedResponse {
totalItems: number;
totalPages: number;
};
- /** Current group tree for this (user, contextKey) pair — undefined if no grouping configured. */
- groupTree?: TableGroupNode[];
+ groupLayout?: GroupLayout;
+ appliedView?: { viewKey?: string; displayName?: string };
}
export interface CreateConnectionData {
@@ -138,8 +141,8 @@ export async function fetchConnections(
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
- if (params.groupId) paginationObj.groupId = params.groupId;
- if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree;
+ if (params.viewKey) paginationObj.viewKey = params.viewKey;
+ if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts
index 7e2c67a..75151c3 100644
--- a/src/api/fileApi.ts
+++ b/src/api/fileApi.ts
@@ -34,8 +34,8 @@ export interface PaginationParams {
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record;
search?: string;
- groupId?: string;
- saveGroupTree?: any[];
+ viewKey?: string;
+ groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
}
export interface PaginatedResponse {
@@ -46,6 +46,8 @@ export interface PaginatedResponse {
totalItems: number;
totalPages: number;
};
+ groupLayout?: import('./connectionApi').GroupLayout;
+ appliedView?: { viewKey?: string; displayName?: string };
}
// Type for the request function passed to API functions
@@ -105,9 +107,9 @@ export async function fetchFiles(
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
- if (params.groupId) paginationObj.groupId = params.groupId;
- if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree;
-
+ if (params.viewKey) paginationObj.viewKey = params.viewKey;
+ if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
+
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
@@ -249,28 +251,13 @@ export async function deleteGroup(
});
}
-/** Collect all file IDs belonging to a group recursively (client-side, from known groupTree) */
+/** @deprecated Group tree removed — use view-based grouping (viewKey). Returns empty array. */
export function collectGroupItemIds(
- groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>,
- groupId: string
+ _groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>,
+ _groupId: string
): string[] {
- const collect = (nodes: Array<{ id: string; itemIds: string[]; subGroups: any[] }>): string[] | null => {
- for (const node of nodes) {
- if (node.id === groupId) {
- const ids: string[] = [...node.itemIds];
- const sub = (n: { id: string; itemIds: string[]; subGroups: any[] }) => {
- ids.push(...n.itemIds);
- n.subGroups.forEach(sub);
- };
- node.subGroups.forEach(sub);
- return ids;
- }
- const found = collect(node.subGroups);
- if (found) return found;
- }
- return null;
- };
- return collect(groupTree) ?? [];
+ const collect = (): string[] | null => null;
+ return collect() ?? [];
}
// Note: The following operations require special handling (FormData, blob responses)
diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts
index 7946395..24aee62 100644
--- a/src/api/mandateApi.ts
+++ b/src/api/mandateApi.ts
@@ -46,8 +46,7 @@ export interface PaginationParams {
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record;
search?: string;
- groupId?: string;
- saveGroupTree?: any[];
+ viewKey?: string;
}
export interface PaginatedResponse {
@@ -86,8 +85,7 @@ export async function fetchMandates(
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
- if (params.groupId) paginationObj.groupId = params.groupId;
- if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree;
+ if (params.viewKey) paginationObj.viewKey = params.viewKey;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts
index e735ae0..164a633 100644
--- a/src/api/promptApi.ts
+++ b/src/api/promptApi.ts
@@ -49,8 +49,8 @@ export interface PaginationParams {
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record;
search?: string;
- groupId?: string;
- saveGroupTree?: any[];
+ viewKey?: string;
+ groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
}
export interface PaginatedResponse {
@@ -61,6 +61,8 @@ export interface PaginatedResponse {
totalItems: number;
totalPages: number;
};
+ groupLayout?: import('./connectionApi').GroupLayout;
+ appliedView?: { viewKey?: string; displayName?: string };
}
export interface CreatePromptData {
@@ -112,9 +114,9 @@ export async function fetchPrompts(
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
- if (params.groupId) paginationObj.groupId = params.groupId;
- if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree;
-
+ if (params.viewKey) paginationObj.viewKey = params.viewKey;
+ if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
+
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
diff --git a/src/api/tableViewApi.ts b/src/api/tableViewApi.ts
new file mode 100644
index 0000000..08039e8
--- /dev/null
+++ b/src/api/tableViewApi.ts
@@ -0,0 +1,59 @@
+import api from '../api';
+
+export interface TableListViewRow {
+ id: string;
+ userId?: string;
+ mandateId?: string | null;
+ contextKey: string;
+ viewKey: string;
+ displayName: string;
+ config: TableViewConfig;
+ updatedAt?: number;
+}
+
+export interface TableViewConfig {
+ schemaVersion?: number;
+ filters?: Record;
+ sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
+ groupByLevels?: Array<{ field: string; nullLabel?: string }>;
+ /** Section mode (`tableGroupLayoutMode="sections"`): stable keys (`sk`) of collapsed sections. */
+ collapsedSectionKeys?: string[];
+ /** Inline `groupLayout` bands: keys are `band.path.join('///')`. */
+ collapsedGroupKeys?: string[];
+}
+
+export async function listTableViews(contextKey: string): Promise {
+ const { data } = await api.get('/api/table-views', {
+ params: { contextKey },
+ });
+ return Array.isArray(data) ? data : [];
+}
+
+export async function getTableView(contextKey: string, viewKey: string): Promise {
+ const { data } = await api.get(`/api/table-views/${encodeURIComponent(viewKey)}`, {
+ params: { contextKey },
+ });
+ return data;
+}
+
+export async function createTableView(payload: {
+ contextKey: string;
+ viewKey: string;
+ displayName: string;
+ config: TableViewConfig;
+}): Promise {
+ const { data } = await api.post('/api/table-views', payload);
+ return data;
+}
+
+export async function updateTableView(
+ viewId: string,
+ updates: { displayName?: string; viewKey?: string; config?: TableViewConfig },
+): Promise {
+ const { data } = await api.put(`/api/table-views/${encodeURIComponent(viewId)}`, updates);
+ return data;
+}
+
+export async function deleteTableView(viewId: string): Promise {
+ await api.delete(`/api/table-views/${encodeURIComponent(viewId)}`);
+}
diff --git a/src/api/userApi.ts b/src/api/userApi.ts
index 98dd7a2..3615375 100644
--- a/src/api/userApi.ts
+++ b/src/api/userApi.ts
@@ -48,8 +48,7 @@ export interface PaginationParams {
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record;
search?: string;
- groupId?: string;
- saveGroupTree?: any[];
+ viewKey?: string;
}
export interface PaginatedResponse {
@@ -154,8 +153,7 @@ export async function fetchUsers(
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
- if (params.groupId) paginationObj.groupId = params.groupId;
- if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree;
+ if (params.viewKey) paginationObj.viewKey = params.viewKey;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
index 8b43b01..e57fce2 100644
--- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
+++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
@@ -4,7 +4,7 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './FormGeneratorControls.module.css';
import { Button } from '../../UiComponents/Button';
import { IoIosRefresh } from "react-icons/io";
-import { FaTrash, FaDownload, FaLayerGroup } from "react-icons/fa";
+import { FaTrash, FaDownload } from "react-icons/fa";
import type { AttributeType } from '../../../utils/attributeTypeMapper';
// Generic field/column config interface
@@ -77,10 +77,6 @@ export interface FormGeneratorControlsProps {
onSelectAllFiltered?: () => void;
selectAllFilteredActive?: boolean;
selectAllFilteredLoading?: boolean;
- // Grouping
- groupingEnabled?: boolean;
- onCreateGroup?: () => void;
- activeGroupId?: string | null;
}
export function FormGeneratorControls({
@@ -114,9 +110,6 @@ export function FormGeneratorControls({
onSelectAllFiltered,
selectAllFilteredActive = false,
selectAllFilteredLoading = false,
- groupingEnabled = false,
- onCreateGroup,
- activeGroupId,
}: FormGeneratorControlsProps) {
const { t } = useLanguage();
@@ -186,9 +179,15 @@ export function FormGeneratorControls({
)}
- {/* Search Controls with Pagination - Hide when items are selected */}
- {searchable && selectedCount === 0 && (
+ {/* Toolbar: optional search + filters badge + CSV + pagination (search is optional) */}
+ {selectedCount === 0 &&
+ (searchable ||
+ (pagination && supportsBackendPagination) ||
+ !!onCsvExport ||
+ !!onRefresh ||
+ activeFiltersCount > 0) && (
+ {searchable && (
+ )}
{activeFiltersCount > 0 && (
{activeFiltersCount} {t('Filter')}
@@ -219,16 +219,6 @@ export function FormGeneratorControls({
{csvExporting ? t('Exportiere...') : 'CSV'}
)}
- {groupingEnabled && onCreateGroup && (
-
- )}
{onRefresh && (