460 lines
20 KiB
Markdown
460 lines
20 KiB
Markdown
<!-- status: plan -->
|
||
<!-- started: 2026-04-29 -->
|
||
<!-- component: frontend-nyla | gateway -->
|
||
|
||
# FormGenerator: Persistente Benutzer-Gruppierung
|
||
|
||
## Beschreibung und Kontext
|
||
|
||
Der `FormGeneratorTable` wird auf vielen Seiten der Plattform genutzt. Nutzer sollen Einträge in benannte, rekursive Gruppen organisieren können — mit persistenter Speicherung und vollständiger Kompatibilität mit Pagination, Suche, Filter und allen Action-Buttons.
|
||
|
||
**Kernprinzip: Grouping ist ein eingebautes Feature von `PaginationParams` und `PaginatedResponse` — kein separater Call, keine eigene Route, kein eigenes API-Modul. Der bestehende `refetch()`-Mechanismus ist der einzige Transport.**
|
||
|
||
---
|
||
|
||
## Architektur-Kern: Wie es funktioniert
|
||
|
||
### Grouping reitet auf dem bestehenden Pagination-Call
|
||
|
||
`PaginationParams` (der JSON-Parameter jedes List-Endpoints) bekommt zwei neue optionale Felder:
|
||
|
||
```
|
||
saveGroupTree → wenn gesetzt: Backend speichert diesen Baum VOR dem Fetch
|
||
groupId → wenn gesetzt: Backend filtert Items auf Items dieser Gruppe
|
||
```
|
||
|
||
`PaginatedResponse` bekommt ein neues optionales Feld:
|
||
|
||
```
|
||
groupTree → aktueller Gruppen-Baum des Users für diesen Endpoint (immer mitgeliefert)
|
||
```
|
||
|
||
**Ein Aufruf tut damit drei Dinge auf einmal:**
|
||
1. Speichert den neuen Gruppen-Baum (wenn `saveGroupTree` gesetzt)
|
||
2. Filtert auf eine Gruppe (wenn `groupId` gesetzt)
|
||
3. Gibt aktuelle Items + aktuelle Gruppen-Baum zurück
|
||
|
||
### Ablauf End-to-End
|
||
|
||
```
|
||
Seitenaufruf (erster Load):
|
||
GET /api/connections/?pagination={"page":1,"pageSize":20}
|
||
← { items: [...], pagination: {...}, groupTree: [{id, name, itemIds, subGroups}] }
|
||
|
||
User erstellt Gruppe (lokal sofort sichtbar, dann debounced Save via refetch):
|
||
GET /api/connections/?pagination={"page":1,"pageSize":20,"saveGroupTree":[{neuerBaum}]}
|
||
← { items: [...], pagination: {...}, groupTree: [{neuerBaum, vom Backend bestätigt}] }
|
||
|
||
User betritt Gruppe "Kunden" (id: "g1"):
|
||
GET /api/connections/?pagination={"page":1,"pageSize":20,"groupId":"g1"}
|
||
← { items: [nur Items der Gruppe], pagination: {totalItems: 3, ...}, groupTree: [...] }
|
||
→ Suche, Filter, Sortierung, mode=ids, mode=filterValues — alles funktioniert
|
||
innerhalb des Gruppen-Scopes, da das Backend die IN-Liste kennt
|
||
```
|
||
|
||
### Backend: Pro Route genau 2 Zeilen Overhead
|
||
|
||
Der gesamte Grouping-Mechanismus ist in `routeHelpers.py` als shared Helper implementiert. Jede Route die Grouping unterstützen soll, ruft ihn auf:
|
||
|
||
```python
|
||
# Anfang der Route-Funktion (BEVOR items gebaut werden):
|
||
groupCtx = handleGroupingInRequest(paginationParams, interface, "connections")
|
||
# → speichert saveGroupTree falls vorhanden
|
||
# → gibt groupIdItemIds zurück falls groupId gesetzt
|
||
|
||
# Items bauen (unverändert)...
|
||
|
||
# Falls Gruppen-Scope aktiv: Items auf Gruppe einschränken
|
||
items = applyGroupScopeFilter(items, groupCtx.itemIds)
|
||
|
||
# Am Ende: groupTree in Response einbetten
|
||
return {**result, "groupTree": groupCtx.groupTree}
|
||
```
|
||
|
||
**Kein neues Route-File. Kein neues Interface-File. Keine neue URL.**
|
||
|
||
---
|
||
|
||
## Betroffene Module
|
||
|
||
- **Gateway:**
|
||
- `modules/datamodels/datamodelPagination.py` — `PaginationParams` + `groupId`, `saveGroupTree`; `PaginatedResponse` + `groupTree`; neue Klassen `TableGroupNode` + `TableGrouping` in **dieser Datei**
|
||
- `modules/interfaces/interfaceDbApp.py` — `AppObjects` um `getTableGrouping(contextKey)` + `upsertTableGrouping(contextKey, rootGroups)` erweitern; neue Tabelle `table_groupings` in `poweron_app` (auto-created)
|
||
- `modules/routes/routeHelpers.py` — `handleGroupingInRequest(paginationParams, interface, contextKey)` + `applyGroupScopeFilter(items, itemIds)` hinzufügen
|
||
- Jede List-Route die Grouping unterstützen soll: **2 Zeilen** am Anfang + **1 Feld** in der Response (`groupTree`)
|
||
|
||
- **Frontend:**
|
||
- `FormGeneratorTable.tsx` — `groupingConfig`-Prop; interner Grouping-State; nutzt `hookData.refetch()` als einzigen Transport
|
||
- `FormGeneratorControls.tsx` — Gruppen-Toolbar-Button
|
||
- `FormGenerator/GroupingManager/GroupRow.tsx` — Gruppen-Header-Zeile (neue Komponente)
|
||
- `FormGenerator/GroupingManager/GroupingManager.tsx` — Seitenpanel (neue Komponente)
|
||
- **Kein neuer Hook, kein neues API-Modul, keine Änderungen an bestehenden Feature-Hooks**
|
||
|
||
- **DB-Migration:** Nein (Auto-Create via DatabaseConnector)
|
||
|
||
---
|
||
|
||
## Datenmodell
|
||
|
||
### Ergänzungen in `datamodelPagination.py`
|
||
|
||
```python
|
||
# --- Grouping-Modelle (neu, in derselben Datei) ---
|
||
|
||
class TableGroupNode(BaseModel):
|
||
id: str
|
||
name: str
|
||
itemIds: List[str] = Field(default_factory=list)
|
||
subGroups: List['TableGroupNode'] = Field(default_factory=list)
|
||
order: int = 0
|
||
isExpanded: bool = True
|
||
|
||
TableGroupNode.model_rebuild()
|
||
|
||
class TableGrouping(BaseModel):
|
||
"""DB-Tabelle table_groupings in poweron_app."""
|
||
id: str
|
||
userId: str
|
||
contextKey: str # abgeleitet aus Route-Prefix, z. B. "connections", "prompts", "admin/users"
|
||
rootGroups: List[TableGroupNode] = Field(default_factory=list)
|
||
updatedAt: Optional[float] = None
|
||
|
||
|
||
# --- Erweiterung PaginationParams (2 neue optionale Felder) ---
|
||
|
||
class PaginationParams(BaseModel):
|
||
page: int = Field(ge=1)
|
||
pageSize: int = Field(ge=1, le=1000)
|
||
sort: List[SortField] = Field(default_factory=list)
|
||
filters: Optional[Dict[str, Any]] = None
|
||
# NEU:
|
||
groupId: Optional[str] = None # Scope: nur Items dieser Gruppe
|
||
saveGroupTree: Optional[List[Dict[str, Any]]] = None # Persistieren: diesen Baum speichern
|
||
|
||
|
||
# --- Erweiterung PaginatedResponse (1 neues optionales Feld) ---
|
||
|
||
class PaginatedResponse(BaseModel, Generic[T]):
|
||
items: List[T]
|
||
pagination: Optional[PaginationMetadata]
|
||
groupTree: Optional[List[TableGroupNode]] = None # NEU — immer mitgeliefert wenn vorhanden
|
||
|
||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||
```
|
||
|
||
---
|
||
|
||
## Backend-Implementierung
|
||
|
||
### `routeHelpers.py` — neuer shared Helper
|
||
|
||
```python
|
||
from dataclasses import dataclass
|
||
from typing import Optional, Set
|
||
|
||
@dataclass
|
||
class GroupingContext:
|
||
groupTree: Optional[list] # Für die Response
|
||
itemIds: Optional[Set[str]] # Falls groupId gesetzt — IN-Filter-Menge
|
||
|
||
|
||
def handleGroupingInRequest(
|
||
paginationParams: Optional[PaginationParams],
|
||
interface, # AppObjects
|
||
contextKey: str,
|
||
) -> GroupingContext:
|
||
"""
|
||
Zentraler Grouping-Handler — aufgerufen am Anfang jeder List-Route.
|
||
|
||
1. Falls paginationParams.saveGroupTree gesetzt:
|
||
→ interface.upsertTableGrouping(contextKey, saveGroupTree)
|
||
→ saveGroupTree aus params entfernen (wird nicht weiter verarbeitet)
|
||
|
||
2. Falls paginationParams.groupId gesetzt:
|
||
→ Gruppe im gespeicherten Baum suchen (rekursiv inkl. Subgruppen)
|
||
→ itemIds der Gruppe (+ alle Subgruppen) als Set zurückgeben
|
||
→ groupId aus params entfernen (wird nicht als normaler Filter verarbeitet)
|
||
|
||
3. Aktuellen groupTree laden und für Response bereitstellen.
|
||
|
||
Returns: GroupingContext(groupTree, itemIds)
|
||
"""
|
||
|
||
|
||
def applyGroupScopeFilter(items: list, itemIds: Optional[Set[str]]) -> list:
|
||
"""
|
||
Wendet den Gruppen-Scope-Filter an.
|
||
Gibt items unverändert zurück wenn itemIds is None (kein Scope aktiv).
|
||
Filtert sonst auf item["id"] in itemIds.
|
||
"""
|
||
if itemIds is None:
|
||
return items
|
||
return [item for item in items if str(item.get("id", "")) in itemIds]
|
||
```
|
||
|
||
### Route-Erweiterung — Muster (2 + 1 Zeilen)
|
||
|
||
```python
|
||
@router.get("/")
|
||
async def get_connections(request, pagination=None, mode=None, column=None, currentUser=Depends(getCurrentUser)):
|
||
from modules.routes.routeHelpers import handleGroupingInRequest, applyGroupScopeFilter
|
||
|
||
interface = getInterface(currentUser)
|
||
CONTEXT_KEY = "connections"
|
||
|
||
# 1. Grouping verarbeiten (speichern falls nötig, Scope auflösen)
|
||
groupCtx = handleGroupingInRequest(paginationParams, interface, CONTEXT_KEY)
|
||
|
||
# mode=filterValues / mode=ids (unverändert, aber groupId ist bereits aus params entfernt)
|
||
if mode == "filterValues": ...
|
||
if mode == "ids":
|
||
items = _buildEnhancedItems()
|
||
items = applyGroupScopeFilter(items, groupCtx.itemIds) # Scope auch für ids!
|
||
return handleIdsInMemory(items, pagination)
|
||
|
||
# Items bauen (unverändert)
|
||
items = _buildEnhancedItems()
|
||
|
||
# 2. Gruppen-Scope-Filter anwenden
|
||
items = applyGroupScopeFilter(items, groupCtx.itemIds)
|
||
|
||
# Pagination (unverändert)
|
||
result = paginateInMemory(items, paginationParams)
|
||
|
||
# 3. groupTree in Response einbetten
|
||
return {**result.model_dump(), "groupTree": groupCtx.groupTree}
|
||
```
|
||
|
||
**`mode=filterValues` und `mode=ids` funktionieren automatisch korrekt im Gruppen-Scope**, weil `groupId` aus `paginationParams` entfernt wurde und `applyGroupScopeFilter` aufgerufen wird — dadurch beziehen sich Filter-Dropdowns und "Select All Filtered" auf Items der aktuellen Gruppe.
|
||
|
||
---
|
||
|
||
## Frontend-Implementierung
|
||
|
||
### `FormGeneratorTable` — interner Grouping-State
|
||
|
||
```typescript
|
||
// Neues Prop:
|
||
groupingConfig?: {
|
||
contextKey: string; // Nur für RBAC/Logging — der eigentliche Key wird serverseitig aus dem Endpoint abgeleitet
|
||
enabled: boolean;
|
||
}
|
||
|
||
// Interner State (nur in FormGeneratorTable, nicht im Hook):
|
||
const [groupTree, setGroupTree] = useState<TableGroupNode[]>([]);
|
||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
||
const [pendingGroupTree, setPendingGroupTree] = useState<TableGroupNode[] | null>(null);
|
||
```
|
||
|
||
### Datenfluss — `hookData.refetch()` als einziger Transport
|
||
|
||
```typescript
|
||
// groupTree kommt aus der normalen refetch-Response:
|
||
useEffect(() => {
|
||
if (hookData?.pagination?.groupTree) {
|
||
setGroupTree(hookData.pagination.groupTree);
|
||
setPendingGroupTree(null); // Gespeichert — pending löschen
|
||
}
|
||
}, [hookData]);
|
||
|
||
// Beim Betreten eines Gruppen-Scopes:
|
||
const _enterGroup = (groupId: string) => {
|
||
setActiveGroupId(groupId);
|
||
hookData.refetch({ ...currentPaginationParams, page: 1, groupId });
|
||
};
|
||
|
||
// Beim Verlassen des Scopes:
|
||
const _exitGroup = () => {
|
||
setActiveGroupId(null);
|
||
hookData.refetch({ ...currentPaginationParams, page: 1, groupId: undefined });
|
||
};
|
||
|
||
// Bei Gruppen-Mutation (erstellen, umbenennen, löschen, Item zuordnen):
|
||
const _mutateGroupTree = (newTree: TableGroupNode[]) => {
|
||
setGroupTree(newTree); // Sofort lokal sichtbar (optimistic)
|
||
setPendingGroupTree(newTree); // Markiert für nächsten Save
|
||
_debouncedSave(newTree); // Debounced: nach 500ms via refetch speichern
|
||
};
|
||
|
||
// Debounced Save: normaler refetch + saveGroupTree im pagination param
|
||
const _debouncedSave = useMemo(() => debounce((tree: TableGroupNode[]) => {
|
||
hookData.refetch({
|
||
...currentPaginationParams,
|
||
saveGroupTree: tree, // Backend speichert und bestätigt
|
||
});
|
||
}, 500), [hookData, currentPaginationParams]);
|
||
```
|
||
|
||
**Kein neues API-Modul. Kein eigener fetch-Call. `hookData.refetch()` ist alles.**
|
||
|
||
### Render-Struktur
|
||
|
||
```
|
||
Root-View (kein activeGroupId):
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
▼ Kunden (3 Items) [Delete all] [Download all] [+ Subgruppe] [Umbenennen] [×]
|
||
▶ Aktive (1) [Delete all] [+ Subgruppe] [Umbenennen] [×]
|
||
— Item A [Edit] [Delete] [Download]
|
||
— Item B [Edit] [Delete] [Download]
|
||
— Item C (Aktive) [Edit] [Delete] [Download]
|
||
▶ Intern [5 Items — Gruppe öffnen →]
|
||
── Nicht zugeordnet (2)
|
||
— Item X [Edit] [Delete] [Download]
|
||
|
||
Gruppen-Scope (activeGroupId = "Intern"):
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
← Zurück | Intern (Seite 1/1 · 5 Einträge) [Delete all] [Download all]
|
||
— Item C [Edit] [Delete] [Download]
|
||
— Item D [Edit] [Delete] [Download]
|
||
...
|
||
```
|
||
|
||
Wenn `activeGroupId` gesetzt: refetch läuft mit `groupId` → Backend filtert → Pagination, Suche, Filter, `mode=ids` — alles auf Gruppe begrenzt.
|
||
|
||
---
|
||
|
||
## Pagination, Suche, Filter — vollständig korrekt
|
||
|
||
| Szenario | Was passiert |
|
||
|----------|-------------|
|
||
| Root-View, Seite blättern | Normaler refetch. `groupTree` kommt in Response mit. Gruppen-Counter aus `groupNode.itemIds.length`. |
|
||
| Gruppen-Scope aktiv | `groupId` in PaginationParams → Backend IN-Filter → Totalcount kommt vom Backend (korrekt). |
|
||
| Suche im Scope | `groupId` + `filters.search` → Backend filtert Items der Gruppe nach Suchtext. |
|
||
| "Select All Filtered" (mode=ids) | `groupId` in params → `applyGroupScopeFilter` wird VOR `handleIdsInMemory` angewendet → nur IDs der Gruppe werden zurückgegeben. |
|
||
| Filter-Dropdown (mode=filterValues) | `groupId` in params → `applyGroupScopeFilter` vor `handleFilterValuesInMemory` → Distinct-Werte kommen nur aus Gruppen-Items. |
|
||
| Gruppen-Baum speichern während Paginieren | `saveGroupTree` + Seiten-Params im gleichen Call → Backend speichert Baum UND gibt aktuelle Seite zurück. |
|
||
|
||
---
|
||
|
||
## Entscheidungen
|
||
|
||
| Datum | Entscheidung | Begründung |
|
||
|-------|-------------|------------|
|
||
| 2026-04-29 | `saveGroupTree` + `groupId` in `PaginationParams` statt eigener Endpoint | Ein Call, ein Transport, kein zweiter API-Pfad; Grouping ist integraler Bestandteil der Datenabfrage |
|
||
| 2026-04-29 | `groupTree` in `PaginatedResponse` | Immer synchron mit aktuellen Items; kein separater Lade-Call nötig |
|
||
| 2026-04-29 | Shared Helper in `routeHelpers.py`, 2 Zeilen pro Route | DRY; gesamte Komplexität an einem Ort; pro Route null Eigenlogik |
|
||
| 2026-04-29 | Optimistic UI + debounced Save via normalen refetch | UX sofort; kein Flackern; Persistenz ohne Extra-Call |
|
||
| 2026-04-29 | Modelle in `datamodelPagination.py` | `PaginatedResponse` liegt dort; keine neue Datei; Import-Graph bleibt minimal |
|
||
| 2026-04-29 | `applyGroupScopeFilter` auch für `mode=ids` und `mode=filterValues` | Filter-Dropdown und Bulk-Select funktionieren korrekt im Gruppen-Scope ohne Sonderbehandlung |
|
||
| 2026-04-29 | Gruppen-State nur in `FormGeneratorTable`, nicht im Feature-Hook | Keine Änderungen an bestehenden Hooks nötig; Grouping ist transparent für Seiten-Code |
|
||
|
||
---
|
||
|
||
## Umsetzungs-Checkliste
|
||
|
||
### Phase 1: Backend Core
|
||
|
||
- [ ] `datamodelPagination.py`: `TableGroupNode`, `TableGrouping` Klassen hinzufügen
|
||
- [ ] `datamodelPagination.py`: `PaginationParams` um `groupId` + `saveGroupTree` erweitern
|
||
- [ ] `datamodelPagination.py`: `PaginatedResponse` um `groupTree` erweitern
|
||
- [ ] `datamodelPagination.py`: `normalize_pagination_dict` so erweitern, dass `saveGroupTree` und `groupId` korrekt geparst werden
|
||
- [ ] `interfaceDbApp.py`: `getTableGrouping(contextKey)` + `upsertTableGrouping(contextKey, rootGroups)` zu `AppObjects` hinzufügen
|
||
- [ ] `routeHelpers.py`: `GroupingContext` Dataclass + `handleGroupingInRequest()` + `applyGroupScopeFilter()` implementieren
|
||
|
||
### Phase 2: Route-Erweiterungen
|
||
|
||
Pro Route: `handleGroupingInRequest` am Anfang + `applyGroupScopeFilter` vor Pagination + `groupTree` in Response
|
||
|
||
- [ ] `routeDataConnections.py` (inkl. `mode=ids`, `mode=filterValues`)
|
||
- [ ] `routeDataPrompts.py`
|
||
- [ ] `routeDataUsers.py`
|
||
- [ ] `routeDataMandates.py`
|
||
- [ ] `routeDataFiles.py`
|
||
- [ ] Weitere nach Bedarf (Trustee, RealEstate, Invitations, …)
|
||
|
||
### Phase 3: Frontend Typen + FormGeneratorTable Grundgerüst
|
||
|
||
- [ ] TypeScript-Typen `TableGroupNode` direkt in `FormGeneratorTable.tsx` oder `src/types/tableGrouping.ts`
|
||
- [ ] `groupingConfig`-Prop zu `FormGeneratorTableProps` hinzufügen
|
||
- [ ] `groupTree` aus `hookData`-Response parsen und in internem State halten
|
||
- [ ] `activeGroupId`-State + `_enterGroup` / `_exitGroup`
|
||
- [ ] `pendingGroupTree`-State + `_mutateGroupTree` + debounced Save via `hookData.refetch()`
|
||
- [ ] `PaginatedResponse` Frontend-Typ um `groupTree` erweitern
|
||
|
||
### Phase 4: Render-Logik
|
||
|
||
- [ ] `GroupRow`-Komponente: `src/components/FormGenerator/GroupingManager/GroupRow.tsx`
|
||
- [ ] Render-Algorithmus: Root-View mit Gruppen-Header-Zeilen und DataRows; "Nicht zugeordnet"-Sektion
|
||
- [ ] Gruppen-Scope-View: Breadcrumb, "Zurück"-Button, normales Table-Layout
|
||
- [ ] Gedimmte Gruppen ohne sichtbare Items (Root-View) mit "Gruppe öffnen"-Button
|
||
- [ ] Expand/Collapse je Gruppe (lokal; isExpanded kommt aus `groupTree`)
|
||
|
||
### Phase 5: Aktionen und Interaktion
|
||
|
||
- [ ] `actionButtons` + `customActions` auf `GroupRow`-Ebene (Batch auf alle Items via `mode=ids` im Scope)
|
||
- [ ] Delete-Gruppe mit `useConfirm`: "Nur Gruppe" vs. "Gruppe + alle Items löschen"
|
||
- [ ] "In Gruppe verschieben" als BatchAction bei Multi-Select
|
||
- [ ] Kontextmenü-Button je DataRow → Gruppen-Dropdown
|
||
- [ ] Drag-and-Drop DataRow → GroupRow
|
||
|
||
### Phase 6: GroupingManager Panel
|
||
|
||
- [ ] `GroupingManager`-Komponente: Gruppen-Baum-Panel
|
||
- [ ] Button "Gruppen" in `FormGeneratorControls` (nur wenn `groupingConfig.enabled`)
|
||
- [ ] Neue Gruppe erstellen (`usePrompt`)
|
||
- [ ] Gruppe umbenennen (inline)
|
||
- [ ] Subgruppe erstellen
|
||
- [ ] Gruppe löschen (`useConfirm`)
|
||
- [ ] Reihenfolge Up/Down
|
||
|
||
### Abschluss
|
||
|
||
- [ ] i18n: alle neuen UI-Texte mit `t('...')` getaggt
|
||
- [ ] CSS Modules für alle neuen Komponenten
|
||
- [ ] `b-reference/frontend-nyla/formgenerator.md` aktualisieren
|
||
- [ ] `b-reference/gateway/architecture.md` — `PaginationParams`/`PaginatedResponse`-Erweiterung dokumentieren
|
||
|
||
---
|
||
|
||
## Akzeptanzkriterien
|
||
|
||
| # | Kriterium (Given-When-Then) | Prio |
|
||
|---|---------------------------|------|
|
||
| 1 | Given `FormGeneratorTable` mit `groupingConfig.enabled` — When Seite lädt — Then kommt `groupTree` in der normalen List-Response mit; **kein zweiter API-Call** | must |
|
||
| 2 | Given User erstellt Gruppe — When 500ms nach letzter Änderung — Then ein einziger `refetch` mit `saveGroupTree` wird abgesendet; nach Reload ist Gruppe vorhanden | must |
|
||
| 3 | Given User klickt Gruppe "Kunden" — When `_enterGroup` aufgerufen — Then refetch mit `groupId`; Backend filtert; Pagination und Totalcount beziehen sich auf die Gruppe | must |
|
||
| 4 | Given aktiver Gruppen-Scope — When User sucht "Test" — Then `groupId` + Search in einem Call; Backend zeigt nur Treffer in der Gruppe | must |
|
||
| 5 | Given aktiver Gruppen-Scope — When User klickt "Alle auswählen" (mode=ids) — Then IDs kommen nur aus der Gruppe, nicht aus der Gesamtliste | must |
|
||
| 6 | Given Filter-Dropdown geöffnet im Gruppen-Scope — When Werte geladen — Then kommen nur aus Items der Gruppe (mode=filterValues korrekt) | should |
|
||
| 7 | Given `FormGeneratorTable` ohne `groupingConfig` — Then identisches Verhalten wie vor dem Feature | must |
|
||
| 8 | Given Gruppe mit 5 Items — When User Delete auf Group-Header — Then Confirm → alle 5 Items gelöscht; Gruppe aus Baum entfernt; ein `refetch` mit aktuellem `saveGroupTree` | must |
|
||
| 9 | Given User zieht DataRow auf Gruppen-Header — Then Item wird zur Gruppe zugeordnet; `_mutateGroupTree` + debounced Save | should |
|
||
|
||
---
|
||
|
||
## Testplan
|
||
|
||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||
|----|----|----|--------------|-----------|--------|
|
||
| T1 | 1, 2 | api | ja | `gateway/tests/test_grouping_helpers.py` | pending |
|
||
| T2 | 3, 4, 5, 6 | api | ja | `gateway/tests/test_grouping_helpers.py` | pending |
|
||
| T3 | 7 | component | nein | manuell | pending |
|
||
| T4 | 8 | api + component | nein | manuell | pending |
|
||
| T5 | 9 | component | nein | manuell | pending |
|
||
|
||
---
|
||
|
||
## Offene Fragen
|
||
|
||
1. **`scope: 'user' | 'mandate'`** im `TableGrouping`-Modell bereits vorbereiten für späteres mandate-weites Sharing?
|
||
2. **CSV-Export** soll Gruppen-Spalte enthalten?
|
||
3. **`activeGroupId` im URL-State** (`?group=g1`) für Deep-Links?
|
||
4. **Max-Tiefe** konfigurierbar (`groupingConfig.maxDepth`) oder feste Warnung nach 3 Ebenen?
|
||
|
||
---
|
||
|
||
## Links
|
||
|
||
- PR: —
|
||
- Referenz FormGenerator: `b-reference/frontend-nyla/formgenerator.md`
|
||
- Referenz Gateway-Architektur: `b-reference/gateway/architecture.md`
|
||
- Referenz DB-Architektur: `b-reference/platform/database-architecture.md`
|
||
|
||
---
|
||
|
||
## Abschluss
|
||
|
||
- [ ] `b-reference/frontend-nyla/formgenerator.md` — Grouping-Sektion
|
||
- [ ] `b-reference/gateway/architecture.md` — `PaginationParams`/`PaginatedResponse` Erweiterung
|
||
- [ ] TOPICS.md geprüft
|
||
- [ ] Dieses Dokument → `z-archive/` verschoben
|