274 lines
12 KiB
Markdown
274 lines
12 KiB
Markdown
<!-- status: canonical -->
|
|
<!-- lastReviewed: 2026-04-13 -->
|
|
<!-- verifiedAgainst: frontend_nyla (FormGeneratorTable.tsx, FormGeneratorControls.tsx) + gateway routeHelpers.py -->
|
|
|
|
# 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 = () => (
|
|
<div className={`${adminStyles.adminPage} ${adminStyles.adminPageFill}`}>
|
|
<div className={adminStyles.pageHeader}>...</div>
|
|
|
|
<div className={adminStyles.tableContainer}>
|
|
<FormGeneratorTable {...props} />
|
|
</div>
|
|
</div>
|
|
);
|
|
```
|
|
|
|
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 `<div>` 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<string>) => 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<T> {
|
|
label: string;
|
|
icon?: React.ReactNode;
|
|
loading?: boolean;
|
|
onClick: (selectedRows: T[]) => void | Promise<void>;
|
|
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<string>` | 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 |
|