# FormGeneratorTree (NEU) + Recovery der persistenten Folder/File-Funktionen ## Beschreibung und Kontext In den Commits `c8e9304` (2026-04-29), `e7a79a3` und `7c05cb0` (beide 2026-04-30, zusammen -1263 / +897) wurde `FolderTree` aus - `ui-nyla/src/pages/basedata/FilesPage.tsx` - `ui-nyla/src/components/UnifiedDataBar/FilesTab.tsx` entfernt und durch `FormGeneratorTable` mit `groupingConfig` ersetzt. Begleitend wurden die Folder-Operationen aus `FileContext.tsx` und `useFiles.ts` reduziert, `FileInfo.folderId` aus `fileApi.ts` und das Pydantic-Feld `FileItem.folderId` aus `platform-core/modules/datamodels/datamodelFiles.py` entfernt. Im Repo liegt das Archiviertes Skript `platform-core/modules/migrations/_archive/migrate_folders_to_groups.py`, das die `FileFolder`-Tabelle + `FileItem.folderId`-Spalte in einen JSON-Tree (`TableGrouping.rootGroups`, Context `files/list`) ueberfuehren *kann* -- per Default `--dry-run`, ohne begleitende Alembic-Migration zum DROP-COLUMN / DROP-TABLE. **Datenbank-Stand:** `FileFolder` und `FileItem.folderId` sind in der DB unveraendert vorhanden. Recovery laeuft als reine Pydantic-/Routen-/FE-Wieder- verdrahtung -- **kein** neues Schema fuer diesen Track. ### Beobachtete funktionale Luecken (nach dem Wechsel zu Table-only) - Folders koennen nicht mehr neu erstellt werden (UI-Affordance entfernt). - Pro-File / pro-Folder Scope-Icons + Neutralize-Toggle fehlen (`patchGroupScope` / `patchGroupNeutralize` sind Bulk-Operationen auf Group- Items und ersetzen die Per-Item-UI nicht). - Multiselect / Multidelete mit Cascade ueber gemischte Folders+Files-Auswahl ist nicht abgebildet. - DnD-Protokoll inkompatibel: `application/group-id`, `application/group-file-ids` und `application/tree-items` mit `type: 'file' | 'group'` waren auf Tabellen-Gruppierungen optimiert; `WorkspaceInput.TreeItemDrop` musste mitziehen. - Mandanten-geteilte Folder-Strukturen lassen sich mit per-User-`TableGrouping` nicht abbilden. ### Tree-Fragmentierung im Frontend (zusaetzlicher Treiber) Heute existieren mindestens **acht parallele** Tree-Implementierungen: | # | Datei | Inhalt | Lazy | Multiselect | Inline-Actions | |---|------|--------|---|---|---| | 1 | `components/FolderTree/FolderTree.tsx` | Files+Folders (entkoppelt) | nein | ja | ja | | 2 | `components/FolderTree/SharepointBrowseTree.tsx` | SharePoint-Browse | ja | nein | nein | | 3 | `components/UnifiedDataBar/SourcesTab.tsx` | Connections + Feature-Daten | ja | partiell | partiell | | 4 | `components/UnifiedDataBar/ChatsTab.tsx` | Chat-Historien | nein | nein | partiell | | 5 | `components/FlowEditor/nodes/shared/DataPicker.tsx` | Upstream-Outputs | nein | nein | nein | | 6 | `components/Navigation/TreeNavigation/*` + `MandateNavigation.tsx` | Sidebar | nein | nein | nein | | 7 | `pages/admin/InstanceHierarchyView.tsx` | RBAC-Hierarchie | nein | nein | nein | | 8 | `pages/views/redmine/redmineTreeLogic.ts` | Redmine-Issues | nein | nein | partiell | | 9 | `components/AccessRules/AccessRulesEditor.tsx` | RBAC-Rules | nein | nein | partiell | ### Geschaeftstreiber 1. Recovery der Datei-/Ordner-Funktionen, ohne `FormGeneratorTable.groupingConfig` fuer andere Use-Cases zurueckzubauen. 2. Konsolidierung der Tree-Implementierungen auf eine getestete Komponente -- ein Bugfix-/Feature-Punkt statt acht. 3. UDB-Konsistenz: drei feste Tabs (Files/Folders, Sources, Chats), alle als hierarchische Sicht auf persistente Entitaeten. --- ## Architektur ### `FormGeneratorTree` als neues Familienmitglied Die FormGenerator-Familie wird um eine Tree-Komponente erweitert (parallel zu `FormGeneratorTable`, `FormGeneratorForm`, `FormGeneratorList`, `FormGeneratorReport`). | Aspekt | Bestimmung | |---|---| | Aufgabe | Hierarchische, lazy-ladbare Sicht auf persistente Entitaeten | | Datenbindung | Pro Use-Case ein **Provider** (siehe Provider-Interface unten) | | Spalten | Backend-Attribut-Resolution analog `resolveColumnTypes`; **keine** hardcoded Cell-Renderer in der Page; Pflicht-Spalte = Tree-Spalte (Indent + Chevron + Icon + Name) | | Selektion | Multiselect mit Shift/Ctrl/Cmd, kaskadierend (Folder selektieren -> alle eigenen Children); `BatchAction[]`-API analog Table | | DnD | Standard-MIME `application/x-poweron-tree-items`, Payload `{ id, type, name, providerKey }[]` | | Nicht zustaendig | Report-Rendering (das ist `FormGeneratorReport`); Forms (`FormGeneratorForm`); eigene Datenpersistenz (haelt der Provider); Replikation der `groupingConfig`-Logik aus `FormGeneratorTable` | **Provider-Interface (vorlaeufig):** ```typescript type Ownership = 'own' | 'shared'; interface TreeNode { id: string; name: string; parentId: string | null; ownership: Ownership; scope?: 'personal' | 'featureInstance' | 'mandate' | 'global'; neutralize?: boolean; // Parent-Kontext (z.B. Folder) ist fuer den Current-User nicht sichtbar; // Knoten wird im Root mit Hint-Icon gerendert. contextOrphan?: boolean; data?: T; } interface TreeNodeProvider { rootKey: string; loadChildren(parentId: string | null, ownership: Ownership): Promise[]>; getAttributes(): Promise; // Mutations gelten ausschliesslich fuer ownership='own'. canCreate?(parentId: string | null): boolean; canRename?(node: TreeNode): boolean; canDelete?(node: TreeNode): boolean; canMove?(source: TreeNode, target: TreeNode | null): boolean; canPatchScope?(node: TreeNode): boolean; canPatchNeutralize?(node: TreeNode): boolean; createChild?(parentId: string | null, name: string): Promise>; renameNode?(id: string, newName: string): Promise; deleteNodes?(ids: string[]): Promise; moveNodes?(ids: string[], targetParentId: string | null): Promise; patchScope?(ids: string[], scope: string, cascadeChildren?: boolean): Promise; patchNeutralize?(ids: string[], neutralize: boolean): Promise; } ``` ### Folder/File vs. `TableGrouping` (zwei orthogonale Konzepte) | Eigenschaft | Folder/File (`FileFolder`, `FileItem`) | Group (`TableGrouping`) | |---|---|---| | Identitaet | First-class Entity (UUID, audit, RBAC) | JSON-Knoten in einem User-spezifischen Tree | | Persistenz | DB-Zeile | JSON-Blob in `TableGrouping.rootGroups` | | Sichtbarkeit | Per `scope` (personal/featureInstance/mandate/global) | per-User (`userId` ist Partition-Key) | | RBAC | Per-Item Scope, Owner-only-Mutationen | keine; nur enthaltene Items haben Scope | | Move / Delete | Beeinflusst alle berechtigten User | Lokal pro User | | Use-Case | Files/Folders (mandate-shared) | Persoenliche Sortier-Sicht auf flachen Listen (Workflow-Runs, Trustee-Positionen, ...) | `FormGeneratorTable.groupingConfig` bleibt unveraendert. `FormGeneratorTree` und `groupingConfig` sind komplementaer; beide Sichten duerfen nebeneinander auf denselben Files leben (siehe `FilesPage` View-Toggle). ### Use-Case-Zuordnung | Use-Case | Komponente | |---|---| | Files in Folders (mandate-shared) | `FormGeneratorTree` + `FolderFileProvider` | | External-Source-Browse (kDrive, SharePoint, ...) | `FormGeneratorTree` + `ConnectorBrowseProvider` | | Feature-Daten in UDB (Mandate -> FeatureConnection -> Group/Table -> Record) | `FormGeneratorTree` + `FeatureDataProvider` | | Chat-Historie nach Feature gruppiert | `FormGeneratorTree` + `ChatProvider` | | AccessRules-Editor | `FormGeneratorTree` + `RbacRuleProvider` | | FlowEditor DataPicker (Upstream-Output-Pfade) | `FormGeneratorTree` + `DataRefProvider` | | InstanceHierarchyView (RBAC-Audit) | `FormGeneratorTree` + `InstanceHierarchyProvider` | | Workflow-Runs nach Mandant + Status filtern | `FormGeneratorTable` + `groupingConfig` (flach, per-User) | | Sidebar (`MandateNavigation`, `TreeNavigation`) | bleibt eigene Komponente -- Routing-Affordance, kein CRUD-Tree | Faustregel: Hat das Item eine eigene DB-Identitaet jenseits des aktuellen Users? Ja -> `FormGeneratorTree`. Nein -> `FormGeneratorTable.groupingConfig`. ### RBAC- und Sichtbarkeits-Modell (verbindlich) Die Recovery folgt dem fuer Files **bereits implementierten** RBAC-Modell (siehe `interfaceRbac.buildFilesScopeWhereClause`, `interfaceDbManagement.updateFile / deleteFile / deleteFilesBatch`). Folders bekommen 1:1 dieselbe Mechanik: 1. **Default-Sichtbarkeit `personal`.** Jedes neu angelegte Folder/File ist nur fuer `sysCreatedBy = currentUser` sichtbar. 2. **Sharing nur ueber `scope`.** Andere User sehen ein Objekt, sobald der Eigentuemer den Scope explizit erhoeht (`featureInstance` / `mandate` / `global`; `global` setzt `ALL`-Permission voraus, in der Praxis SysAdmin). 3. **Mutation = Owner-only** (oder `ALL`-Permission). Rename / Move / Delete / Scope-Cycle / Neutralize-Toggle sind fuer Nicht-Eigentuemer 403; die UI blendet die Affordance aus. 4. **Zwei getrennte Trees in jeder Files-Sicht:** - **„Eigene“** -- `sysCreatedBy = currentUser`. Volles CRUD; das Scope-Icon zeigt Sharing-Status an. - **„Geteilt mit mir“** -- `sysCreatedBy != currentUser`, ueber `scope` sichtbar. Read-only; Scope-/Neutralize nur als Indikator-Icon. DnD aus diesem Tree ist rein lesend (z.B. Drop in Chat-Input); Drop in den eigenen Tree erzeugt **keinen** Move (kein Ownership-Wechsel). 5. **Render-Regel.** Files erscheinen unter ihrem Folder, wenn der Folder fuer den Current-User sichtbar ist. Fehlt der Folder-Kontext (`FileItem.scope` geteilt, `FileFolder.scope` nicht), wird die Datei im Root des passenden Trees mit `contextOrphan`-Hint („kein Folder-Kontext geteilt“) gerendert. Aendert der Owner spaeter den Folder-Scope, wandert die Datei automatisch unter den Folder. 6. **Cascade-Delete nur Owner.** Multidelete eines (Sub-)Trees laeuft genau dann, wenn alle Items dem Current-User gehoeren. Gemischte Selektionen (Eigene + Geteilte) blockieren die Bulk-Aktion mit Hinweis. 7. **Folder-Scope-Cascade auf Files = opt-in.** Beim Setzen eines Folder- Scopes wird der Owner per Dialog gefragt, ob die enthaltenen Files den Scope erben. Default: nein (Folders und Files behalten unabhaengige Scopes, konsistent mit der heutigen Files-Logik). 8. **Folder-Neutralize-Vererbung beim Drop = nein.** Beim Drop eines Files in einen Folder mit `neutralize=true` wird der Datei-Wert nicht automatisch geaendert; ein Indikator zeigt den Mismatch. > **Datentabellen-Sicht (`FormGeneratorTable.groupingConfig` auf Files):** > Status quo. Die Tabelle zeigt weiterhin alle sichtbaren Files (eigene + > geteilte) flach; nur die Tree-Sicht trennt Eigene/Geteilt. Falls spaeter > gewuenscht, kann ein `ownerFilter` ergaenzt werden -- nicht in diesem Plan. --- ## Entscheidungen (konsolidiert) | # | Datum | Bereich | Entscheidung | Begruendung | |---|-------|--------|-------------|------------| | D1 | 2026-05-01 | Komponenten | UDB hat fix drei Tabs (Files, Sources, Chats); alle drei rendern `FormGeneratorTree` | Konsistente UDB-Struktur | | D2 | 2026-05-01 | Komponenten | FormGenerator-Familie um `Tree` erweitern; nicht `FolderTree` v2 | Konsolidiert ~8 parallele Tree-Implementierungen | | D3 | 2026-05-01 | Komponenten | `FormGeneratorTable.groupingConfig` bleibt unveraendert | Gilt fuer flache Daten (Workflow-Runs, Trustee-Positionen) | | D4 | 2026-05-01 | Komponenten | Berichte = `FormGeneratorReport`; **kein** `FormGeneratorChart` (Namens-Klarstellung) | Bestehende Familienkomponente | | D5 | 2026-05-01 | Komponenten | Sidebar (`MandateNavigation` / `TreeNavigation`) bleibt eigene Komponente | Routing-Affordance, kein CRUD-Tree | | D6 | 2026-05-01 | Persistenz | Recovery via Modell A: `FileFolder` + `FileItem.folderId` reaktivieren (Pydantic, Routen, FE) | Mandanten-geteilte Ordnerstruktur; DB-Schema unveraendert | | D7 | 2026-05-01 | Persistenz | Keine Schema-Aenderung in dev/int/prod; `migrate_folders_to_groups.py` wird archiviert (audit trail) | Nur API/FE-Wiederverdrahtung | | D8 | 2026-05-01 | Persistenz | Soft-Delete wird **nicht** eingefuehrt; Audit-Eintrag pro Mutation reicht | Bestehende Pattern-Konsistenz | | D9 | 2026-05-01 | Persistenz | Owner-Wechsel von Folders/Files ist nicht im Scope (`sysCreatedBy` immutable) | Bestehender Kontrakt | | D10 | 2026-05-01 | RBAC | Default-Scope `personal` fuer neue Folders/Files; Sharing nur via Owner-Scope-Patch | Konsistent zu Files-RBAC heute | | D11 | 2026-05-01 | RBAC | Geteilte Folders/Files sind fuer Nicht-Owner read-only (Backend 403, UI keine Affordance) | Owner-only-Kontrakt analog `updateFile` / `deleteFile` | | D12 | 2026-05-01 | RBAC | Scope-Auflader: `featureInstance`-Sichtbarkeit gilt nur, wenn der User Zugriff auf die Feature-Instanz hat (bestehende `buildFilesScopeWhereClause`-Logik unveraendert) | Konsistenz zu heutigem Files-Pfad | | D13 | 2026-05-01 | RBAC | `scope=global` ist im Eigenen-Tree des Owners (i.d.R. SysAdmin) sichtbar mit eigenem Indikator; bei allen anderen Usern im „Geteilt mit mir“-Tree | Symmetrische Owner-Sicht | | D14 | 2026-05-01 | UI-Modell | Zwei getrennte Trees pro Files-Sicht: „Eigene“ (CRUD) + „Geteilt mit mir“ (read-only) | Saubere Datenstruktur, keine Vermischung | | D15 | 2026-05-01 | UI-Modell | Render-Regel: File unter Folder, falls Folder sichtbar; sonst Root mit `contextOrphan`-Hint | Vermeidet Phantom-Hierarchien | | D16 | 2026-05-01 | UI-Modell | „Geteilt mit mir“-Tree sortiert primaer nach Owner-Name, dann Folder-Hierarchie | Quelle des Sharings ist die wichtigste Information | | D17 | 2026-05-01 | UI-Modell | „Geteilt mit mir“-Sektion kollabiert sich, wenn leer | UI-Hygiene | | D18 | 2026-05-01 | UI-Modell | `FormGeneratorTable.groupingConfig`-Sicht auf Files bleibt flach (Eigene + Geteilte gemischt); kein zusaetzlicher Owner-Filter in diesem Plan | Status quo der Tabelle | | D19 | 2026-05-01 | Sharing | Folder-Scope-Cascade auf enthaltene Files = opt-in (Dialog beim Patch) | Vermeidet versehentliche Massen-Sharings | | D20 | 2026-05-01 | Sharing | Folder-Neutralize-Vererbung beim File-Drop = nein (analog Scope; Indikator zeigt Mismatch) | Owner-Entscheidung pro Item | | D21 | 2026-05-01 | Sharing | Drag aus „Geteilt mit mir“ in eigenen Tree = gesperrt (kein Move, keine implizite Kopie) | Ownership-Wechsel ausserhalb Scope; Kopier-Workflow ggf. spaeterer Plan | | D22 | 2026-05-01 | Stage 3 | `SourcesTab` und `ChatsTab` bekommen je **einen** Tree (kein Eigene/Geteilt-Split) | Connections und Chats sind heute personal-aequivalent; kein Sharing-Modell | --- ## Ziel und Nicht-Ziele - **Ziel 1.** `FormGeneratorTree` als Komponente in `ui-nyla/src/components/FormGenerator/FormGeneratorTree/` mit Provider-Interface, Built-in-Features (Multiselect, Cascade, DnD, Inline-Edit, Scope-/Neutralize-Indikatoren) und Backend-Attribut-Resolution. - **Ziel 2.** `FolderFileProvider` als erste Implementierung; recovered die in `7c05cb0` entfallenen Funktionen auf Basis Modell A. - **Ziel 3.** UDB-Tabs `FilesTab`, `SourcesTab`, `ChatsTab` rendern `FormGeneratorTree`. `FilesTab` zeigt zwei Tree-Sektionen (Eigene + Geteilt). - **Ziel 4.** `FilesPage.tsx` Split-View: links zwei `FormGeneratorTree`- Sektionen, rechts `FormGeneratorTable` mit View-Toggle „Tree-Sicht | Liste-mit-Gruppen-Sicht“. - **Nicht-Ziel.** `FormGeneratorTable.groupingConfig` rebauen oder entfernen. - **Nicht-Ziel.** `FormGeneratorReport` anfassen. - **Nicht-Ziel.** Sidebar (`MandateNavigation` / `TreeNavigation`) migrieren. - **Nicht-Ziel.** Owner-Wechsel oder Soft-Delete fuer Folders/Files einfuehren. --- ## Betroffene Module ### Frontend Nyla - **Neu:** `components/FormGenerator/FormGeneratorTree/` (`FormGeneratorTree.tsx`, `FormGeneratorTree.module.css`, `types.ts`, `__tests__/`) - **Neu:** `providers/FolderFileProvider.tsx` - **Recovery:** `components/UnifiedDataBar/FilesTab.tsx` (Tree statt Table-mit-Grouping; Eigene + Geteilt) - **Recovery:** `pages/basedata/FilesPage.tsx` (Split-View Tree+Table) - **Recovery:** `contexts/FileContext.tsx`, `hooks/useFiles.ts` (Folder-Operationen zurueck) - **Recovery:** `api/fileApi.ts` (`folderId`, Folder-API-Wrapper) - **Anpassen:** `pages/views/workspace/WorkspaceInput.tsx` -- `TreeItemDrop.type` auf `'file' | 'folder'` zuruecksetzen, neuer Standard-MIME `application/x-poweron-tree-items` - **Stage 3:** `components/UnifiedDataBar/SourcesTab.tsx` und `ChatsTab.tsx` auf `FormGeneratorTree` (je ein Tree, kein Sharing-Split) - **Stage 4 (optional):** `SharepointBrowseTree`, `DataPicker`, `InstanceHierarchyView`, `AccessRulesEditor` ### Gateway - **Recovery:** `datamodels/datamodelFiles.py` -- neues `FileFolder` Pydantic- Modell + `FileItem.folderId: Optional[str]` mit `fk_target` auf `FileFolder.id` - **Recovery:** `interfaces/interfaceDbApp.py` / `interfaces/interfaceDbManagement.py` -- `getOwnFolderTree()`, `getSharedFolderTree()`, `createFolder` (Default-Scope `personal`), `renameFolder`, `moveFolder`, `deleteFolderCascade`, `patchFolderScope`, `patchFolderNeutralize`. Mutationen strikt owner-only (oder `ALL`). - **Recovery:** `routes/routeDataFiles.py` -- Folder-CRUD-Routen (`POST /folders`, `GET /folders/tree?owner=me|shared`, `PATCH /folders/{id}`, `POST /folders/{id}/move`, `DELETE /folders/{id}?cascade=bool`, `PATCH /folders/{id}/scope`, `PATCH /folders/{id}/neutralize`) - **Behalten:** `routeDataFiles.py` Group-Endpunkte (`/groups/{groupId}/scope`, `/neutralize`, `/download`, `delete_group`) fuer `FormGeneratorTable.groupingConfig`-Use-Cases - **Archivieren:** `migrations/migrate_folders_to_groups.py` -> `migrations/_archive/` mit kurzem README (audit trail) - **Unangetastet:** `serviceCenter/.../coreTools/_workspaceTools.py` und `mainServiceChat.py` (TableGrouping-Pfad bleibt). Bei Bedarf spaeter `_listFolders` / `_listFilesInFolder` als zusaetzliche Agent-Tools. ### DB Keine Schema-Aenderung. Optionaler Schema-Sanity-Check in Stage 0. --- ## Umsetzungs-Checkliste (Packets pro Modell) Jedes Packet ist ein abgeschlossener Auftrag fuer **eine** LLM-Session. Header pro Packet: **Modell** | **Eingang** (Voraussetzung) | **Lieferumfang** | **Checkliste** | **Abnahme**. Modelle: - **Composer 2 Fast** -- mechanische Wiederherstellung, Pattern-Wiederholung, Boilerplate, Doku gegen vorhandenen Code. - **Opus 4.6** -- Architektur, RBAC-Korrektheit, Greenfield-API, nicht-triviale Refactors. Bei Unsicherheit hochstufen. ### Reihenfolge / Block-Strategie ``` Stage 0 (optional) | v Stage 1.A (BE Boilerplate) ─┐ Stage 1.B (BE RBAC-Kern) ├─ Block 1 (Backend in beliebiger Reihenfolge, Stage 1.C (FE Greenfield) ─┘ FE kann parallel zu BE) | v Stage 2.A (FE Recovery-Boilerplate) | v Stage 2.B (FE UI-Komposition) | v Stage 2.C (manuelle Verifikation, User) <-- DONE (2026-05-03) | v Stage 3.A (Provider-Refactors) <-- DEFERRED Stage 3.B (Tab-Umbau + Cleanup) <-- DEFERRED | v Stage 4 (Tree-Assessment + Dead-Code-Cleanup) <-- DONE (2026-05-03) | v Stage 5 (Doku & Cleanup) <-- DONE (2026-05-03) ``` Empfehlung Merge-Bloecke: **Stage 0+1+2 = PR 1** (Recovery -- abgeschlossen). Stage 3 deferred. **Stage 4+5 = PR 2** (Cleanup + Doku -- abgeschlossen). --- ### Stage 0 -- Schema-Sanity-Check (optional, < 1 h) -- DONE | | | |---|---| | **Modell** | Composer 2 Fast | | **Eingang** | DB-Zugriff dev/int/prod | | **Lieferumfang** | Bestaetigte Schema-Praesenz, kein Code-Output | | **Abnahme** | Schriftliche Bestaetigung: `FileFolder` + `FileItem.folderId` in allen drei Umgebungen vorhanden; ggf. Zeilenanzahl | **Checkliste** - [x] `information_schema`-Query: `FileFolder`-Tabelle vorhanden in **dev** (`poweron_management`; int/prod separat durch Team) - [x] `information_schema`-Query: `FileItem.folderId`-Spalte vorhanden (**dev**; FK-Verifikation: Spalte + Ziel `FileFolder` lt. Schema) - [ ] Optional: `SELECT COUNT(*) FROM "FileFolder"` und Stichprobe pro Mandant auf Datenkonsistenz **Hinweis (dev):** `python -m scripts.stage0_filefolder_schema_check` im Ordner `platform-core`. > Falls eine Umgebung abweicht: Plan zurueckziehen, Restore-/Migration- > Subtask einplanen, **bevor** Stage 1 startet. --- ### Stage 1.A -- Backend Boilerplate (Modell A reaktivieren) -- DONE | | | |---|---| | **Modell** | Composer 2 Fast | | **Eingang** | Stage 0 OK; pre-`7c05cb0`-Stand von `datamodelFiles.py` und `routeDataFiles.py` per Git als Referenz | | **Lieferumfang** | Pydantic-Modelle, Routen-Skelett, Migrations-Skript archiviert | | **Abnahme** | App startet; `GET /folders/tree?owner=me` liefert leere Liste fuer leeren Mandanten ohne Fehler | **Checkliste** - [x] `datamodels/datamodelFiles.py`: `FileFolder` Pydantic-Modell (id, name, parentId, mandateId, featureInstanceId, scope, neutralize, sysCreatedAt/By) mit `frontend_options` analog `FileItem.scope`; `i18nModel("Ordner")` - [x] `datamodels/datamodelFiles.py`: `FileItem.folderId: Optional[str]` mit `fk_target` auf `FileFolder.id` - [x] `routes/routeDataFiles.py`: Routen-Skelett (`POST /folders`, `GET /folders/tree?owner=me|shared`, `PATCH /folders/{id}`, `POST /folders/{id}/move`, `DELETE /folders/{id}`, `PATCH /folders/{id}/scope`, `PATCH /folders/{id}/neutralize`) — GET stub leere Liste; Mutations 501 bis Stage 1.B - [x] `migrations/migrate_folders_to_groups.py` -> `migrations/_archive/migrate_folders_to_groups.py` + README mit Verweis auf diesen Plan --- ### Stage 1.B -- Backend RBAC-Kern + Audit-Tests -- DONE | | | |---|---| | **Modell** | **Opus 4.6** | | **Eingang** | Stage 1.A OK; `interfaceRbac.buildFilesScopeWhereClause` und `interfaceDbManagement.updateFile` als RBAC-Vorlage | | **Lieferumfang** | `interfaceDbManagement`/`interfaceDbApp` Folder-Methoden mit Owner-only-Guards; vollstaendige RBAC-Tests; Verifikation des bestehenden Files-Pfads | | **Abnahme** | `test_folderRbac.py` und `test_folder_crud.py` gruen; manuelle Stichprobe Zwei-User: Owner B kann auf Folder von User A keine Mutationsroute aufrufen (alle 403); `contextOrphan`-Render in der Tree-API korrekt | **Checkliste** - [x] `interfaces/interfaceDbApp.py` / `interfaceDbManagement.py`: `getOwnFolderTree()`, `getSharedFolderTree()` mit Scope-Where-Klausel analog `buildFilesScopeWhereClause` - [x] `createFolder` (Default-Scope `personal`) - [x] `renameFolder`, `moveFolder`, `deleteFolderCascade`, `patchFolderScope`, `patchFolderNeutralize` -- jede Methode mit explizitem `sysCreatedBy == currentUser`-Guard (oder `ALL`); sonst `PermissionError` -> 403 - [x] `patchFolderScope`: Cascade-auf-Files als optionaler Parameter (D19); ohne expliziten Opt-in keine File-Aenderung - [x] **Verifikation bestehender Files-RBAC** (kein Code-Aenderung, nur Tests): `updateFile` / `deleteFile` / `deleteFilesBatch` werfen fuer Nicht-Owner mit Scope-Sichtbarkeit weiterhin `PermissionError` - [x] `tests/unit/routes/test_folder_crud.py`: Create / Move / Cascade-Delete / Scope-Patch / Neutralize-Patch (Happy Paths + Cross-Tenant) - [x] `tests/unit/interfaces/test_folderRbac.py`: Zwei-User-Matrix, jede Mutationsroute fuer Nicht-Owner = 403; `getSharedFolderTree` respektiert Scope; `contextOrphan`-Fall (File geteilt, Folder nicht) --- ### Stage 1.C -- Frontend `FormGeneratorTree` Greenfield -- DONE | | | |---|---| | **Modell** | **Opus 4.6** | | **Eingang** | Architektur-Abschnitt (Provider-Interface) + RBAC-Modell aus diesem Plan; bestehende `FolderTree.tsx` als UX-Referenz; `FormGeneratorTable` als Bulk-Action-Pattern-Referenz | | **Lieferumfang** | Generische `FormGeneratorTree`-Komponente + `FolderFileProvider` + Vitest+RTL-Tests; **kann parallel zu Stage 1.A/1.B laufen**, da Provider gegen Mocks getestet wird | | **Abnahme** | Vitest gruen inkl. Edge-Cases (mixed selection, partial-selected, cascade, contextOrphan); Storybook-Story (oder Smoke-Page) zeigt beide Modi `own`/`shared` mit Mock-Provider | **Checkliste** - [x] Ordner `components/FormGenerator/FormGeneratorTree/` mit `FormGeneratorTree.tsx`, `FormGeneratorTree.module.css`, `types.ts`, `providers/FolderFileProvider.tsx`, `__tests__/` - [x] `TreeNodeProvider`-Interface (siehe Architektur-Abschnitt) in `types.ts` - [x] Built-in Features: - Expand/Collapse mit Tastatur-Navigation - Multiselect mit Shift/Ctrl/Cmd; getrennt zwischen `ownership='own'` und `'shared'`; gemischte Selektion blockiert Bulk-Mutationen - Cascading-Selection (Folder selektieren -> alle eigenen Children), Partial-Selected-Indikator - DnD `application/x-poweron-tree-items`; aus `shared` nur lesende Drops, kein Move - Inline-Rename (Doppelklick / F2) -- nur fuer `ownership='own'` - Scope/Neutralize interaktiv nur fuer Eigene; im geteilten Tree Indikator-Icons - `contextOrphan`-Render-Regel (Knoten am Tree-Root mit Hint-Icon) - Bulk-Action-Toolbar analog `FormGeneratorTable.batchActions`; im geteilten Tree maximal Lesen/Download - [x] `FolderFileProvider.tsx`: Referenzimplementierung gegen die in Stage 1.B definierten Folder-API-Routen (kann initial gegen Mock-Server laufen) - [x] Vitest+RTL-Tests: Lazy-Load, Multiselect-Cascade, Mixed-Selection- Block, DnD-Payload, Inline-Rename, Bulk-Delete, `contextOrphan`- Render -- alle gegen Provider-Mock --- ### Stage 2.A -- FE Recovery-Boilerplate -- DONE | | | |---|---| | **Modell** | Composer 2 Fast | | **Eingang** | Stage 1.B + 1.C OK; pre-`7c05cb0`-Stand von `FileContext.tsx`, `useFiles.ts`, `fileApi.ts` per Git | | **Lieferumfang** | Folder-Operationen reaktiviert in Context/Hook/API-Wrapper | | **Abnahme** | TypeScript clean; bestehende Files-Tests gruen; FilesPage rendert weiterhin (alter Code-Pfad noch aktiv) | **Checkliste** - [x] `contexts/FileContext.tsx`: `FolderInfo`-Typ + `handleCreateFolder`, `handleRenameFolder`, `handleDeleteFolderCascade`, `handleMoveFolder`, `handleMoveFiles`, `handleDownloadFolder`, `refreshFolders`, `treeFileNodes`, `expandedFolderIds` -- gegen die neuen Routen - [x] `hooks/useFiles.ts`: Folder-Operationen wieder einbauen - [x] `api/fileApi.ts`: `FileInfo.folderId`, `getFolderTree`, `createFolder`, `renameFolder`, `moveFolder`, `deleteFolderCascade`, `moveFiles` als API-Wrapper - [x] `pages/views/workspace/WorkspaceInput.tsx`: `TreeItemDrop.type` auf `'file' | 'folder'` zuruecksetzen --- ### Stage 2.B -- FE UI-Komposition (FilesTab + FilesPage) -- DONE | | | |---|---| | **Modell** | **Opus 4.6** | | **Eingang** | Stage 2.A OK | | **Lieferumfang** | `FilesTab` mit zwei Tree-Sektionen; `FilesPage` Split-View mit Toggle Tree/Liste-mit-Gruppen-Sicht | | **Abnahme** | Storybook/Smoke der UDB zeigt beide Sektionen, leerer Geteilt-Tree kollabiert; FilesPage-Toggle wechselt korrekt | **Checkliste** - [x] `components/UnifiedDataBar/FilesTab.tsx`: zwei `FormGeneratorTree`- Sektionen via `FolderFileProvider` - **Eigene** mit Toolbar („Neuer Ordner“, „Multidelete“, „Multimove“, „Download als ZIP“) - **Geteilt mit mir** read-only, kollabiert wenn leer, Owner-Name als oberste Sortier-Achse (D16/D17) - `compact={true}`; `onSendToChat` auf `'file' | 'folder'` - [x] `pages/basedata/FilesPage.tsx`: Split-View - links zwei `FormGeneratorTree`-Sektionen (Eigene / Geteilt) mit klar getrennten Headern - rechts `FormGeneratorTable` mit Toggle „Tree-Sicht | Liste-mit-Gruppen-Sicht“; `groupingConfig` bleibt als alternative Sicht - Selektion im geteilten Tree -> Tabellenzeilen read-only --- ### Stage 2.C -- Manuelle Verifikation (User) -- DONE | | | |---|---| | **Modell** | -- (User) | | **Eingang** | Stage 2.B deployed in dev; Test-Mandant mit zwei Usern verfuegbar | | **Lieferumfang** | Bestaetigung der Akzeptanzkriterien 1-8b + 10 | | **Abnahme** | Alle Punkte unten haken; sonst Defekt-Issue oeffnen, zustaendige Stage erneut | **Checkliste** - [x] Neuer Folder anlegen -> Default `personal`, nur eigener Tree zeigt ihn - [x] Scope/Neutralize: Eigene interaktiv, Geteilt nur Indikator *(Fix 2026-05-02: FolderFileProvider typeMap fuer Folder- vs File-Endpoints)* - [x] Multiselect Folder + Files (eigen) cascadiert; gemischt Eigene/Geteilt blockiert Bulk-Mutationen mit Hinweis - [x] Multidelete cascadiert nur bei reinem Owner-Set; Confirmation *(Fix 2026-05-02: TreeBatchAction.typeFilter, separate Ordner/Dateien-Buttons, window.confirm)* - [x] DnD: File von Folder A nach Folder B (eigen) funktioniert; Drag aus „Geteilt mit mir“ in eigenen Folder = kein Move - [x] DnD: File aus FilesTab in Workspace-Chat-Input dropped korrekt (Standard-MIME erkannt) -- aus beiden Trees lesend zulaessig - [ ] Cross-User-Sharing: User A setzt Folder-Scope auf `mandate` -> User B sieht den Folder im „Geteilt mit mir“ (read-only, 403 auf Mutationen via DevTools-Probe) *(nicht getestet -- erfordert zwei User im selben Mandant)* - [x] FilesPage-Toggle: umgebaut zu „Ordner-Sicht“ (Tabelle gefiltert nach selektiertem Ordner) und „Alle Dateien“ (ohne Folder-Filterung). Gruppierung entfernt. **Zusaetzliche Fixes aus Stage 2.C Verifikation (2026-05-02/03):** - FolderFileProvider: 405-Fix (falscher API-Endpoint `/api/files` -> `/api/files/list`) - FolderFileProvider: typeMap eingefuehrt -- alle Operationen (rename, delete, move, patchScope, patchNeutralize) verwenden korrekte Endpoints fuer Folder vs File - FormGeneratorTree: Refresh-Icon im Section-Header (`FaSyncAlt`) - FormGeneratorTree: Tree-Reload nach Batch-Actions und inline Delete - FormGeneratorTree: `window.confirm()` vor Delete (inline + batch) - FilesPage: Tree-Table-Sync (selectedFolderId filtert Tabelle, File-Klick highlighted Tabellenzeile) --- ### Stage 3.A -- Provider-Refactors (Sources + Feature-Daten) -- DEFERRED | | | |---|---| | **Status** | **DEFERRED (2026-05-03)** | | **Modell** | **Opus 4.6** | | **Eingang** | Stage 1.C OK (FormGeneratorTree stabil) | | **Lieferumfang** | `ConnectorBrowseProvider` und `FeatureDataProvider` mit Aequivalenz-Garantie zur bisherigen Logik | | **Abnahme** | Vitest+RTL-Tests pro Provider gruen; Smoke-Test: alle bisherigen Knoten-Ebenen sind reproduzierbar | **Grund fuer Deferral:** `SourcesTab` und `ChatsTab` enthalten hochgradig domaen-spezifische Tree-Logik (Multi-Level lazy-load ueber Connections, Services, Feature-Daten, Mandate-Gruppierungen, Chat-Filterung), die sich nicht sinnvoll auf die generische `TreeNodeProvider`-Abstraktion abbilden laesst, ohne die Provider-Interfaces erheblich zu erweitern. Der Aufwand uebersteigt den Nutzen gegenueber dem bestehenden, funktionierenden Code. Kann bei Bedarf spaeter als eigenes Projekt aufgegriffen werden. ~~**Checkliste**~~ - ~~`providers/ConnectorBrowseProvider.tsx`~~ - ~~`providers/FeatureDataProvider.tsx`~~ - ~~Pro Provider Vitest+RTL-Tests~~ --- ### Stage 3.B -- Tab-Umbau + Cleanup (Feature-Flag) -- DEFERRED | | | |---|---| | **Status** | **DEFERRED (2026-05-03)** | | **Modell** | Composer 2 Fast | | **Eingang** | Stage 3.A OK | | **Lieferumfang** | `SourcesTab.tsx` und `ChatsTab.tsx` auf `FormGeneratorTree`; alter Custom-Tree-Code entfernt; Feature-Flag fuer Rollback | | **Abnahme** | Mit Flag aus = Status quo; Flag ein = neue Tree-UI; manuelle Verifikation der UDB-Tabs OK | **Grund fuer Deferral:** Abhaengig von Stage 3.A (s.o.). ~~**Checkliste**~~ - ~~`providers/ChatProvider.tsx`~~ - ~~`SourcesTab.tsx` Umbau~~ - ~~`ChatsTab.tsx` Umbau~~ - ~~Feature-Flag~~ - ~~Manuelle Verifikation~~ --- ### Stage 4 -- Restliche Tree-Migrationen (Assessment + Cleanup) -- DONE | | | |---|---| | **Status** | **DONE (2026-05-03)** | | **Modell** | Composer 2 Fast | | **Ergebnis** | Assessment aller Kandidaten; Dead-Code geloescht; keine weiteren Migrationen sinnvoll | **Assessment-Ergebnis** - [x] `FolderTree.tsx` + `SharepointBrowseTree.tsx` + gesamter `components/FolderTree/`-Ordner: **GELOESCHT** (Dead Code, keine externen Imports). ESLint-Deprecation-Regel entfernt. - [x] `DataPicker.tsx`: **SKIP** -- Schema-basierter Automation2-Picker (read-only, in-memory, kein Entity-Tree; TreeNodeProvider-Abstraktion passt nicht) - [x] `InstanceHierarchyView.tsx`: **SKIP** -- Read-only Admin-Visualisierung (synchrone Props, kein CRUD/DnD/Multiselect; 300 Zeilen, stabil) - [x] `AccessRulesEditor.tsx`: **SKIP** -- Kein Tree, sondern tabellarischer RBAC-Rule-Editor mit Checkbox-Matrix (774 Zeilen; komplett anderes Paradigma) - [x] `TreeNavigation.tsx`: **SKIP** -- Sidebar-Navigation mit React Router (NavLink, aktiver Pfad); UI-Navigation, nicht Daten-Tree - [x] `redmineTreeLogic.ts`: **SKIP** -- Reiner BFS-Algorithmus fuer Redmine-Tickets; kein UI-Komponente --- ### Stage 5 -- Doku & Cleanup -- DONE | | | |---|---| | **Modell** | Composer 2 Fast | | **Eingang** | Stage 2 mindestens gemerged; spaetestens nach Stage 4 | | **Lieferumfang** | Wiki-Update der kanonischen Stellen + neuer Tree-Topic | | **Abnahme** | `lastReviewed` aktualisiert; `wiki/c-work/_CHANGELOG.md` pro Stage eine Zeile | **Checkliste** - [x] `wiki/b-reference/ui-nyla/architecture.md`: Komponenten-Tabelle um `FormGeneratorTree` ergaenzt, `FolderTree`-Eintrag ersetzt, `components/`-Ordner-Beschreibung aktualisiert - [x] `wiki/b-reference/ui-nyla/formgenerator.md`: Abschnitt `FormGeneratorTree` (Provider-Interface, Built-in Features, Vergleichstabelle Tree vs. Table-with-grouping, Props, Verwendung) - [x] `wiki/TOPICS.md`: Neuer Eintrag FormGenerator (Table, Form, Tree, Report) - [x] `wiki/c-work/_CHANGELOG.md`: Stage 4+5 Eintrag - [x] Plan-Dokument verschoben nach `4-done/` --- ### Eskalations-Regel zwischen Modellen - Composer-Packet stoesst auf Entscheidung, die nicht in **D1-D22** steht -> stoppen, Plan ergaenzen (Opus), dann zurueck. - Composer-Test schlaegt fehl und Fix ist nicht mechanisch -> Diagnose-Loop in Opus, Fix-Patch zurueck zu Composer. - Pre-Commit-Hook-Modifikationen (lint/format) bleiben in Composer. --- ## Akzeptanzkriterien | # | Kriterium (Given-When-Then) | Prio | |---|---------------------------|------| | 1 | **Given** ein Mandant mit eigenen Folders + Files, **When** der User die Files-Page oeffnet, **Then** die linke Spalte zeigt zwei Sektionen („Eigene“, „Geteilt mit mir“) mit rekursiven Folder-Trees | must | | 2 | **Given** der User klickt im Eigenen-Tree auf „Neuer Ordner“, **When** er einen Namen bestaetigt, **Then** der Folder erscheint sofort, ist persistiert mit Default-Scope `personal` und nur fuer den Ersteller sichtbar | must | | 3 | **Given** ein eigener Folder mit 5 Files, **When** der User per Multiselect den Folder + 2 weitere Files anwaehlt und „Loeschen“ klickt, **Then** Confirmation `Wirklich 1 Folder (mit 5 Files) und 2 weitere Files loeschen?`; nach OK sind alle 8 Items weg | must | | 4 | **Given** ein eigenes File mit `scope=personal`, **When** der User auf das Scope-Icon klickt, **Then** das Icon cyclet `personal -> featureInstance -> mandate -> global -> personal`; jeder Click persistiert via PATCH | must | | 5 | **Given** ein eigener Folder, **When** der Owner den Scope auf `mandate` patcht, **Then** Dialog fragt „Files mit teilen?“ (Default: nein); bei Ja erben enthaltene Files den Scope, sonst bleibt deren Scope unveraendert | must | | 6 | **Given** der User draggt ein File aus dem FilesTab, **When** im Workspace-Chat-Input gedropped, **Then** Standard-MIME `application/x-poweron-tree-items` wird empfangen; aus beiden Trees lesend zulaessig | must | | 7 | **Given** UDB ist offen, **When** der User durch Files / Sources / Chats blaettert, **Then** alle drei Tabs zeigen `FormGeneratorTree` mit konsistentem Look-and-Feel | should | | 8 | **Given** zwei Mandanten-Mitglieder (User A, User B), **When** User A einen Folder anlegt (Default `personal`), **Then** User B sieht den Folder **nicht**. Erst nach `mandate`-Scope-Patch erscheint er bei User B im „Geteilt mit mir“ (read-only, Indikator-Icons, keine Mutations-Affordance) | must | | 8a | **Given** User A teilt eine Datei (`scope=mandate`), aber den enthaltenden Folder nicht, **When** User B die Files-Page oeffnet, **Then** die Datei erscheint im Root des „Geteilt mit mir“-Trees mit `contextOrphan`-Hint; aendert User A den Folder-Scope, wandert die Datei automatisch unter den Folder | should | | 8b | **Given** User B steht im „Geteilt mit mir“ auf einem Folder von User A, **When** er Mutationsroute (rename / move / delete / scope-cycle / neutralize-toggle) aufruft, **Then** Backend antwortet 403 und UI hat keine Affordance | must | | 9 | **Given** `FormGeneratorTable.groupingConfig` aktiv auf `AutomationsDashboardPage`, **When** der User die Page laedt, **Then** Workflow-Runs sind weiterhin nach Mandant + Status gruppiert | must | | 10 | **Given** `FilesPage` View-Toggle steht auf „Liste-mit-Gruppen-Sicht“, **When** der User auf „Tree-Sicht“ und zurueck wechselt, **Then** beide Sichten zeigen dieselben Files; `TableGrouping` und `FileFolder` sind orthogonal sichtbar | should | | 11 | **Given** Drag-Source aus `SourcesTab`, **When** auf eine Workflow-Node gedropped, **Then** Empfaenger sieht denselben `application/x-poweron-tree-items`-MIME wie aus FilesTab | should | --- ## Testplan | ID | AC | Art | Automatisiert | Repo-Pfad | Status | |----|----|-----|--------------|-----------|--------| | T1 | 1, 2 | api | ja | `platform-core/tests/unit/routes/test_folder_crud.py` | pending | | T2 | 3 | api | ja | `platform-core/tests/unit/routes/test_folder_cascade_delete.py` | pending | | T3 | 4, 5 | api | ja | `platform-core/tests/unit/routes/test_folder_scope_neutralize.py` (inkl. Cascade-Opt-in) | pending | | T4 | 8, 8a, 8b | api | ja | `platform-core/tests/unit/interfaces/test_folderRbac.py` (zwei User; alle Mutationsrouten 403; Render-Regel inkl. `contextOrphan`) | pending | | T5 | 1-6 | ui | ja | `ui-nyla/src/components/FormGenerator/FormGeneratorTree/__tests__/FormGeneratorTree.test.tsx` | pending | | T6 | 6, 11 | ui | ja | `ui-nyla/src/components/FormGenerator/FormGeneratorTree/__tests__/dnd.test.tsx` | pending | | T7 | 1-7, 10 | manuell | nein | -- | pending | | T8 | 9 | manuell | nein | -- | pending | | T9 | 7 | manuell | nein | -- | pending | --- ## Risiken | Risiko | Wahrscheinlichkeit | Impact | Mitigation | |--------|-------------------|--------|------------| | `FileFolder` / `folderId` in einer Umgebung doch entfernt (Abweichung vom Repo-Stand) | sehr niedrig | hoch | Stage 0 Spot-Check; ggf. Restore aus Backup oder Ableitung aus `TableGrouping.meta.migratedFromFolderId` (One-Shot) | | Read-only-Kontrakt fuer Folders nicht durchgaengig (UI versteckt, Backend offen) | mittel | hoch | Pro Mutationsroute expliziter `sysCreatedBy == currentUser`-Guard; Zwei-User-Tests pro Route in Stage 1 | | `contextOrphan`-Render leakt Pfad-/Owner-Information mehr als beabsichtigt | niedrig | mittel | Im geteilten Tree nur Owner-Name + File-Name; keine Folder-Pfade aus dem fremden Tree | | `_workspaceTools.py` (writeFile -> Group-Auto-Add) bricht nach Recovery | mittel | mittel | Code unangetastet; TableGrouping-Pfad bleibt; User kann Files spaeter manuell in Folder verschieben | | TableGrouping-Eintrag und FileFolder-Eintrag widersprechen sich (selber File in Group X UND Folder Y) | hoch | niedrig | By design orthogonal; UI zeigt beide Sichten ueber Toggle; keine Konsistenz-Erzwingung | | Stage 3 enthuellt subtile Abhaengigkeiten in SourcesTab/ChatsTab | hoch | mittel | **Eingetreten:** Stage 3 deferred -- domaen-spezifische Logik passt nicht auf generische TreeNodeProvider-Abstraktion | | Cascade-Selection-Logik im FormGeneratorTree subtil falsch (z.B. partial-selected) | mittel | niedrig | Umfangreiche Vitest+RTL-Tests in Stage 1 mit Provider-Mock | --- ## Links - PR: tba (Stage 0+1+2 -- bereit), Stage 3 deferred, Stage 4+5 abgeschlossen - Issue: -- - Vorgaenger-Commits: `c8e9304`, `e7a79a3`, `7c05cb0` - Migrations-Skript (archiviert): `platform-core/modules/migrations/_archive/migrate_folders_to_groups.py` - Heutige `FolderTree`-Komponente (vollstaendig, nicht mehr eingebunden): `ui-nyla/src/components/FolderTree/FolderTree.tsx` --- ## Abschluss - [x] `b-reference/ui-nyla/architecture.md` aktualisiert (Komponenten-Tabelle, UDB-Beschreibung) - [x] `b-reference/ui-nyla/formgenerator.md` aktualisiert (Abschnitt FormGeneratorTree) - [x] `TOPICS.md` aktualisiert - [x] Dokument verschoben nach `4-done/`