# Typed Node Handover System — Spezifikation & Execution Plan **Ansatz: Greenfield.** Keine Rückwärtskompatibilität, keine Migration, kein Deprecation. --- ## 1 Problemstellung Der Graphical Editor hat keine formale Typisierung der Daten zwischen Nodes. Jede Node produziert ein freiformiges Dict; die Folge-Node extrahiert heuristisch. Kern-Symptome: - `actionNodeExecutor.py` (~860 Zeilen, >50 % handover-Heuristik pro Node-Paar) - `outputPreviewRegistry.ts` hartcodiert Beispiel-Schemas, die von Laufzeit-Outputs abweichen - Keine `data.transform`, `data.filter`, `data.aggregate`, `flow.merge` implementiert - Frontend baut pro Node-Typ eine eigene Config-Component (12+ Dateien) - Keine System-Variablen (Timestamp, Datum, User, etc.) Die Method/Action-Schicht hat bereits `WorkflowActionParameter` mit `FrontendType`, `_validateType`, `_validateParameters`. Dieses System wird für den Graphen übernommen und erweitert. --- ## 2 Architektur ### 2.1 Port-Schema (Kern-Datenmodell) Jede Node deklariert **typisierte Ports** für Ein- und Ausgänge sowie **Parameter** für die Konfiguration: ``` ┌──────────────────────────────────────────────────────────────────────┐ │ Node Definition (Beispiel: email.draftEmail) │ │ │ │ inputPorts: │ │ [0]: { accepts: [EmailDraft, AiResult] } │ │ │ │ outputPorts: │ │ [0]: { schema: ActionResult } │ │ │ │ parameters: │ │ connectionId: { type: str, frontendType: USER_CONNECTION } │ │ subject: { type: str, frontendType: TEXT } │ │ body: { type: str, frontendType: TEXTAREA } │ │ to: { type: List[str], frontendType: TEXT } │ └──────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 Port-Schema Pydantic-Modelle **Neue Datei:** `gateway/modules/features/graphicalEditor/portTypes.py` ```python class PortField(BaseModel): name: str type: str # str, int, bool, List[str], List[Document], Dict[str,Any] description: Dict[str, str] # {en, de, fr} required: bool = True class PortSchema(BaseModel): name: str # z.B. "EmailDraft", "AiResult", "Transit" fields: List[PortField] class InputPortDef(BaseModel): accepts: List[str] # Liste akzeptierter Schema-Namen class OutputPortDef(BaseModel): schema: str # Schema-Name aus dem Katalog dynamic: bool = False # True = Schema wird aus Parametern abgeleitet deriveFrom: Optional[str] = None # Parameter-Name für dynamische Ableitung ``` ### 2.3 Port-Typen-Katalog | Port-Typ | Felder | Produziert von | Konsumiert von | |----------|--------|----------------|----------------| | `DocumentList` | `documents: List[{name, data, mimeType, metadata}]` | sharepoint.readFile, sharepoint.downloadFile, ai.*, file.create, input.upload | ai.*, file.create, email.draftEmail, sharepoint.uploadFile | | `FileList` | `files: List[{url, name, path, size}]` | sharepoint.listFiles, sharepoint.findFile | sharepoint.readFile, sharepoint.downloadFile, flow.loop | | `EmailDraft` | `subject: str, body: str, to: List[str], cc?: List[str], attachments?: List[Document]` | ai.prompt (outputFormat=emailDraft) | email.draftEmail | | `EmailList` | `emails: List[{subject, from, to, body, date, attachments}]` | email.checkEmail, email.searchEmail | ai.prompt, flow.loop | | `TaskList` | `tasks: List[{id, name, status, url}]` | clickup.searchTasks, clickup.listTasks | flow.loop, clickup.updateTask | | `TaskResult` | `success: bool, taskId: str, task: {id, name, status}` | clickup.createTask, clickup.updateTask | flow.ifElse | | `FormPayload` | `payload: Dict[str, Any]` (dynamisch) | trigger.form, input.form | Alle (via Feld-Referenz) | | `AiResult` | `prompt: str, response: str, responseData?: Dict, context: str, documents: List[Document]` | ai.* | email.draftEmail, file.create, sharepoint.uploadFile | | `BoolResult` | `result: bool, reason?: str` | input.approval, input.confirmation | flow.ifElse | | `TextResult` | `text: str` | input.comment | ai.*, file.create | | `LoopItem` | `currentItem: Any, currentIndex: int, items: List[Any], count: int` | flow.loop (pro Iteration) | Loop-Body Nodes | | `AggregateResult` | `items: List[Any], count: int` | data.aggregate | Alle | | `MergeResult` | `inputs: Dict[int, Any], first: Any, merged: Dict` | flow.merge | Alle | Jeder Port-Typ bekommt implizit zwei Meta-Felder: **`_success: bool`** und **`_error: str?`**. Wenn ein Node fehlschlägt, liefert der Normalizer `{ _success: false, _error: "...", ...restliche Felder leer/default }`. ### 2.4 Transit-Typ Flow-Control-Nodes transformieren keine Daten — sie routen. Transit bedeutet: **Output-Schema = Input-Schema des Upstream-Produzenten**. Transit-Nodes produzieren ein Envelope: ```python { "_transit": True, "_meta": { "branch": 0, "conditionResult": True }, # Routing-Metadaten "data": , } ``` | Node | Output-Typ | _meta-Felder | |------|-----------|-------------| | `flow.ifElse` | Transit | `branch: int`, `conditionResult: bool` | | `flow.switch` | Transit | `match: int`, `value: str` | | `data.filter` | Transit | `originalCount: int`, `filteredCount: int` | **Transit-Auflösung** (rekursiv): DataPicker und Engine folgen `_transit`-Kette bis zum echten Produzenten. Multi-Output-Ports (ifElse hat 2): Transit-Auflösung berücksichtigt `sourceOutput`-Index, beide Ports zeigen dasselbe Upstream-Schema. `flow.loop` ist **kein** Transit — er transformiert eine Liste in `LoopItem` pro Iteration. ### 2.5 Dynamische Schemas Nodes mit konfigurationsabhängigem Output (Formulare, data.transform): ```python { "id": "input.form", "outputPorts": { 0: {"schema": "FormPayload", "dynamic": True, "deriveFrom": "fields"}, }, } ``` Schema-Ableitung: die Engine und das Frontend leiten aus dem Parameter `fields` die konkreten Output-Felder ab. Jedes Formular-Feld erscheint als wählbares Attribut im DataPicker. --- ## 3 Handover-Mechanismen ### 3.1 Prioritätsreihenfolge Wenn ein Parameter-Wert aus mehreren Quellen kommen kann: **DataRef/SystemVar/Static > Wire-Handover > Default** 1. Wire-Handover: Extraktor füllt Input-Felder aus dem Upstream-Output. 2. DataRef/System/Static: überschreiben Wire-Werte (= explizite User-Wahl gewinnt). 3. Defaults aus Parameter-Definition, falls noch leer. ### 3.2 Mechanismus 1: DataRef (Hauptmechanismus, ~80 %) User wählt im DataPicker, welches Feld eines Vorgängers in welchen Parameter fließt: ``` email.draftEmail.parameters: subject = DataRef { nodeId: "ai_1", path: ["responseData", "subject"] } body = DataRef { nodeId: "ai_1", path: ["responseData", "body"] } to = DataRef { nodeId: "form_1", path: ["payload", "recipient"] } ``` ### 3.3 Mechanismus 2: Wire-Handover (Extraktoren) Pro **Input-Port-Typ** ein Extraktor (~12 Funktionen, nicht N×M): ```python INPUT_EXTRACTORS: Dict[str, Callable] = { "EmailDraft": _extractEmailDraft, "DocumentList": _extractDocuments, "TextResult": _extractText, ... } ``` Extraktor kennt das Upstream-Schema (aus `outputPorts`) und greift Felder direkt ab — kein Raten mit Fallbacks. ### 3.4 Mechanismus 3: data.transform Expliziter Konverter-Node für Fälle, wo DataRef und Extraktor nicht reichen. ### 3.5 Mechanismus 4: System-Variablen Dritter DynamicValue-Typ neben `ref` und `value`: ```python SYSTEM_VARIABLES = { "system.timestamp": { "type": "int", "description": "Unix timestamp (ms)" }, "system.date": { "type": "str", "description": "ISO date (YYYY-MM-DD)" }, "system.datetime": { "type": "str", "description": "ISO datetime" }, "system.time": { "type": "str", "description": "HH:MM:SS" }, "system.userId": { "type": "str", "description": "Aktueller User-ID" }, "system.userName": { "type": "str", "description": "Aktueller User-Name" }, "system.userEmail": { "type": "str", "description": "Aktueller User-Email" }, "system.workflowId": { "type": "str", "description": "Workflow-ID" }, "system.runId": { "type": "str", "description": "Run-ID" }, "system.instanceId": { "type": "str", "description": "Feature-Instanz-ID" }, "system.mandateId": { "type": "str", "description": "Mandant-ID" }, "system.loopIndex": { "type": "int", "description": "Aktueller Loop-Index (nur in Loop)" }, "system.loopCount": { "type": "int", "description": "Anzahl Loop-Items (nur in Loop)" }, "system.uuid": { "type": "str", "description": "Neue zufällige UUID" }, } ``` --- ## 4 Generisches Frontend-Rendering ### 4.1 Prinzip: Jede UI-Darstellung ist ein FrontendType Es gibt **keine** Node-spezifischen Config-Components. `NODE_CONFIG_REGISTRY` entfällt komplett. ### 4.2 FrontendType-Katalog ```python class FrontendType(str, Enum): # Standard Types TEXT = "text" TEXTAREA = "textarea" NUMBER = "number" CHECKBOX = "checkbox" DATE = "date" DATETIME = "datetime" EMAIL = "email" SELECT = "select" MULTISELECT = "multiselect" JSON = "json" FILE = "file" HIDDEN = "hidden" # Picker Types (API-backed) USER_CONNECTION = "userConnection" SHAREPOINT_FOLDER = "sharepointFolder" SHAREPOINT_FILE = "sharepointFile" CLICKUP_LIST = "clickupList" CLICKUP_TASK = "clickupTask" # Complex Structure Types CASE_LIST = "caseList" FIELD_BUILDER = "fieldBuilder" KEY_VALUE_ROWS = "keyValueRows" CRON = "cron" CONDITION = "condition" MAPPING_TABLE = "mappingTable" FILTER_EXPRESSION = "filterExpression" ``` ### 4.3 Parameter-Abhängigkeiten Picker-Parameter die von anderen Parametern desselben Nodes abhängen: ```python { "name": "path", "type": "str", "frontendType": "sharepointFolder", "frontendOptions": {"dependsOn": "connectionId"}, } ``` Der generische Renderer übergibt den Wert des abhängigen Parameters. Der Picker rendert sich disabled, solange die Abhängigkeit nicht erfüllt ist. ### 4.4 Generischer Renderer ```typescript const FRONTEND_TYPE_RENDERERS: Record> = { text: TextInput, textarea: TextareaInput, number: NumberInput, checkbox: CheckboxInput, date: DatePicker, select: SelectInput, multiselect: MultiSelectInput, json: JsonEditor, userConnection: ConnectionPicker, sharepointFolder: FolderPicker, clickupList: ClickUpListPicker, caseList: CaseListEditor, fieldBuilder: FieldBuilderEditor, keyValueRows: KeyValueRowsEditor, cron: CronBuilder, condition: ConditionBuilder, mappingTable: MappingTableEditor, filterExpression: FilterExpressionEditor, }; function GenericNodeConfig({ node, nodeType }) { return nodeType.parameters.map(param => { const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput; return ; }); } ``` --- ## 5 Neue Nodes ### 5.1 data.aggregate ```python { "id": "data.aggregate", "category": "data", "label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"}, "parameters": [ {"name": "mode", "type": "str", "frontendType": "select", "options": ["collect", "concat", "sum", "count"], "default": "collect"}, ], "inputPorts": {0: {"accepts": ["Transit"]}}, "outputPorts": {0: {"schema": "AggregateResult"}}, "executor": "data", } ``` **Engine-Semantik:** Wenn data.aggregate im Loop-Body steht, wird ihr Output **nicht** pro Iteration überschrieben, sondern in `_aggregateAccumulators[nodeId]` gesammelt. Nach Loop-Ende: `nodeOutputs[nodeId] = { items: akkumulator, count: len }`. ### 5.2 data.transform ```python { "id": "data.transform", "category": "data", "label": {"en": "Transform", "de": "Umwandeln", "fr": "Transformer"}, "parameters": [ {"name": "mappings", "type": "json", "frontendType": "mappingTable"}, ], "inputPorts": {0: {"accepts": ["Transit"]}}, "outputPorts": {0: {"schema": "Dict", "dynamic": True, "deriveFrom": "mappings"}}, "executor": "data", } ``` ### 5.3 data.filter ```python { "id": "data.filter", "category": "data", "label": {"en": "Filter", "de": "Filtern", "fr": "Filtrer"}, "parameters": [ {"name": "condition", "type": "str", "frontendType": "filterExpression"}, ], "inputPorts": {0: {"accepts": ["AggregateResult", "FileList", "TaskList", "EmailList", "DocumentList"]}}, "outputPorts": {0: {"schema": "Transit"}}, "executor": "data", } ``` **Filter-Expression-Sprache:** Vergleiche (`==`, `!=`, `<`, `>`, `<=`, `>=`), Logik (`and`, `or`, `not`), String (`contains`, `startsWith`), Null (`isEmpty`, `isNotEmpty`). Der `FILTER_EXPRESSION`-Renderer baut einen visuellen Condition-Builder (Dropdown Feld → Operator → Wert). ### 5.4 flow.merge ```python { "id": "flow.merge", "category": "flow", "label": {"en": "Merge", "de": "Zusammenführen", "fr": "Fusionner"}, "parameters": [ {"name": "mode", "type": "str", "frontendType": "select", "options": ["first", "all", "append"], "default": "first"}, ], "inputs": 2, "outputs": 1, "inputPorts": {0: {"accepts": ["Transit"]}, 1: {"accepts": ["Transit"]}}, "outputPorts": {0: {"schema": "MergeResult"}}, "executor": "flow", "meta": {"icon": "mdi-call-merge", "color": "#FF9800"}, } ``` **Engine-Semantik:** flow.merge wird erst ausgeführt, wenn alle verbundenen Vorgänger verarbeitet sind. Für inaktive Branches (nach ifElse): der übersprungene Vorgänger ist nicht in `nodeOutputs` → wird als „nicht verfügbar" markiert. mode=first nimmt den ersten verfügbaren, mode=all merged nur die verfügbaren. --- ## 6 Entscheidungen | # | Frage | Entscheidung | |---|-------|-------------| | 1 | Typ-Mismatch bei Kante? | **Soft** — Warnung + visuelle Markierung, kein Hard-Block. | | 2 | Transit statt Any für Flow-Nodes? | **Ja.** Transit-Envelope mit `_meta` und `data`. DataPicker resolved rekursiv. | | 3 | Dynamische Schemas? | **Ja.** `deriveFrom`-Parameter → Schema-Ableitung. | | 4 | Migration? | **Nein.** Greenfield. | | 5 | data.aggregate separat? | **Ja.** Separater Node. | | 6 | flow.merge? | **Ja, sofort.** Komplettes Set. | | 7 | `_paramMap` Zukunft? | **Entfällt.** Node-Parameter = Action-Parameter-Namen. UI-Labels separat in `description`. | | 8 | Parameter vs Wire Priorität? | **DataRef > Wire > Default.** | | 9 | Fehler-Output? | Jeder Port-Typ hat `_success`/`_error` Meta-Felder. | | 10 | AI Structured Output? | `AiResult.responseData: Dict` + Parameter `outputFormat` (text/json). | | 11 | Pause/Resume? | Resume-Handler validiert User-Eingabe gegen Output-Schema der pausierten Node. | --- ## 7 Execution Plan — Backend ### 7.1 NEU: `gateway/modules/features/graphicalEditor/portTypes.py` Erstellen. Inhalt: - `PortField`, `PortSchema`, `InputPortDef`, `OutputPortDef` (Pydantic-Modelle, Abschnitt 2.2) - `PORT_TYPE_CATALOG`: Dict aller PortSchemas (Abschnitt 2.3) - `SYSTEM_VARIABLES`: Dict (Abschnitt 3.5) - Output-Normalizer: `_normalizeToSchema(raw: Dict, schemaName: str) -> Dict` + pro Port-Typ eine Funktion - Input-Extraktoren: `INPUT_EXTRACTORS: Dict[str, Callable]` (Abschnitt 3.3) - Transit-Envelope-Helpers: `_wrapTransit(data, meta)`, `_unwrapTransit(output)`, `_resolveTransitChain(nodeId, nodeOutputs, connectionMap)` - Schema-Ableitungsfunktionen: `_deriveFormPayloadSchema(node)`, `_deriveTransformSchema(node)` (Abschnitt 2.5) ### 7.2 ÄNDERN: `gateway/modules/shared/frontendTypes.py` **Aktuell:** `FrontendType` Enum mit 14 Werten (Zeilen 16–63). **Änderung:** Erweitern um Complex Structure Types: ``` Hinzufügen: SHAREPOINT_FILE = "sharepointFile" CLICKUP_LIST = "clickupList" CLICKUP_TASK = "clickupTask" CASE_LIST = "caseList" FIELD_BUILDER = "fieldBuilder" KEY_VALUE_ROWS = "keyValueRows" CRON = "cron" CONDITION = "condition" MAPPING_TABLE = "mappingTable" FILTER_EXPRESSION = "filterExpression" ``` `CUSTOM_TYPE_OPTIONS_API` und `CUSTOM_TYPE_DESCRIPTIONS` entsprechend erweitern. ### 7.3 NEU SCHREIBEN: `gateway/modules/features/graphicalEditor/nodeDefinitions/*.py` Alle 10 Dateien (`triggers.py`, `flow.py`, `input.py`, `ai.py`, `email.py`, `sharepoint.py`, `clickup.py`, `file.py`, `trustee.py`, `__init__.py`) **komplett neu**. Jede Node-Definition bekommt: - `inputPorts`: Dict mit `InputPortDef` pro Port-Index - `outputPorts`: Dict mit `OutputPortDef` pro Port-Index - `parameters`: Liste von Dicts im `WorkflowActionParameter`-Format (mit `frontendType`, `frontendOptions` inkl. `dependsOn`) - **Kein** `_paramMap` mehr — Parameter-Namen = Action-Parameter-Namen - `_method` und `_action` bleiben (für ActionExecutor-Mapping) NEU: `data.py` mit `data.aggregate`, `data.transform`, `data.filter`. `__init__.py`: `STATIC_NODE_TYPES` um `DATA_NODES` erweitern; `flow.py` um `flow.merge` erweitern. ### 7.4 ÄNDERN: `gateway/modules/features/graphicalEditor/nodeRegistry.py` **Funktion `getNodeTypesForApi`** (Zeile 54–75): - `_localizeNode` anpassen: `inputPorts` und `outputPorts` in API-Response einschließen (nicht als `_`-Prefix, also nicht wegstrippen) - Response erweitern um `systemVariables` aus `portTypes.SYSTEM_VARIABLES` - Response erweitern um `portTypeCatalog` aus `portTypes.PORT_TYPE_CATALOG` **Funktion `getNodeTypeToMethodAction`** (Zeile 78–89): bleibt, `_method`/`_action` weiterhin verwendet. ### 7.5 ÄNDERN: `gateway/modules/workflows/automation2/graphUtils.py` **Funktion `resolveParameterReferences`** (Zeile 185–243): - Neuen Case hinzufügen für `type: "system"`: ```python if isinstance(value, dict) and value.get("type") == "system": return _resolveSystemVariable(value["variable"], context) ``` - `_resolveSystemVariable(variable, context)` als neue Funktion (Abschnitt 3.5). **Funktion `validateGraph`** (Zeile 86–120): erweitern um Port-Kompatibilitäts-Check (soft — Warnings sammeln, nicht hart ablehnen). ### 7.6 NEU SCHREIBEN: `gateway/modules/workflows/automation2/executors/actionNodeExecutor.py` **Komplett neu.** Die ~860 Zeilen mit heuristischen Merge-Funktionen (`_extractEmailContentFromUpstream`, `_getContextFromUpstream`, `_gatherAttachmentDocumentsFromUpstream`, `_formatEmailOutputAsContext`, `_unpackIncomingEmail`, `_getIncomingEmailFromUpstream`, `_buildActionParams`, `_paramMap`-Logik, etc.) werden **ersetzt** durch: 1. Node-Definition laden (`_getNodeDefinition`) 2. Parameter per `resolveParameterReferences` auflösen (DataRef, SystemVar, Static) 3. Wire-Handover: wenn `inputSources[nodeId][0]` existiert, Extraktor aus `INPUT_EXTRACTORS` aufrufen 4. Priorität anwenden (DataRef > Wire > Default) 5. `ActionExecutor.executeAction(method, action, params)` aufrufen 6. Ergebnis durch `_normalizeToSchema(result, outputSchema)` normalisieren 7. Fehler-Envelope: bei Exception `{ _success: false, _error: str, ...defaults }` zurückgeben **Keine** node-type-spezifische Logik mehr im Executor. ### 7.7 NEU: `gateway/modules/workflows/automation2/executors/dataExecutor.py` Neuer Executor für `data.aggregate`, `data.transform`, `data.filter`: - `data.aggregate`: im Loop-Kontext akkumuliert Ergebnisse (Modus: collect, concat, sum, count) - `data.transform`: wendet `mappings` an (jedes Mapping löst eine DataRef/Static-Referenz auf) - `data.filter`: evaluiert `condition` pro Item der Input-Liste, gibt gefilterte Liste zurück ### 7.8 ÄNDERN: `gateway/modules/workflows/automation2/executors/flowExecutor.py` **Funktion `_ifElse`** (Zeile 55–65): Output ändern zu Transit-Envelope: ```python # Heute: return {"branch": 0 if ok else 1, "conditionResult": ok, "input": inp} # Neu: return {"_transit": True, "_meta": {"branch": 0 if ok else 1, "conditionResult": ok}, "data": inp} ``` **Funktion `_switch`** (Zeile 199–207): analog Transit-Envelope. **Funktion `_loop`** (Zeile 261–272): bleibt wie bisher (kein Transit, produziert `LoopItem`). **NEU: `_merge`** Funktion: sammelt Outputs aller verbundenen Input-Ports aus `nodeOutputs`, wendet Modus an (first/all/append), gibt `MergeResult` zurück. ### 7.9 ÄNDERN: `gateway/modules/workflows/automation2/executors/__init__.py` `DataExecutor` importieren und exportieren. ### 7.10 ÄNDERN: `gateway/modules/workflows/automation2/executionEngine.py` **Funktion `_getExecutor`** (Zeile 71–85): Case für `data.*` hinzufügen → `DataExecutor`. **Funktion `_is_node_on_active_path`** (Zeile 39–68): Transit-Envelope berücksichtigen — `branch`/`match` jetzt in `out["_meta"]` statt direkt in `out`. **Loop-Body-Verarbeitung** (Zeile 400–460): Spezialbehandlung für `data.aggregate` Nodes: statt `nodeOutputs[bnid] = result` ein `_aggregateAccumulators[bnid].append(result)`. Nach Loop-Ende: `nodeOutputs[aggregateNodeId] = { items: akkumulator, count: len }`. **Nach jedem Execute** (Zeile 390+): `_normalizeToSchema(result, outputSchema)` aufrufen bevor in `nodeOutputs` gespeichert. **flow.merge Wartelogik:** merge-Nodes werden von `topoSort` automatisch nach ihren Vorgängern eingeordnet (BFS). Vor Ausführung: prüfe ob alle verbundenen Vorgänger in `nodeOutputs` sind oder übersprungen wurden (inaktiver Branch → `not in nodeOutputs` und Node wurde als skipped geloggt). **Pause/Resume:** Resume-Handler (`initialNodeOutputs`) — Validierung der User-Eingabe gegen Output-Schema der pausierten Node (in `executeGraph` nach Empfang von `initialNodeOutputs`). ### 7.11 ÄNDERN: `gateway/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py` **Funktion `get_node_types`** (Zeile 146–168): Response-Struktur erweitern: ```python # Heute: return {"nodeTypes": localized, "categories": categories} # Neu: return { "nodeTypes": localized, # mit inputPorts, outputPorts "categories": categories, "portTypeCatalog": PORT_TYPE_CATALOG, # alle Schemas "systemVariables": SYSTEM_VARIABLES, } ``` --- ## 8 Execution Plan — Frontend ### 8.1 ÄNDERN: `frontend_nyla/src/api/workflowApi.ts` **Interface `NodeTypeParameter`** (Zeile 14–20): erweitern um `frontendType`, `frontendOptions`, `options`, `validation`. **Interface `NodeType`** (Zeile 22–39): erweitern um `inputPorts`, `outputPorts`. **Interface `NodeTypesResponse`** (Zeile 46–49): erweitern um `portTypeCatalog`, `systemVariables`. ### 8.2 LÖSCHEN + NEU: `frontend_nyla/src/components/FlowEditor/nodes/configs/` **Löschen:** `index.ts`, `AiNodeConfig.tsx`, `EmailNodeConfig.tsx`, `SharePointNodeConfig.tsx`, `ClickUpNodeConfig.tsx`, `ApprovalNodeConfig.tsx`, `UploadNodeConfig.tsx`, `CommentNodeConfig.tsx`, `ReviewNodeConfig.tsx`, `SelectionNodeConfig.tsx`, `ConfirmationNodeConfig.tsx`, `FileCreateNodeConfig.tsx`, `TrusteeNodeConfig.tsx`, `types.ts`. **Neu:** `frontendTypeRenderers/` Ordner mit einem Renderer pro FrontendType (TextInput, TextareaInput, NumberInput, CheckboxInput, DatePicker, SelectInput, MultiSelectInput, JsonEditor, ConnectionPicker, FolderPicker, ClickUpListPicker, CaseListEditor, FieldBuilderEditor, KeyValueRowsEditor, CronBuilder, ConditionBuilder, MappingTableEditor, FilterExpressionEditor). **Neu:** `frontendTypeRenderers/index.ts` mit `FRONTEND_TYPE_RENDERERS` Registry. Bestehende Logik aus den gelöschten Config-Components (z.B. ConnectionPicker aus `EmailNodeConfig`, CaseEditor aus `SwitchNodeConfig`, FieldBuilder aus `FormNodeConfig`) wird in die entsprechenden FrontendType-Renderer extrahiert. ### 8.3 ÄNDERN: `frontend_nyla/src/components/FlowEditor/editor/NodeConfigPanel.tsx` **Komplett neu.** Statt `NODE_CONFIG_REGISTRY`-Lookup: ```typescript import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers'; // Iteriere nodeType.parameters, rendere pro Parameter den passenden Renderer nodeType.parameters.map(param => { const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput; return ; }); ``` ### 8.4 LÖSCHEN: `frontend_nyla/src/components/FlowEditor/nodes/shared/outputPreviewRegistry.ts` Komplett entfernen. Ersetzen durch generische Funktion die aus dem `portTypeCatalog` + `outputPorts` Schema den Preview-Baum baut: ```typescript function buildPreviewFromSchema( node: CanvasNode, nodeTypes: NodeType[], portTypeCatalog: Record ): Record { const nt = nodeTypes.find(t => t.id === node.type); const outputSchema = nt?.outputPorts?.[0]?.schema; if (outputSchema === 'Transit') return {}; // resolved dynamisch const schema = portTypeCatalog[outputSchema]; // Generiere Beispielwerte pro Feld aus schema.fields } ``` ### 8.5 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/dataRef.ts` **`DynamicValue` Union** (Zeile 20): erweitern um `SystemVarRef`: ```typescript interface SystemVarRef { type: 'system'; variable: string; } type DynamicValue = DataRef | DataValue | SystemVarRef; ``` `isRef`, `isValue` etc. ergänzen um `isSystemVar` Type Guard. ### 8.6 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx` - `buildPickablePaths` (Zeile 20–40): **Schema-basiert** statt aus Preview-Daten. Erhält `portTypeCatalog` und baut Baum aus Schema-Feldern. - Transit-Auflösung: wenn Output-Schema = Transit, folge connectionMap rückwärts bis zum echten Produzenten. - Neue Sektion **„System"** mit allen Variablen aus `systemVariables`, gruppiert (Datum/Zeit, User, Workflow). - Bei Pick eines System-Werts: `onPick` liefert `SystemVarRef` statt `DataRef`. ### 8.7 ÄNDERN: `frontend_nyla/src/components/FlowEditor/context/Automation2DataFlowContext.tsx` - `Automation2DataFlowContextValue` (Zeile 10–19): erweitern um `portTypeCatalog`, `systemVariables`. - `nodeOutputsPreview` Berechnung: aus Schema statt aus `outputPreviewRegistry`. ### 8.8 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/graphUtils.ts` `fromApiGraph` / `toApiGraph`: `inputPorts` und `outputPorts` mitserialisieren in `CanvasNode`. ### 8.9 ÄNDERN: `frontend_nyla/src/components/FlowEditor/editor/FlowCanvas` (Verbindungs-Validierung) Beim Verbinden zweier Ports: prüfe `sourceNode.outputPorts[outputIdx].schema` gegen `targetNode.inputPorts[inputIdx].accepts`. Bei Mismatch: Kante gelb/orange markieren (Soft-Warnung). --- ## 9 Dateien-Übersicht ### Neue Dateien | Datei | Inhalt | |-------|--------| | `gateway/.../graphicalEditor/portTypes.py` | PortSchema, Katalog, Normalizer, Extraktoren, System-Variablen | | `gateway/.../automation2/executors/dataExecutor.py` | Executor für data.aggregate, data.transform, data.filter | | `gateway/.../nodeDefinitions/data.py` | Node-Definitionen: data.aggregate, data.transform, data.filter | | `frontend_nyla/.../nodes/frontendTypeRenderers/index.ts` | FRONTEND_TYPE_RENDERERS Registry | | `frontend_nyla/.../nodes/frontendTypeRenderers/*.tsx` | Ein Renderer pro FrontendType | ### Komplett neu geschriebene Dateien | Datei | Grund | |-------|-------| | `gateway/.../automation2/executors/actionNodeExecutor.py` | Heuristische Merge-Logik → Normalizer + Extraktor | | `gateway/.../nodeDefinitions/triggers.py` | + inputPorts, outputPorts, frontendType | | `gateway/.../nodeDefinitions/flow.py` | + flow.merge, Transit-Ports | | `gateway/.../nodeDefinitions/input.py` | + dynamische FormPayload-Schemas | | `gateway/.../nodeDefinitions/ai.py` | + AiResult-Ports, outputFormat-Param | | `gateway/.../nodeDefinitions/email.py` | + EmailDraft/EmailList-Ports | | `gateway/.../nodeDefinitions/sharepoint.py` | + FileList/DocumentList-Ports | | `gateway/.../nodeDefinitions/clickup.py` | + TaskList/TaskResult-Ports | | `gateway/.../nodeDefinitions/file.py` | + DocumentList-Ports | | `gateway/.../nodeDefinitions/trustee.py` | + Ports | | `gateway/.../nodeDefinitions/__init__.py` | + DATA_NODES Import | | `frontend_nyla/.../editor/NodeConfigPanel.tsx` | Generischer Renderer statt NODE_CONFIG_REGISTRY | ### Geänderte Dateien | Datei | Änderung | |-------|----------| | `gateway/.../shared/frontendTypes.py` | FrontendType Enum erweitern (+10 Werte) | | `gateway/.../automation2/graphUtils.py` | `resolveParameterReferences` + SystemVar, `validateGraph` + Port-Check | | `gateway/.../automation2/executors/flowExecutor.py` | ifElse/switch → Transit-Envelope, + _merge | | `gateway/.../automation2/executors/__init__.py` | + DataExecutor Export | | `gateway/.../automation2/executionEngine.py` | + DataExecutor, Transit-_meta, aggregate-Akkumulator, merge-Wartelogik | | `gateway/.../graphicalEditor/nodeRegistry.py` | API-Response + portTypeCatalog, systemVariables | | `gateway/.../graphicalEditor/routeFeatureGraphicalEditor.py` | Response-Struktur erweitern | | `frontend_nyla/.../api/workflowApi.ts` | Interfaces erweitern (inputPorts, outputPorts, frontendType) | | `frontend_nyla/.../nodes/shared/dataRef.ts` | + SystemVarRef | | `frontend_nyla/.../nodes/shared/DataPicker.tsx` | Schema-basiert, Transit-Auflösung, System-Sektion | | `frontend_nyla/.../nodes/shared/graphUtils.ts` | inputPorts/outputPorts serialisieren | | `frontend_nyla/.../context/Automation2DataFlowContext.tsx` | + portTypeCatalog, systemVariables | | `frontend_nyla/.../editor/FlowCanvas` | Verbindungs-Validierung | ### Gelöschte Dateien | Datei | Grund | |-------|-------| | `frontend_nyla/.../nodes/configs/index.ts` | Ersetzt durch FRONTEND_TYPE_RENDERERS | | `frontend_nyla/.../nodes/configs/AiNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/EmailNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/SharePointNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/ClickUpNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/ApprovalNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/UploadNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/CommentNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/ReviewNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/SelectionNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/ConfirmationNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/FileCreateNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/configs/TrusteeNodeConfig.tsx` | Generischer Renderer | | `frontend_nyla/.../nodes/shared/outputPreviewRegistry.ts` | Schema-basierte Preview |