# FormGenerator -- Referenz ## Uebersicht Der FormGenerator besteht aus mehreren Komponenten: | Komponente | Datei | Zweck | |------------|-------|-------| | `FormGeneratorTable` | `components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx` | Generische Datentabelle mit Pagination, Sortierung, Filterung, Suche, Selektion, Bulk-Actions | | `FormGeneratorControls` | `components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx` | Toolbar: Suche, Pagination, Batch-Action-Buttons, "Select All Filtered"-Banner | | `FormGeneratorForm` | `components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` | Dynamische Formulare aus Backend-Attribut-Definitionen | | `FormGeneratorList` | `components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx` | Listen-Darstellung | Diese Referenz fokussiert auf **FormGeneratorTable** und das zugehoerige Backend-API-Pattern. --- ## Page Layout Chain (Pflicht) `FormGeneratorTable` rendert intern mit `flex:1; min-height:0; overflow:hidden`. Damit die Tabelle die volle Seitenbreite/-hoehe ausnuetzt und nicht abgeschnitten wird, **muss** die Eltern-Hierarchie eine **bounded height chain** liefern. Andernfalls passiert eines davon: - Tabelle ist 0 px hoch (kollabiert) -- nur Toolbar sichtbar. - Tabelle waechst ueber den Viewport hinaus, horizontaler Scroll fehlt -- letzte Spalten werden abgeschnitten. - Page-Container hat `max-width` -> Tabelle ist auf z.B. 800 px begrenzt. ### Korrektes Pattern (verwendet PromptsPage, TrusteePositionsView, TrusteeDataTablesView) ```tsx import adminStyles from '../../admin/Admin.module.css'; export const MyPage: React.FC = () => (
...
); ``` Die drei CSS-Klassen aus `frontend_nyla/src/pages/admin/Admin.module.css`: | Klasse | Wirkung | |--------|---------| | `.adminPage` | `display:flex; flex-direction:column; width:100%; box-sizing:border-box; flex:0 0 auto` (Default fuer Standard-Seiten ohne Tabelle) | | `.adminPageFill` | `flex:1 1 auto; min-height:0; overflow:hidden` -- aktiviert die bounded height chain | | `.tableContainer` | `flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column` -- direktes Eltern-Element der Tabelle | `FeatureView`/`MainLayout` liefern bereits `height:100%` mit `min-height:0` -- Page-Komponenten brauchen also nur `adminPage adminPageFill` und einen `tableContainer` direkt um die `FormGeneratorTable`. ### Bei Tabs / Wrapper-Komponenten Wenn die Tabelle in einem Tab-Wrapper (z.B. `TrusteeDataTab`) gemountet wird, muss **jeder** Wrapper die Flex-Chain weiterreichen. Pattern: ```tsx const _rootStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, width: '100%', }; const _tableWrapStyle: React.CSSProperties = { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%', }; ``` Eine Toolbar oberhalb der Tabelle bekommt `flexShrink: 0`, der Tabellenwrapper `flex: 1; minHeight: 0`. ### Anti-Patterns (verursachen das "Tabelle abgeschnitten"-Bug) - Outer-Wrapper mit `max-width: 800px` (z.B. `.expenseImportSection` aus `TrusteeViews.module.css`) -- begrenzt die Breite. - Outer `
` ohne `flex:1; min-height:0` -- Tabelle kollabiert auf 0 px Hoehe. - `.adminPage` ohne `.adminPageFill` -- Default ist `flex:0 0 auto`, das innere `.tableContainer flex:1` hat keinen Platz. - Padding auf einem Zwischen-Wrapper -- bricht das Box-Sizing der Chain. --- ## FormGeneratorTable -- Props ### Datenanbindung | Prop | Typ | Beschreibung | |------|-----|-------------| | `data` | `T[]` | Anzuzeigende Datensaetze (aktuelle Seite) | | `columns` | `ColumnConfig[]` | Spaltendefinitionen (key, label, type, sortable, filterable, searchable, filterOptions, width) | | `apiEndpoint` | `string?` | Backend-Endpunkt fuer automatische Filter-/ID-Abfragen | | `hookData` | `{ refetch, pagination, fetchFilterValues? }` | Daten-Hook mit Refetch-Callback und Pagination-Metadata | | `supportsBackendPagination` | `boolean` | Aktiviert serverseitige Pagination, Sortierung und Filterung | ### Interaktion | Prop | Typ | Beschreibung | |------|-----|-------------| | `selectable` | `boolean` | Aktiviert Zeilen-Selektion (Checkboxen) | | `onSelectionChange` | `(selectedIds: Set) => void` | Callback bei Aenderung der Selektion | | `idField` | `string` | Feld fuer eindeutige ID (Default: `"id"`) | | `isRowSelectable` | `(row: T) => boolean` | Bestimmt, ob eine Zeile selektierbar ist | | `batchActions` | `BatchAction[]` | Bulk-Aktionen fuer selektierte Zeilen | | `actionButtons` | `ActionButton[]` | Zeilen-Aktionen (Edit, Delete etc.) | ### BatchAction Interface ```typescript interface BatchAction { label: string; icon?: React.ReactNode; loading?: boolean; onClick: (selectedRows: T[]) => void | Promise; isApplicable?: (row: T) => boolean; } ``` `isApplicable` ermoeglicht pro Action zu bestimmen, welche der selektierten Zeilen fuer diese Aktion qualifizieren. Der Button zeigt dann `(applicableCount/totalSelected)` und ist `disabled` wenn `applicableCount === 0`. --- ## Unified Filter API ### Prinzip Alle Daten-Endpunkte unterstuetzen drei Modi ueber Query-Parameter auf dem **gleichen** Endpunkt: | Modus | Query-Parameter | Response | Zweck | |-------|----------------|----------|-------| | Normal (default) | `pagination={...}` | `{ items: T[], pagination: PaginationMetadata }` | Paginierte Datenliste | | `filterValues` | `mode=filterValues&column=xxx&pagination={crossFilters}` | `string[]` | Distinct-Werte fuer Spaltenfilter-Dropdown | | `ids` | `mode=ids&pagination={filters}` | `string[]` | Alle IDs der gefilterten Datensaetze | **Kein separater `/filter-values`-Endpunkt.** Alle alten `/filter-values`-Sub-Pfade wurden entfernt (Stand 2026-04-13). ### Cross-Filtering Bei `mode=filterValues` werden alle aktiven Filter **ausser** dem angeforderten Column-Filter angewendet (Cross-Filtering). So zeigt z.B. der Status-Filter nur Werte an, die bei den aktuell gesetzten Mandant- und Typ-Filtern vorkommen. ### Beispiele ``` # Normale Liste (Seite 1, 20 Eintraege) GET /api/users/?pagination={"page":1,"pageSize":20,"sort":[{"field":"name","direction":"asc"}]} # Filter-Werte fuer Spalte "status" (mit Cross-Filter auf mandateLabel) GET /api/users/?mode=filterValues&column=status&pagination={"filters":{"mandateLabel":"Demo AG"}} # Alle IDs der gefilterten Ansicht (fuer "Select All Filtered") GET /api/users/?mode=ids&pagination={"filters":{"status":"active"},"search":"admin"} ``` ### Backend-Implementierung Zentrale Hilfsfunktionen in `gateway/modules/routes/routeHelpers.py`: | Funktion | Zweck | |----------|-------| | `handleFilterValuesMode(db, modelClass, column, ...)` | SQL-basierte Distinct-Werte via DB-Connector | | `handleIdsMode(db, modelClass, ...)` | SQL-basierte ID-Liste via DB-Connector | | `handleFilterValuesInMemory(items, column, ...)` | In-Memory Distinct-Werte (fuer enriched/aggregierte Listen) | | `handleIdsInMemory(items, ...)` | In-Memory ID-Liste | | `parseCrossFilterPagination(column, paginationJson)` | Parsed Pagination-JSON und entfernt den angeforderten Column-Filter | | `_applyFiltersAndSort(items, paginationParams)` | In-Memory Filterung und Sortierung | | `_extractDistinctValues(items, column, ...)` | Distinct-Werte aus Item-Liste extrahieren | | `paginateInMemory(items, paginationParams)` | In-Memory Paginierung | ### Route-Integration Pattern ```python @router.get("/", response_model=PaginatedResponse[MyModel]) def list_items( request: Request, pagination: Optional[str] = Query(None), mode: Optional[str] = Query(None, description="'filterValues' or 'ids'"), column: Optional[str] = Query(None, description="Column key"), context: RequestContext = Depends(getRequestContext), ): if mode in ("filterValues", "ids"): from modules.routes.routeHelpers import handleFilterValuesInMemory, handleIdsInMemory items = _buildFullList(context) if mode == "filterValues": if not column: raise HTTPException(status_code=400, detail="column required") return handleFilterValuesInMemory(items, column, pagination) return handleIdsInMemory(items, pagination) # Normal paginated list... ``` --- ## Selektion ### ID-basierte Selektion Die Selektion arbeitet mit **IDs** (nicht Zeilen-Indizes). Das `idField`-Prop bestimmt, welches Feld als eindeutiger Schluessel verwendet wird (Default: `"id"`). | State | Typ | Beschreibung | |-------|-----|-------------| | `selectedIds` | `Set` | Menge der selektierten IDs | ### Selektion-Reset Die Selektion wird automatisch zurueckgesetzt bei: - Seitenwechsel (currentPage) - Seitengroesse-Aenderung (pageSize) - Filter-Aenderung - Suchbegriff-Aenderung - Sortier-Aenderung ### Select All Filtered Wenn `apiEndpoint` und `supportsBackendPagination` gesetzt sind, zeigt die Toolbar einen "Alle X Eintraege auswaehlen"-Button. Dieser ruft `GET {apiEndpoint}?mode=ids&pagination={currentFilters}` auf und fuegt alle zurueckgegebenen IDs zur Selektion hinzu. Ein "Auswahl aufheben"-Button erscheint, wenn "Select All Filtered" aktiv ist. ### Row-Level Delete Wenn eine Zeile geloescht wird (via `actionButtons` Delete), wird deren ID automatisch aus `selectedIds` entfernt. --- ## FilterValuesList (Spaltenfilter-Dropdown) Wenn ein filterbarer Spalten-Header geklickt wird, laedt das Dropdown die Distinct-Werte: 1. `column.filterOptions` (statische Enum) -- wird direkt verwendet, kein Backend-Call 2. `hookData.fetchFilterValues(columnKey, crossFilters)` -- falls im Hook bereitgestellt 3. `GET {apiEndpoint}?mode=filterValues&column=xxx&pagination={currentFilters}` -- automatisch Bei mehr als 10 Werten erscheint ein **Suchfeld** (Client-seitige Filterung der geladenen Werte). Boolean-Spalten rendern als "Ja"/"Nein". Datum-Spalten rendern als Range-Picker. --- ## Migrierte Endpunkte (Stand 2026-04-13) Alle folgenden Endpunkte unterstuetzen `mode=filterValues` und `mode=ids`: ### Core Routes | Route-Datei | Endpunkt | Methode | |-------------|----------|---------| | `routeDataUsers.py` | `GET /api/users/` | SQL + In-Memory (je nach Scope) | | `routeDataConnections.py` | `GET /api/connections/` | In-Memory | | `routeDataPrompts.py` | `GET /api/prompts/` | In-Memory | | `routeInvitations.py` | `GET /api/invitations/` | In-Memory | | `routeSubscription.py` | `GET /api/subscriptions/admin/all` | In-Memory | | `routeAdminFeatures.py` | `GET /api/admin/features/instances` | In-Memory | | `routeAdminFeatures.py` | `GET /api/admin/features/templates/roles` | In-Memory | | `routeAdminFeatures.py` | `GET /api/admin/features/instances/{id}/users` | In-Memory | | `routeAdminRbacRules.py` | `GET /api/admin/rbac/roles` | In-Memory | | `routeDataMandates.py` | `GET /api/mandates/` | SQL + In-Memory (je nach Scope) | | `routeDataMandates.py` | `GET /api/mandates/{id}/users` | In-Memory | | `routeDataFiles.py` | `GET /api/files/list` | SQL + In-Memory Fallback | | `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/` (Runs) | Enriched + SQL | | `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/workflows` | Enriched + SQL | | `routeBilling.py` | `GET /api/billing/view/users/transactions` | Billing-Interface | ### Feature Routes | Route-Datei | Endpunkt | Methode | |-------------|----------|---------| | `routeFeatureTrustee.py` | `GET /api/trustee/{id}/documents` | RBAC SQL + In-Memory Fallback | | `routeFeatureTrustee.py` | `GET /api/trustee/{id}/positions` | RBAC SQL + In-Memory Fallback | | `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/projects` | In-Memory | | `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/parcels` | In-Memory |