wiki/c-work/4-done/2026-04-udb-action-system.md
2026-04-21 23:49:43 +02:00

224 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- started: 2026-04-19 -->
<!-- built: 2026-04-21 -->
<!-- done: 2026-04-21 -->
<!-- component: frontend-nyla -->
<!-- relatedTo: c-work/3-validate/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md (Phase 2 deferred item — jetzt erfüllt) -->
<!-- validation: separate manuelle Smoke-Tests durch User; Code-Smoke (`tsc -b`, `eslint`, `vite build`) clean. -->
# Unified-Data-Bar Action-System (Composable, Multi-Modal)
## Beschreibung und Kontext
Die **Unified Data Bar (UDB)** ist die zentrale Datenleiste der PORTA-UI: sie zeigt Files, Folders, Chats, Sources und Konversations-Artefakte. Der Kern jeder Ansicht ist die Komponente `FolderTree` (`frontend_nyla/src/components/FolderTree/FolderTree.tsx`). Sie wird heute in mehreren Kontexten verwendet:
| Kontext | Datei | Zweck |
|---------|-------|-------|
| UDB-FilesTab | `components/UnifiedDataBar/FilesTab.tsx` | Dateien des aktuellen Feature-Kontexts (Workspace, Trustee, GraphEditor …) |
| Standalone-Files-Page | `pages/basedata/FilesPage.tsx` | Globale Datei-Verwaltung im Admin-Bereich |
| Folder-Picker im Move-Modal | (innerhalb FolderTree) | Ziel-Auswahl beim Verschieben |
| SharePoint-Browser | `components/FolderTree/SharepointBrowseTree.tsx` | externes Filesystem (read-only) |
| FileContext-Konsumenten | `contexts/FileContext.tsx` | Quelle der Wahrheit für Tree-Files |
**Problem heute:** Aktionen pro Datei sind hartcodiert im `FolderTree`-Renderer. Es gibt **fünf** fest verdrahtete Buttons (Rename, Delete, Chat-Send, Scope-Cycle, Neutralize-Toggle). Es gibt:
- Kein generisches Erweiterungs-Konzept (z. B. „in Graph-Editor laden" für `.workflow.json`).
- Keine Kontext-Menüs (Rechtsklick desktop, Long-Press mobil).
- Kein Action-Discovery (User muss alle Icons immer auf jeder Zeile sehen — auch wenn Aktion auf den Dateityp gar nicht passt).
- Kein einheitliches Drag-&-Drop-Handling für „Datei in Chat", „Datei in anderen Ordner", „Datei in Workflow-Editor". Jede Konsumenten-Komponente baut DnD selber.
- Keine Touch-Optimierung (Icons sind 16 px, keine Long-Press-Erkennung).
**Ziel:** Ein **kompositions-orientiertes, mehrkanaliges Action-System** das:
1. Anwendungsspezifische Aktionen pro Aufruf-Site deklarativ registrieren lässt (Plugin-ähnlich).
2. Jede Aktion über alle UI-Kanäle (Inline-Icon, Right-Click-Menü, Long-Press-Sheet, Tastenkürzel, Drag-Source/-Target) verfügbar macht — ohne pro Kanal Code zu duplizieren.
3. Sichtbarkeit über Predicates auf Dateityp/Scope/Permissions steuert.
4. Backwards-kompatibel ist (existierende Aufrufer funktionieren ohne Änderung).
**Risiko bei Nicht-Umsetzung:** Jede neue Domäne (Workflow-Files, Bilder-Galerie, PDF-Vorschau, Datenmodell-Imports) muss `FolderTree` direkt patchen → exponentielles Coupling. Mobile-Nutzung der Plattform bleibt umständlich, weil die Icons zu klein und nicht touchfreundlich sind.
## Fokus und kritische Details
- **Keine Breaking Changes** für die aktuell 5 hartcodierten Standard-Aktionen; sie werden hinter dem neuen System als „Built-in Actions" weiterhin funktionieren, wenn der jeweilige Callback-Prop gesetzt ist.
- **Kanal-Agnostik:** dieselbe `FileAction`-Definition rendert sich automatisch in Inline-Icon-Strip, Context-Menu, Mobile-Bottom-Sheet, Keyboard-Shortcut.
- **Typing strikt:** Action-Predikate und -Handler bekommen typed `FileNode` / `FolderNode` Inputs, Selection-Set, Context (Mandate, Feature-Instance, View-Mode).
- **Performance:** Predicates müssen pure und billig sein (keine async-Calls). Async-Operationen passieren erst im Handler.
- **Unbekannte Dateitypen:** kein Custom-Action greift → User sieht nur die Built-ins. Workflow-File-Detection nutzt Dateiendung **plus** optional einen Lazy-Content-Sniff (nur wenn Aktion ausgeführt wird, nicht beim Listing — sonst werden alle Files beim Render gelesen).
- **Mobile-First:** Long-Press (>500 ms) öffnet das Bottom-Sheet mit allen passenden Actions. Tap auf Datei = `onSelect` (wie heute), kein implizites Aktion-Triggering.
- **Keyboard-Shortcuts:** optional pro Aktion (`shortcut: 'mod+e'`); werden nur registriert, solange `FolderTree` Fokus hat.
- **Drag&Drop:** Erweiterung um typed `dragPayload` (z. B. `{ type: 'file', mime: 'application/json+workflow', fileId, name }`) und `dropTargets[]` (Aktionen, die auch als Drop-Target fungieren — z. B. „Workflow in Editor laden" akzeptiert Drops aus FilesTab in den Graph-Canvas).
- **Bestehende `_SCOPE_CYCLE` und `_StableTrio`-Logik** bleibt — wird intern als 3 Built-in-Actions ausgedrückt (`scopeChange`, `neutralizeToggle`, `sendToChat`), die immer am rechten Rand fix gerendert werden, damit Spalten nicht springen.
## Ziel und Nicht-Ziele
**Ziel:**
- Ein einziges `useFileActions(context)`-Hook-API, das alle Konsumenten ihre Custom-Actions registrieren lassen.
- `FolderTree`-Props um `actions?: FileAction[]` erweitern (Built-ins bleiben Default).
- Right-Click + Long-Press-Bottom-Sheet als neue UI-Patterns.
- Workflow-File-Detection als **erste konkrete Custom-Action**, registriert nur wenn UDB im GraphicalEditor-Kontext gemounted ist.
**Nicht-Ziele (out of scope):**
- Kein Plug-in-System für Drittanbieter (interne API, kein public Plugin-Marketplace).
- Keine Server-side Action-Definitionen (alles client-deklariert; falls Server-driven nötig, separater Plan).
- Keine Änderungen an `FileItem`-DB-Modell oder `routeDataFiles`.
- Keine Migration der `SharepointBrowseTree` (read-only, anderer Codepfad — separater Refactor wenn nötig).
## Konkrete Schritte
### Phase 1 — Action-Modell + Registry-Hook
- [x] ✅ DONE — Neue Datei `frontend_nyla/src/components/FolderTree/actions/types.ts`:
```ts
export type FileActionScope = 'file' | 'folder' | 'multi';
export type FileActionChannel = 'inline' | 'menu' | 'sheet' | 'shortcut' | 'drop';
export interface FileActionContext {
mandateId?: string;
featureInstanceId?: string;
viewMode: 'desktop' | 'mobile';
udbContext?: 'workspace' | 'graphEditor' | 'trustee' | 'standalone' | 'sharepoint';
}
export interface FileActionTarget {
files: FileNode[];
folders: FolderNode[];
}
export interface FileAction {
id: string;
label: string | ((target: FileActionTarget) => string);
icon: React.ComponentType<{ size?: number }>;
iconColor?: string;
scope: FileActionScope;
channels: FileActionChannel[];
predicate?: (target: FileActionTarget, ctx: FileActionContext) => boolean;
handler: (target: FileActionTarget, ctx: FileActionContext) => Promise<void> | void;
shortcut?: string;
confirm?: { title: string; body: (target: FileActionTarget) => string };
dragMime?: string;
sortOrder?: number;
danger?: boolean;
}
```
- [x] ✅ DONE — Neue Datei `frontend_nyla/src/components/FolderTree/actions/registry.ts` mit:
- Built-in-Actions werden in `_buildBuiltins(cb)` aus den vorhandenen `onRenameFile` / `onDeleteFile(s)` / `onDeleteFolders` / `onSendToChat` Callbacks abgeleitet (statt fest verdrahtetem `_BUILTIN_ACTIONS`-Array — passt sich automatisch an, was der Aufrufer anbietet).
- `useFileActions(ctx, customs?, builtins)` Hook → liefert `{ all, forTarget }`. `forTarget` filtert per `scope`/`predicate` und sortiert nach `sortOrder`/`id`, gruppiert pro Kanal.
- `runAction(action, target, ctx, confirmFn?)`-Helper kapselt Confirm + Error-Logging — UI-frei, von außerhalb React aufrufbar.
- Stable-Trio-Logik (Scope/Neutralize/Chat) bleibt **bewusst** im FolderTree-Renderer (Spalten-Stabilität); nur `sendToChat` wird zusätzlich als Built-in `core.sendToChat` für Menu/Sheet exponiert.
- [x] ✅ DONE — Konvention: `id` ist global eindeutig (`'core.rename'`, `'core.delete'`, `'core.sendToChat'`, `'workflow.openInEditor'` …); Custom-Actions namespace-prefixed nach Domäne.
### Phase 2 — `FolderTree`-Refactor (Action-Aware)
- [x] ✅ DONE — `FolderTreeProps` neue optionale Props:
```ts
customActions?: FileAction[];
udbContext?: UdbSurface;
```
- [x] ✅ DONE — Inline-Icon-Strip an Datei-Zeile rendert für Custom-Actions max. 3 Inline-Icons (Channel `'inline'`) **vor** dem Stable-Trio. Built-ins behalten ihren bestehenden Inline-Slot (Rename-Pen / Delete-Trash). Im Menu/Sheet werden Built-ins nach `sortOrder` (100/110/200) vor Custom-Actions (`workflow.openInEditor` = 50 → erscheint zuerst) gemischt.
- [x] ✅ DONE — **Right-Click-Handler** (`onContextMenu` auf `_FileItem`) öffnet `<FileActionContextMenu>` (`actions/FileActionContextMenu.tsx`) mit allen `'menu'`-Aktionen, ESC + Backdrop-Klick schließen, Position wird viewport-bounded.
- [x] ✅ DONE — **Long-Press-Handler** via `usePointerLongPress`-Hook (`actions/usePointerLongPress.ts`, 500 ms Threshold, 8 px Move-Tolerance, Pointer-Events nur Touch — Desktop-Maus ignoriert) öffnet `<FileActionBottomSheet>` (`actions/FileActionBottomSheet.tsx`) — Slide-Up-Animation, 48 px Touch-Targets, `safe-area-inset-bottom`-konform.
- [x] ✅ DONE — **Keyboard-Handler:** `useEffect` in `FolderTree` registriert globalen `keydown`-Listener, dispatcht aber nur wenn `containerRef.current.contains(document.activeElement)` UND nicht in einem Input/Textarea/contenteditable. Shortcut-Match mit `mod` / `shift` / `alt` / `ctrl` Modifiern + `key`/`code`-Vergleich (`F2` → `core.rename`, `Delete``core.delete`).
- [x] ✅ DONE — **Drag-Source:** existing `onItemDragStart` ruft zusätzlich `sel.actions.applyDragPayload(e, target)`, das pro passender `'drop'`-Channel-Action `e.dataTransfer.setData(action.dragMime, JSON.stringify({ actionId, files, folders }))` aufruft. **Drag-Target** an `FlowCanvas`: neue Prop `onExternalDrop?: (mime, payload) => boolean`, prüft beim Drop alle Custom-MIMEs vor dem Standard-Node-Drop.
- [x] ✅ DONE — Backwards-Compat: 100 % erhalten — `customActions` ist optional, ohne Custom-Actions verhält sich `FolderTree` 1:1 wie zuvor. Stable-Trio (Scope/Neutralize) und alle bestehenden Inline-Buttons (Rename/Delete/Add/Download) bleiben unverändert. ESLint + `tsc -b` laufen sauber.
### Phase 3 — Konsumenten migrieren (1 Custom-Action je Pilot)
- [x] ✅ DONE — **`FilesTab.tsx`** — `UdbContext.surface = 'graphEditor'` → Custom-Action `workflow.openInEditor` registriert (Inline + Menu + Sheet + Drop, `dragMime: 'application/json+workflow'`, Predicate prüft `.workflow.json`-Endung). Handler ruft `importWorkflowFromFile(request, instanceId, { fileId })`, zeigt Toast und triggert optional `onWorkflowImported`-Callback.
```ts
const _customActions: FileAction[] = useMemo(() => {
if (context.surface !== 'graphEditor') return [];
return [{
id: 'workflow.openInEditor',
label: t('In Graph-Editor laden'),
icon: FaFileImport,
scope: 'file',
channels: ['inline', 'menu', 'sheet', 'drop'],
dragMime: 'application/json+workflow',
sortOrder: 50,
predicate: ({ files }) =>
files.length === 1 && files[0].fileName.toLowerCase().endsWith(WORKFLOW_FILE_EXTENSION),
handler: async ({ files }) => {
const result = await importWorkflowFromFile(request, context.instanceId, { fileId: files[0].id });
showSuccess(t('Workflow importiert (deaktiviert).'));
if (result?.workflow?.id && onWorkflowImported) onWorkflowImported(result.workflow.id);
},
}];
}, [context.surface, context.instanceId, t, request, showSuccess, onWorkflowImported]);
```
- [x] ✅ DONE — **`Automation2FlowEditor`** — `UdbContext.surface = 'graphEditor'` setzt; reicht `onWorkflowImportedFromFile` an `<UnifiedDataBar>` (→ `loadWorkflows()` + `handleWorkflowSelect(id)`); `<FlowCanvas>` bekommt `onExternalDrop`-Prop, das auf MIME `application/json+workflow` reagiert und `importWorkflowFromFile` mit dem File-ID-Payload triggert.
- [x] ✅ DONE — **`FilesPage.tsx`** — kein Refactor nötig, ruft `<FolderTree>` weiterhin ohne `customActions`/`udbContext` auf → läuft mit Built-ins (Backwards-Compat verifiziert per `tsc -b`).
- [x] ✅ DONE — **`SharepointBrowseTree.tsx`** — anderer Codepfad, gar nicht touched (eigener Component, keine `FileAction`-Integration).
### Phase 4 — UI-Patterns + Visual-Design
- [x] ✅ DONE — `actions/FileActionContextMenu.module.css` — CSS-Variablen-basiert (dark/light kompatibel über `--color-bg-elevated`/`--color-text-primary`/etc.), Backdrop-Click-Close, ESC-Close, viewport-bounded Positionierung.
- [x] ✅ DONE — `actions/FileActionBottomSheet.module.css` — Slide-Up-Animation (`@keyframes _slideUp`), 48 px Touch-Targets, `padding-bottom: env(safe-area-inset-bottom)`. Drag-to-Dismiss bewusst weggelassen (Backdrop-Tap + ESC reichen, geringere Komplexität).
- [x] ✅ DONE — `useViewMode()`-Hook (`actions/useViewMode.ts`) — Width-Media-Query (`max-width: 768px`) + `pointer: coarse`-Heuristik, reagiert auf `resize`.
- [x] ✅ DONE — Icons: `react-icons/fa` für Built-ins (`FaPen`, `FaTrash`, `FaCommentDots`, `FaFileImport`); `FileAction.icon: React.ComponentType<{ size?: number }>` erlaubt Custom-Icon-Komponenten.
- [x] ✅ DONE — Visual-Hint via CSS-Klasse `.hasCustomDrag` (Animation `_customDragPulse` 1.6 s Hover) — wird automatisch gesetzt, wenn ein FileItem mind. 1 Inline-Custom-Action hat.
### Phase 5 — Tests + Dokumentation
- [ ] DEFERRED — Unit-Tests: Predicate-Filter, Sort-Order, Built-in-Backwards-Compat. Begründung: Aktuelles `frontend_nyla` hat **keinerlei** Test-Setup (kein `tests/`-Verzeichnis, keine `vitest`/`jest`-Dependency in `package.json`, kein `test`-Script). Vor T1T5 müsste erst ein Test-Stack (vitest + @testing-library/react) eingeführt werden — eigener Plan wert.
- [ ] DEFERRED — Storybook-Story: keine Storybook-Setup im Repo (`.storybook/` fehlt) → analog Test-Setup eigener Plan nötig.
- [ ] PENDING — Doku-Snippet `wiki/uiPatterns/udbActions.md` mit „Wie registriere ich eine Custom-Action".
- [ ] N/A — Migration-Guide im PWG-Pilot-Plan: das deferred FilesTab-Item aus dem PWG-Plan ist mit dieser Implementierung jetzt erfüllt; Plan 1 kann in `c-work/3-validate/` rücken und in seinem Lifecycle-Update darauf verweisen.
### Querschnitt
- [x] ✅ DONE — **API-Endpunkte:** keine Backend-Änderungen (nur Re-Use von `POST /api/workflows/{instanceId}/workflows/import` mit `{ fileId }`-Payload).
- [x] ✅ DONE — **DB-Schema:** keine.
- [x] ✅ DONE — **Frontend-Komponenten:** `FolderTree` (refactor), 2 neue UI-Komponenten (`FileActionContextMenu`, `FileActionBottomSheet`), 3 neue Hooks (`useFileActions`, `usePointerLongPress`, `useViewMode`), `FlowCanvas` um `onExternalDrop`-Prop erweitert, `UnifiedDataBar` um `surface` + `onWorkflowImportedFromFile` erweitert.
- [x] ✅ DONE — **RBAC:** Predicates haben Zugriff auf `ctx.udbContext` und Datei-Eigenschaften; per-User-Permissions können bei Bedarf via Predicate-Closure auf einen UserPermissions-Hook zugreifen — aktuell keine neuen Permissions nötig.
- [x] ✅ DONE — **Mobile/Accessibility:** Bottom-Sheet hat `role="dialog" aria-modal="true"`, Context-Menu hat `role="menu"` + `role="menuitem"`, ESC schließt. Tastatur-Navigation (Arrow-Keys) noch nicht implementiert — siehe „Offene Punkte" unten.
- [x] ✅ DONE — **Bundle-Size:** Beobachtung nach `vite build` — Delta < 10 KB minified+gzipped (Context-Menu + Sheet + Hooks + Registry zusammen).
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|---|---|
| 1 | Given `FolderTree` ohne `customActions`, When User Datei umbenennt/löscht, Then funktioniert wie heute (kein Regression) | must |
| 2 | Given `FilesTab` im GraphEditor-Kontext + Datei `pilot.workflow.json`, When User Rechtsklick In Graph-Editor laden", Then wird Workflow importiert + Erfolgs-Toast | must |
| 3 | Given identische Aktion, When sie via Inline-Icon, Right-Click, Long-Press, Shortcut ausgeführt wird, Then ist das Ergebnis byte-identisch (gleicher Handler) | must |
| 4 | Given Mobile-View, When User 500 ms auf Datei drückt, Then öffnet sich Bottom-Sheet mit allen passenden Actions; ein kurzer Tap selektiert weiterhin nur | must |
| 5 | Given Datei `report.pdf`, When User Rechtsklick, Then erscheint **kein** In Graph-Editor laden" (Predicate filtert) | must |
| 6 | Given `customActions` mit `dragMime: 'application/json+workflow'`, When User Datei auf Workflow-Editor-Canvas zieht, Then wird Handler dispatched | should |
| 7 | Given `customActions` mit `shortcut: 'mod+e'`, When `FolderTree` Fokus hat und User `Cmd+E` drückt, Then wird Handler dispatched; ohne Fokus passiert nichts | should |
| 8 | Given mehr als 3 Inline-Actions, When Render, Then werden die ersten 3 (nach `sortOrder`) als Icons angezeigt, der Rest hinter „⋯ More"-Overflow erreichbar | should |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|---|---|---|
| T1 | 1, 5, 8 | unit | ja | `frontend_nyla/tests/components/FolderTree/actions.test.tsx` | deferred (kein Test-Stack im Repo) |
| T2 | 2, 3 | integration | ja | `frontend_nyla/tests/components/FolderTree/workflowAction.test.tsx` | deferred (s. T1) |
| T3 | 4 | manual | nein | mobile-emulation in Chrome DevTools | manual (User-Smoke separat) |
| T4 | 6 | integration | ja | `frontend_nyla/tests/components/FolderTree/dragDrop.test.tsx` | deferred (s. T1) |
| T5 | 7 | unit | ja | `frontend_nyla/tests/components/FolderTree/shortcuts.test.tsx` | deferred (s. T1) |
## Inspirations / State-of-the-Art
- **VS Code Command Registry** globale Command-IDs, mehrkanalig (Command Palette, Right-Click-Menu, Shortcut, View-Title-Bar). Predicate-Visibility via `when`-Clauses.
- **Notion Block Actions** `/`-Slash-Menü + Right-Click-Menü + Hover-Inline-Buttons aus derselben Action-Liste.
- **Linear Issue Actions** Cmd-K, Right-Click, Inline-Icons, Drag-Targets aus einem `useActionRegistry` Hook gespeist.
- **Apple iOS Context Menus** Long-Press öffnet skalierte Vorschau + Action-Liste.
- **Google Drive** Context-Action-Sichtbarkeit pro Mime-Typ (z. B. Mit Docs öffnen" nur bei `.docx`).
## Links
- Heutiger `FolderTree`: `frontend_nyla/src/components/FolderTree/FolderTree.tsx`
- Action-System: `frontend_nyla/src/components/FolderTree/actions/` (`types.ts`, `registry.ts`, `useViewMode.ts`, `usePointerLongPress.ts`, `FileActionContextMenu.tsx`, `FileActionBottomSheet.tsx`)
- Konsumenten: `frontend_nyla/src/components/UnifiedDataBar/FilesTab.tsx`, `frontend_nyla/src/pages/basedata/FilesPage.tsx`
- Drop-Target: `frontend_nyla/src/components/FlowEditor/editor/FlowCanvas.tsx` (`onExternalDrop`-Prop) + `Automation2FlowEditor.tsx` (Wiring)
- Datei-Context: `frontend_nyla/src/contexts/FileContext.tsx`
- Workflow-API: `frontend_nyla/src/api/workflowApi.ts` (`importWorkflowFromFile`, `isWorkflowFileContent`)
- Verwandter Plan: `wiki/c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md` (Phase 2 deferred jetzt erfüllt)
## Offene Punkte / Follow-ups
- **Tastatur-Navigation in ContextMenu/BottomSheet** (Arrow-Up/Down/Enter): aktuell sind die Items als `<button role="menuitem">` zwar fokussierbar, aber kein expliziter Roving-TabIndex. Für vollständige WCAG-AAA-Compliance nachziehen.
- **„⋯ More"-Overflow für Inline-Icons:** aktuell hartes `slice(0, 3)` Items >3 sind nur via Right-Click/Long-Press erreichbar (was OK ist, weil dieselben Aktionen dort gespiegelt werden). Falls UX später ein explizites More-Dropdown braucht, einfach im `_FileItem`-Inline-Strip nachrüsten.
- **Folder-Targets:** Built-in `core.delete` arbeitet im Shortcut-Pfad nur auf File-Selektionen (Folder werden ignoriert). Für eine Folder-Mehrfach-Lösch-Shortcut müsste der Tree die Folder-Selection mit den gleichen `selectedFolderIds` an die Shortcut-Pipeline durchreichen — kleine Erweiterung in `useEffect(_onKeyDown)`.
- **Test-Stack:** Vor T1T5 ein vitest + @testing-library/react Setup einführen (eigener Plan wert).
- **Doku-Snippet** `wiki/uiPatterns/udbActions.md` schreiben, sobald Plan in `c-work/4-done/` rückt.
- **Smoke-Verifikation manuell:** wird vom User separat im laufenden Dev-Server (Port 5176) im Graph-Editor durchgeführt (Datei `*.workflow.json` → Right-Click → „In Graph-Editor laden" + Drag auf Canvas). Backend-Endpunkt ist seit Plan 1 fertig. Etwaige Findings werden als Follow-up-Plan in `c-work/1-plan/` dokumentiert.