diff --git a/src/api/connectionApi.ts b/src/api/connectionApi.ts index 67d6750..41a79e4 100644 --- a/src/api/connectionApi.ts +++ b/src/api/connectionApi.ts @@ -55,6 +55,19 @@ 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[]; +} + +export interface TableGroupNode { + id: string; + name: string; + itemIds: string[]; + subGroups: TableGroupNode[]; + order: number; + isExpanded: boolean; } export interface PaginatedResponse { @@ -65,6 +78,8 @@ export interface PaginatedResponse { totalItems: number; totalPages: number; }; + /** Current group tree for this (user, contextKey) pair — undefined if no grouping configured. */ + groupTree?: TableGroupNode[]; } export interface CreateConnectionData { @@ -123,6 +138,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 (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts index 18dc47e..44102f1 100644 --- a/src/api/fileApi.ts +++ b/src/api/fileApi.ts @@ -34,6 +34,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -103,6 +105,8 @@ 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 (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts index 38bf41c..7946395 100644 --- a/src/api/mandateApi.ts +++ b/src/api/mandateApi.ts @@ -46,6 +46,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -84,6 +86,8 @@ 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 (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index 00f1be7..e735ae0 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -49,6 +49,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -110,6 +112,8 @@ 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 (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/userApi.ts b/src/api/userApi.ts index d16bf38..98dd7a2 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -48,6 +48,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -152,6 +154,8 @@ 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 (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 0e5ff03..8b43b01 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 } from "react-icons/fa"; +import { FaTrash, FaDownload, FaLayerGroup } from "react-icons/fa"; import type { AttributeType } from '../../../utils/attributeTypeMapper'; // Generic field/column config interface @@ -77,6 +77,10 @@ export interface FormGeneratorControlsProps { onSelectAllFiltered?: () => void; selectAllFilteredActive?: boolean; selectAllFilteredLoading?: boolean; + // Grouping + groupingEnabled?: boolean; + onCreateGroup?: () => void; + activeGroupId?: string | null; } export function FormGeneratorControls({ @@ -110,6 +114,9 @@ export function FormGeneratorControls({ onSelectAllFiltered, selectAllFilteredActive = false, selectAllFilteredLoading = false, + groupingEnabled = false, + onCreateGroup, + activeGroupId, }: FormGeneratorControlsProps) { const { t } = useLanguage(); @@ -212,6 +219,16 @@ export function FormGeneratorControls({ {csvExporting ? t('Exportiere...') : 'CSV'} )} + {groupingEnabled && onCreateGroup && ( + + )} {onRefresh && ( + + {/* Folder icon */} + + {isExpanded ? : } + + + {/* Name / inline input */} + {isEditing ? ( + { + if (e.key === 'Enter') onEditCommit(e.currentTarget.value); + if (e.key === 'Escape') onEditCancel(); + }} + onBlur={(e) => onEditCommit(e.target.value)} + /> + ) : ( + { e.stopPropagation(); onToggle(); }}> + {node.name || {t('(Unbenannt)')}} + + )} + + {/* Item count badge */} + {!isEditing && ( + + {visibleCount < totalCount && totalCount > 0 + ? `${visibleCount} / ${totalCount}` + : String(totalCount)} + + )} + + {/* Drop hint */} + {(isDragOver || isDragOverFromGroup) && ( + + {isDragOverFromGroup ? t('Als Untergruppe ablegen') : t('Hierher ziehen')} + + )} + + {/* ── Bulk actions (delete all, custom batch) right after badge ── */} + {!isEditing && bulkActions.length > 0 && ( + <> + + + {bulkActions.map((action, i) => ( + + ))} + + + )} + + {/* ── Group management: rename / add-subgroup ── */} + {!isEditing && ( + + + + + )} + + + + + + + ); +} + +// --------------------------------------------------------------------------- +// BreadcrumbRow +// --------------------------------------------------------------------------- + +interface BreadcrumbRowProps { + groupName: string; + totalItems: number; + colSpan: number; + onBack: () => void; +} + +export function BreadcrumbRow({ groupName, totalItems, colSpan, onBack }: BreadcrumbRowProps) { + const { t } = useLanguage(); + return ( + + +
+ + + {groupName} + {totalItems > 0 && ( + + ({totalItems} {t('Einträge')}) + + )} +
+ + + ); +} + +// --------------------------------------------------------------------------- +// UngroupedRow — also a drop zone for removing items/groups from groups +// --------------------------------------------------------------------------- + +interface UngroupedRowProps { + count: number; + colSpan: number; + isDragOver?: boolean; + onDragOver?: (e: React.DragEvent) => void; + onDrop?: (e: React.DragEvent) => void; + onDragLeave?: () => void; +} + +export function UngroupedRow({ count, colSpan, isDragOver, onDragOver, onDrop, onDragLeave }: UngroupedRowProps) { + const { t } = useLanguage(); + return ( + e.preventDefault()} + > + + + {t('Nicht zugeordnet')} + {count} + {isDragOver && {t('Aus Gruppe entfernen')}} + + + ); +} diff --git a/src/hooks/useConnections.ts b/src/hooks/useConnections.ts index 670bca7..b3b43d7 100644 --- a/src/hooks/useConnections.ts +++ b/src/hooks/useConnections.ts @@ -22,6 +22,7 @@ import { // Re-export types for backward compatibility export type { Connection, AttributeDefinition, PaginationParams, CreateConnectionData, ConnectResponse }; +export type { TableGroupNode } from '../api/connectionApi'; // Hook for managing connections export function useConnections() { @@ -34,6 +35,7 @@ export function useConnections() { totalItems: number; totalPages: number; } | null>(null); + const [groupTree, setGroupTree] = useState([]); const [isConnecting, setIsConnecting] = useState(false); const [connectError, setConnectError] = useState(null); const { request, isLoading, error } = useApiRequest(); @@ -101,6 +103,9 @@ export function useConnections() { if (data.pagination) { setPagination(data.pagination); } + if (Array.isArray(data.groupTree)) { + setGroupTree(data.groupTree); + } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; @@ -826,7 +831,8 @@ export function useConnections() { // Additional methods for FormGenerator updateOptimistically, handleInlineUpdate, - fetchConnectionById + fetchConnectionById, + groupTree, }; } diff --git a/src/pages/basedata/ConnectionsPage.tsx b/src/pages/basedata/ConnectionsPage.tsx index a1ad7ba..39673ca 100644 --- a/src/pages/basedata/ConnectionsPage.tsx +++ b/src/pages/basedata/ConnectionsPage.tsx @@ -44,6 +44,7 @@ export const ConnectionsPage: React.FC = () => { refreshMicrosoftToken, refreshGoogleToken, isConnecting, + groupTree, } = useConnections(); const [editingConnection, setEditingConnection] = useState(null); @@ -469,7 +470,9 @@ export const ConnectionsPage: React.FC = () => { handleDelete: deleteConnection, handleInlineUpdate, updateOptimistically, + groupTree, }} + groupingConfig={{ contextKey: 'connections', enabled: true }} emptyMessage={t('Keine Verbindungen gefunden')} />