20 KiB
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:
- Anwendungsspezifische Aktionen pro Aufruf-Site deklarativ registrieren lässt (Plugin-ähnlich).
- 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.
- Sichtbarkeit über Predicates auf Dateityp/Scope/Permissions steuert.
- 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/FolderNodeInputs, 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, solangeFolderTreeFokus hat. - Drag&Drop: Erweiterung um typed
dragPayload(z. B.{ type: 'file', mime: 'application/json+workflow', fileId, name }) unddropTargets[](Aktionen, die auch als Drop-Target fungieren — z. B. „Workflow in Editor laden" akzeptiert Drops aus FilesTab in den Graph-Canvas). - Bestehende
_SCOPE_CYCLEund_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 umactions?: 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 oderrouteDataFiles. - Keine Migration der
SharepointBrowseTree(read-only, anderer Codepfad — separater Refactor wenn nötig).
Konkrete Schritte
Phase 1 — Action-Modell + Registry-Hook
-
✅ DONE — Neue Datei
frontend_nyla/src/components/FolderTree/actions/types.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; } -
✅ DONE — Neue Datei
frontend_nyla/src/components/FolderTree/actions/registry.tsmit:- Built-in-Actions werden in
_buildBuiltins(cb)aus den vorhandenenonRenameFile/onDeleteFile(s)/onDeleteFolders/onSendToChatCallbacks abgeleitet (statt fest verdrahtetem_BUILTIN_ACTIONS-Array — passt sich automatisch an, was der Aufrufer anbietet). useFileActions(ctx, customs?, builtins)Hook → liefert{ all, forTarget }.forTargetfiltert perscope/predicateund sortiert nachsortOrder/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
sendToChatwird zusätzlich als Built-incore.sendToChatfür Menu/Sheet exponiert.
- Built-in-Actions werden in
-
✅ DONE — Konvention:
idist global eindeutig ('core.rename','core.delete','core.sendToChat','workflow.openInEditor'…); Custom-Actions namespace-prefixed nach Domäne.
Phase 2 — FolderTree-Refactor (Action-Aware)
- ✅ DONE —
FolderTreePropsneue optionale Props:customActions?: FileAction[]; udbContext?: UdbSurface; - ✅ 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 nachsortOrder(100/110/200) vor Custom-Actions (workflow.openInEditor= 50 → erscheint zuerst) gemischt. - ✅ DONE — Right-Click-Handler (
onContextMenuauf_FileItem) öffnet<FileActionContextMenu>(actions/FileActionContextMenu.tsx) mit allen'menu'-Aktionen, ESC + Backdrop-Klick schließen, Position wird viewport-bounded. - ✅ 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. - ✅ DONE — Keyboard-Handler:
useEffectinFolderTreeregistriert globalenkeydown-Listener, dispatcht aber nur wenncontainerRef.current.contains(document.activeElement)UND nicht in einem Input/Textarea/contenteditable. Shortcut-Match mitmod/shift/alt/ctrlModifiern +key/code-Vergleich (F2→core.rename,Delete→core.delete). - ✅ DONE — Drag-Source: existing
onItemDragStartruft zusätzlichsel.actions.applyDragPayload(e, target), das pro passender'drop'-Channel-Actione.dataTransfer.setData(action.dragMime, JSON.stringify({ actionId, files, folders }))aufruft. Drag-Target anFlowCanvas: neue ProponExternalDrop?: (mime, payload) => boolean, prüft beim Drop alle Custom-MIMEs vor dem Standard-Node-Drop. - ✅ DONE — Backwards-Compat: 100 % erhalten —
customActionsist optional, ohne Custom-Actions verhält sichFolderTree1:1 wie zuvor. Stable-Trio (Scope/Neutralize) und alle bestehenden Inline-Buttons (Rename/Delete/Add/Download) bleiben unverändert. ESLint +tsc -blaufen sauber.
Phase 3 — Konsumenten migrieren (1 Custom-Action je Pilot)
- ✅ DONE —
FilesTab.tsx—UdbContext.surface = 'graphEditor'→ Custom-Actionworkflow.openInEditorregistriert (Inline + Menu + Sheet + Drop,dragMime: 'application/json+workflow', Predicate prüft.workflow.json-Endung). Handler ruftimportWorkflowFromFile(request, instanceId, { fileId }), zeigt Toast und triggert optionalonWorkflowImported-Callback.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]); - ✅ DONE —
Automation2FlowEditor—UdbContext.surface = 'graphEditor'setzt; reichtonWorkflowImportedFromFilean<UnifiedDataBar>(→loadWorkflows()+handleWorkflowSelect(id));<FlowCanvas>bekommtonExternalDrop-Prop, das auf MIMEapplication/json+workflowreagiert undimportWorkflowFromFilemit dem File-ID-Payload triggert. - ✅ DONE —
FilesPage.tsx— kein Refactor nötig, ruft<FolderTree>weiterhin ohnecustomActions/udbContextauf → läuft mit Built-ins (Backwards-Compat verifiziert pertsc -b). - ✅ DONE —
SharepointBrowseTree.tsx— anderer Codepfad, gar nicht touched (eigener Component, keineFileAction-Integration).
Phase 4 — UI-Patterns + Visual-Design
- ✅ 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. - ✅ 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). - ✅ DONE —
useViewMode()-Hook (actions/useViewMode.ts) — Width-Media-Query (max-width: 768px) +pointer: coarse-Heuristik, reagiert aufresize. - ✅ DONE — Icons:
react-icons/fafür Built-ins (FaPen,FaTrash,FaCommentDots,FaFileImport);FileAction.icon: React.ComponentType<{ size?: number }>erlaubt Custom-Icon-Komponenten. - ✅ DONE — Visual-Hint via CSS-Klasse
.hasCustomDrag(Animation_customDragPulse1.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_nylahat keinerlei Test-Setup (keintests/-Verzeichnis, keinevitest/jest-Dependency inpackage.json, keintest-Script). Vor T1–T5 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.mdmit „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
- ✅ DONE — API-Endpunkte: keine Backend-Änderungen (nur Re-Use von
POST /api/workflows/{instanceId}/workflows/importmit{ fileId }-Payload). - ✅ DONE — DB-Schema: keine.
- ✅ DONE — Frontend-Komponenten:
FolderTree(refactor), 2 neue UI-Komponenten (FileActionContextMenu,FileActionBottomSheet), 3 neue Hooks (useFileActions,usePointerLongPress,useViewMode),FlowCanvasumonExternalDrop-Prop erweitert,UnifiedDataBarumsurface+onWorkflowImportedFromFileerweitert. - ✅ DONE — RBAC: Predicates haben Zugriff auf
ctx.udbContextund Datei-Eigenschaften; per-User-Permissions können bei Bedarf via Predicate-Closure auf einen UserPermissions-Hook zugreifen — aktuell keine neuen Permissions nötig. - ✅ DONE — Mobile/Accessibility: Bottom-Sheet hat
role="dialog" aria-modal="true", Context-Menu hatrole="menu"+role="menuitem", ESC schließt. Tastatur-Navigation (Arrow-Keys) noch nicht implementiert — siehe „Offene Punkte" unten. - ✅ 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
useActionRegistryHook 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.deletearbeitet 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 gleichenselectedFolderIdsan die Shortcut-Pipeline durchreichen — kleine Erweiterung inuseEffect(_onKeyDown). - Test-Stack: Vor T1–T5 ein vitest + @testing-library/react Setup einführen (eigener Plan wert).
- Doku-Snippet
wiki/uiPatterns/udbActions.mdschreiben, sobald Plan inc-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 inc-work/1-plan/dokumentiert.