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

20 KiB
Raw Blame History

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

  • 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.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.
  • 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)

  • DONE — FolderTreeProps neue 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 nach sortOrder (100/110/200) vor Custom-Actions (workflow.openInEditor = 50 → erscheint zuerst) gemischt.
  • 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.
  • 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: 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 (F2core.rename, Deletecore.delete).
  • 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.
  • 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)

  • DONE — FilesTab.tsxUdbContext.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.
    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 — Automation2FlowEditorUdbContext.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.
  • 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).
  • DONE — SharepointBrowseTree.tsx — anderer Codepfad, gar nicht touched (eigener Component, keine FileAction-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 auf resize.
  • DONE — Icons: react-icons/fa fü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 _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

  • DONE — API-Endpunkte: keine Backend-Änderungen (nur Re-Use von POST /api/workflows/{instanceId}/workflows/import mit { fileId }-Payload).
  • DONE — DB-Schema: keine.
  • 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.
  • 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.
  • 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.
  • 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).
  • 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.