diff --git a/scripts/apply_i18n_from_index.py b/scripts/apply_i18n_from_index.py new file mode 100644 index 0000000..27d47aa --- /dev/null +++ b/scripts/apply_i18n_from_index.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Patrick Motsch +""" +i18n-Index für DB-gestützte UI-Sprachen (de = Key, siehe wiki). + +Dieses Skript **regeneriert** `i18n_ui_strings_index.json` aus den im Projekt verwendeten +`t('…')`- und `t("…")`-Aufrufen in den angegebenen Quellfiles. Es wendet **keine** +Code-Änderungen an — Migration neuer Texte erfolgt im TSX mit `t()`, anschließend Index +mit `--write` aktualisieren und fehlende Keys in der Admin-UI (UI-Sprachen) nachziehen. + +Nutzung (im Ordner frontend_nyla): + python scripts/apply_i18n_from_index.py --write + python scripts/apply_i18n_from_index.py # nur prüfen: gleiche Keys wie in JSON? + +Konvention: deutscher Klartext ist der Key (siehe `2026-04-ui-i18n-dynamic-language-sets.md`). +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +INDEX_PATH = Path(__file__).resolve().parent / "i18n_ui_strings_index.json" + +# Dateien mit nutzerrelevanten t()-Aufrufen (erweitern bei weiteren Migrationen) +SCAN_PATHS = [ + ROOT / "src/pages/views/trustee/TrusteeAccountingSettingsView.tsx", + ROOT / "src/pages/AutomationsDashboardPage.tsx", + ROOT / "src/pages/billing/BillingDataView.tsx", + ROOT / "src/pages/billing/BillingAdmin.tsx", + ROOT / "src/components/Navigation/MandateNavigation.tsx", +] + + +def _extractTKeys(source: str) -> set[str]: + """Extrahiert erste String-Literale aus t('…') / t("…") (ohne verschachtelte Klammern).""" + keys: set[str] = set() + for m in re.finditer(r"\bt\(\s*((?:'([^'\\]|\\.)*'|\"([^\"\\]|\\.)*\"))\s*(?:,|\))", source): + raw = m.group(1) + if raw.startswith("'"): + inner = raw[1:-1].replace("\\'", "'").replace("\\\\", "\\") + else: + inner = raw[1:-1].replace('\\"', '"').replace("\\\\", "\\") + keys.add(inner) + return keys + + +def _scanAllKeys() -> dict[str, list[str]]: + fileToKeys: dict[str, list[str]] = {} + for p in SCAN_PATHS: + if not p.exists(): + print(f"WARN: fehlt {p}", file=sys.stderr) + continue + text = p.read_text(encoding="utf-8") + keys = sorted(_extractTKeys(text)) + rel = str(p.relative_to(ROOT)).replace("\\", "/") + fileToKeys[rel] = keys + return fileToKeys + + +def _mergeUnique(fileToKeys: dict[str, list[str]]) -> list[str]: + allKeys: set[str] = set() + for ks in fileToKeys.values(): + allKeys.update(ks) + return sorted(allKeys) + + +def main() -> None: + parser = argparse.ArgumentParser(description="i18n-Index aus t()-Aufrufen pflegen.") + parser.add_argument("--write", action="store_true", help="i18n_ui_strings_index.json schreiben") + args = parser.parse_args() + + byFile = _scanAllKeys() + merged = _mergeUnique(byFile) + payload = { + "version": 2, + "description": "Extrahierte UI-Keys (Deutsch = Key) für DB/Admin UI-Sprachen; erzeugt durch apply_i18n_from_index.py", + "sourceRoot": "frontend_nyla", + "keysByFile": byFile, + "keysAll": merged, + } + + if args.write: + INDEX_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"Written {INDEX_PATH} ({len(merged)} unique keys)") + return + + if INDEX_PATH.exists(): + existing = json.loads(INDEX_PATH.read_text(encoding="utf-8")) + prev = set(existing.get("keysAll", [])) + now = set(merged) + if prev != now: + onlyNew = sorted(now - prev) + onlyOld = sorted(prev - now) + print("Index differs from scan. Run with --write to refresh.") + if onlyNew: + print(f" + new ({len(onlyNew)}):", onlyNew[:20], "..." if len(onlyNew) > 20 else "") + if onlyOld: + print(f" - removed ({len(onlyOld)}):", onlyOld[:20], "..." if len(onlyOld) > 20 else "") + sys.exit(1) + print(f"OK: index matches scan ({len(merged)} keys)") + else: + print("No index file; run with --write to create.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/i18n_ui_strings_index.json b/scripts/i18n_ui_strings_index.json new file mode 100644 index 0000000..b943033 --- /dev/null +++ b/scripts/i18n_ui_strings_index.json @@ -0,0 +1,312 @@ +{ + "version": 2, + "description": "Extrahierte UI-Keys (Deutsch = Key) für DB/Admin UI-Sprachen; erzeugt durch apply_i18n_from_index.py", + "sourceRoot": "frontend_nyla", + "keysByFile": { + "src/pages/views/trustee/TrusteeAccountingSettingsView.tsx": [ + "Aktueller Datenbestand:", + "Anbindung entfernen", + "Anzeigename", + "Bis (optional)", + "Buchhaltungsanbindung entfernen? Synchronisierte Daten bleiben erhalten.", + "Buchhaltungsanbindung wurde entfernt.", + "Buchhaltungsdaten importieren", + "Buchhaltungseinstellungen werden geladen…", + "Buchhaltungssystem", + "Buchhaltungssystem-Anbindung", + "Cache geleert", + "Cache konnte nicht geleert werden.", + "Daten jetzt einlesen", + "Die Buchhaltungskonfiguration wurde erfolgreich gespeichert.", + "Entfernen", + "Entfernen der Konfiguration fehlgeschlagen.", + "Entfernt", + "Fehler", + "Gespeichert", + "Import abgeschlossen", + "Import abgeschlossen in {sek}s:", + "Import fehlgeschlagen", + "Import teilweise fehlgeschlagen", + "Importiere…", + "KI-Cache leeren", + "Keine Verbindung möglich.", + "Konfiguration speichern", + "Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.", + "Laufendes Jahr", + "Leere…", + "Letzter Import:", + "Letzter Monat", + "Letzter Sync:", + "Letztes Jahr", + "Speichern der Konfiguration fehlgeschlagen.", + "Speichern…", + "Status:", + "System auswählen…", + "Teste…", + "Unbekannter Fehler", + "Verbinden Sie ein Buchhaltungssystem, um Buchungen aus dieser Trustee-Instanz automatisch zu synchronisieren.", + "Verbindung OK", + "Verbindung erfolgreich!", + "Verbindung fehlgeschlagen", + "Verbindung fehlgeschlagen: {message}", + "Verbindung testen", + "Verbindung zum Buchhaltungssystem erfolgreich.", + "Verbindungstest fehlgeschlagen.", + "Verbunden:", + "z. B. Run My Accounts – Muster AG", + "{konten} Konten, {buchungen} Buchungen ({zeilen} Zeilen), {kontakte} Kontakte, {salden} Salden", + "{konten} Konten, {buchungen} Buchungen, {kontakte} Kontakte, {salden} Salden importiert.", + "{konten} Konten, {buchungen} Buchungen, {zeilen} Zeilen, {kontakte} Kontakte, {salden} Salden", + "{n} gecachte Abfragen entfernt. Die nächste KI-Abfrage liest frische Daten." + ], + "src/pages/AutomationsDashboardPage.tsx": [ + "Aktive Workflows", + "Aktualisieren", + "Automations", + "Beendet", + "Credits gesamt:", + "Download fehlgeschlagen", + "Fehler beim Laden des Automations-Dashboards", + "Gestartet", + "Letzte Runs", + "Mandant", + "Noch keine Workflow-Runs vorhanden.", + "Runs gesamt", + "Status", + "Tokens gesamt:", + "Tracing-Protokoll herunterladen", + "Workflow", + "Workflow-Runs über alle Features und Mandanten", + "Workflows", + "—" + ], + "src/pages/billing/BillingDataView.tsx": [ + "Alle (RBAC)", + "Anbieter", + "Balken", + "Benutzer", + "Beschreibung", + "Betrag (CHF)", + "Datum", + "Diagramme", + "Feature", + "Fehler beim Laden der Transaktionen", + "Keine Statistiken verfügbar", + "Keine Transaktionen vorhanden", + "Kontext:", + "Mandant", + "Mandant: {name}", + "Meine Kosten", + "Modell", + "Pie", + "Schließen", + "Transaktionen", + "Typ", + "Zahlung abgebrochen.", + "Zahlung erfolgreich, aber Verbuchung konnte nicht bestätigt werden.", + "Zahlung erfolgreich. Guthaben wird gutgeschrieben.", + "nur meine Daten", + "Übersicht" + ], + "src/pages/billing/BillingAdmin.tsx": [ + "Abonnement", + "Aktiv", + "Auto-Nachladung (KI-Guthaben bei niedrigem Stand)", + "Bei Warnung benachrichtigen", + "Beschreibung", + "Beschreibung der Gutschrift", + "Betrag (CHF)", + "Betrag darf nicht null sein", + "Billing-Einstellungen", + "Bitte wählen Sie einen Mandanten aus.", + "Checkout fehlgeschlagen", + "Deaktiviert", + "Einstellungen", + "Einstellungen speichern", + "Fehler bei der Buchung", + "Guthaben abziehen", + "Guthaben aufladen", + "Guthaben manuell verwalten", + "Guthaben via Stripe aufladen", + "Guthaben:", + "Kein Mandanten-Konto vorhanden", + "Keine Konten vorhanden", + "Konten", + "Lade Konten…", + "Mandant auswählen", + "Mandanten-Konto", + "Manuelle Buchung durch Admin", + "Max. Nachladungen / Monat", + "Mit Stripe bezahlen", + "OK", + "Sie werden zu Stripe weitergeleitet. Nach erfolgreicher Zahlung kehren Sie hierher zurück.", + "Speichern…", + "Status:", + "Warnschwelle (%)", + "Weiterleitung…", + "Wird verbucht…", + "Zahlung abgebrochen.", + "Zahlung erfolgreich, aber Verbuchung konnte nicht bestätigt werden.", + "Zahlung erfolgreich. Guthaben wird gutgeschrieben.", + "Zahlung erfolgreich. Guthaben wurde verbucht.", + "z.B. 50 oder -20", + "{betrag} erfolgreich abgezogen.", + "{betrag} erfolgreich gutgeschrieben." + ], + "src/components/Navigation/MandateNavigation.tsx": [ + "Administration", + "Fehler", + "Keine Feature-Instanzen verfügbar.", + "Kontaktiere einen Administrator, um Zugriff zu erhalten.", + "Meine Sicht", + "Navigation wird geladen…", + "Neuer Name:", + "Umbenennen", + "Umbenennung fehlgeschlagen: {detail}" + ] + }, + "keysAll": [ + "Abonnement", + "Administration", + "Aktiv", + "Aktive Workflows", + "Aktualisieren", + "Aktueller Datenbestand:", + "Alle (RBAC)", + "Anbieter", + "Anbindung entfernen", + "Anzeigename", + "Auto-Nachladung (KI-Guthaben bei niedrigem Stand)", + "Automations", + "Balken", + "Beendet", + "Bei Warnung benachrichtigen", + "Benutzer", + "Beschreibung", + "Beschreibung der Gutschrift", + "Betrag (CHF)", + "Betrag darf nicht null sein", + "Billing-Einstellungen", + "Bis (optional)", + "Bitte wählen Sie einen Mandanten aus.", + "Buchhaltungsanbindung entfernen? Synchronisierte Daten bleiben erhalten.", + "Buchhaltungsanbindung wurde entfernt.", + "Buchhaltungsdaten importieren", + "Buchhaltungseinstellungen werden geladen…", + "Buchhaltungssystem", + "Buchhaltungssystem-Anbindung", + "Cache geleert", + "Cache konnte nicht geleert werden.", + "Checkout fehlgeschlagen", + "Credits gesamt:", + "Daten jetzt einlesen", + "Datum", + "Deaktiviert", + "Diagramme", + "Die Buchhaltungskonfiguration wurde erfolgreich gespeichert.", + "Download fehlgeschlagen", + "Einstellungen", + "Einstellungen speichern", + "Entfernen", + "Entfernen der Konfiguration fehlgeschlagen.", + "Entfernt", + "Feature", + "Fehler", + "Fehler bei der Buchung", + "Fehler beim Laden der Transaktionen", + "Fehler beim Laden des Automations-Dashboards", + "Gespeichert", + "Gestartet", + "Guthaben abziehen", + "Guthaben aufladen", + "Guthaben manuell verwalten", + "Guthaben via Stripe aufladen", + "Guthaben:", + "Import abgeschlossen", + "Import abgeschlossen in {sek}s:", + "Import fehlgeschlagen", + "Import teilweise fehlgeschlagen", + "Importiere…", + "KI-Cache leeren", + "Kein Mandanten-Konto vorhanden", + "Keine Feature-Instanzen verfügbar.", + "Keine Konten vorhanden", + "Keine Statistiken verfügbar", + "Keine Transaktionen vorhanden", + "Keine Verbindung möglich.", + "Konfiguration speichern", + "Kontaktiere einen Administrator, um Zugriff zu erhalten.", + "Konten", + "Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.", + "Kontext:", + "Lade Konten…", + "Laufendes Jahr", + "Leere…", + "Letzte Runs", + "Letzter Import:", + "Letzter Monat", + "Letzter Sync:", + "Letztes Jahr", + "Mandant", + "Mandant auswählen", + "Mandant: {name}", + "Mandanten-Konto", + "Manuelle Buchung durch Admin", + "Max. Nachladungen / Monat", + "Meine Kosten", + "Meine Sicht", + "Mit Stripe bezahlen", + "Modell", + "Navigation wird geladen…", + "Neuer Name:", + "Noch keine Workflow-Runs vorhanden.", + "OK", + "Pie", + "Runs gesamt", + "Schließen", + "Sie werden zu Stripe weitergeleitet. Nach erfolgreicher Zahlung kehren Sie hierher zurück.", + "Speichern der Konfiguration fehlgeschlagen.", + "Speichern…", + "Status", + "Status:", + "System auswählen…", + "Teste…", + "Tokens gesamt:", + "Tracing-Protokoll herunterladen", + "Transaktionen", + "Typ", + "Umbenennen", + "Umbenennung fehlgeschlagen: {detail}", + "Unbekannter Fehler", + "Verbinden Sie ein Buchhaltungssystem, um Buchungen aus dieser Trustee-Instanz automatisch zu synchronisieren.", + "Verbindung OK", + "Verbindung erfolgreich!", + "Verbindung fehlgeschlagen", + "Verbindung fehlgeschlagen: {message}", + "Verbindung testen", + "Verbindung zum Buchhaltungssystem erfolgreich.", + "Verbindungstest fehlgeschlagen.", + "Verbunden:", + "Warnschwelle (%)", + "Weiterleitung…", + "Wird verbucht…", + "Workflow", + "Workflow-Runs über alle Features und Mandanten", + "Workflows", + "Zahlung abgebrochen.", + "Zahlung erfolgreich, aber Verbuchung konnte nicht bestätigt werden.", + "Zahlung erfolgreich. Guthaben wird gutgeschrieben.", + "Zahlung erfolgreich. Guthaben wurde verbucht.", + "nur meine Daten", + "z. B. Run My Accounts – Muster AG", + "z.B. 50 oder -20", + "{betrag} erfolgreich abgezogen.", + "{betrag} erfolgreich gutgeschrieben.", + "{konten} Konten, {buchungen} Buchungen ({zeilen} Zeilen), {kontakte} Kontakte, {salden} Salden", + "{konten} Konten, {buchungen} Buchungen, {kontakte} Kontakte, {salden} Salden importiert.", + "{konten} Konten, {buchungen} Buchungen, {zeilen} Zeilen, {kontakte} Kontakte, {salden} Salden", + "{n} gecachte Abfragen entfernt. Die nächste KI-Abfrage liest frische Daten.", + "Übersicht", + "—" + ] +} diff --git a/src/App.tsx b/src/App.tsx index 9c69204..d012e32 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,10 +38,11 @@ import { SettingsPage } from './pages/Settings'; import { GDPRPage } from './pages/GDPR'; import StorePage from './pages/Store'; import { FeatureViewPage } from './pages/FeatureView'; -import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage } from './pages/admin'; +import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminLanguagesPage } from './pages/admin'; import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing'; +import { AutomationsDashboardPage } from './pages/AutomationsDashboardPage'; function App() { // Load saved theme preference and set app name on app mount useEffect(() => { @@ -114,8 +115,14 @@ function App() { } /> } /> + } /> + {/* ============================================== */} + {/* AUTOMATIONS DASHBOARD */} + {/* ============================================== */} + } /> + {/* Legacy top-level routes – redirect to dashboard (migrated to feature-instance routes) */} } /> } /> @@ -191,11 +198,12 @@ function App() { } /> } /> - } /> + } /> } /> } /> } /> + } /> } /> } /> diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index c97b9f8..4aa550f 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -17,6 +17,32 @@ export interface NodeTypeParameter { required?: boolean; description?: string; default?: unknown; + frontendType?: string; + frontendOptions?: Record; + options?: unknown[]; + validation?: Record; +} + +export interface PortField { + name: string; + type: string; + description: Record; + required: boolean; +} + +export interface PortSchema { + name: string; + fields: PortField[]; +} + +export interface InputPortDef { + accepts: string[]; +} + +export interface OutputPortDef { + schema: string; + dynamic?: boolean; + deriveFrom?: string; } export interface NodeType { @@ -27,9 +53,10 @@ export interface NodeType { parameters: NodeTypeParameter[]; inputs: number; outputs: number; - /** Labels per output (e.g. ["Ja", "Nein"] for flow.ifElse) */ outputLabels?: string[]; executor: string; + inputPorts?: Record; + outputPorts?: Record; meta?: { icon?: string; color?: string; @@ -43,9 +70,16 @@ export interface NodeTypeCategory { label: Record | string; } +export interface SystemVariable { + type: string; + description: string; +} + export interface NodeTypesResponse { nodeTypes: NodeType[]; categories: NodeTypeCategory[]; + portTypeCatalog?: Record; + systemVariables?: Record; } export interface Automation2GraphNode { diff --git a/src/components/ContentPreview/ContentPreview.tsx b/src/components/ContentPreview/ContentPreview.tsx index 7e9e101..d492662 100644 --- a/src/components/ContentPreview/ContentPreview.tsx +++ b/src/components/ContentPreview/ContentPreview.tsx @@ -141,7 +141,7 @@ export function ContentPreview({ const actions: PopupAction[] = [ // Copy Content Button - only show for text-based files (exclude PDFs and images) or corrupted PDFs ...(mimeType !== 'application/pdf' && !mimeType?.startsWith('image/') && (mimeType?.startsWith('text/') || mimeType === 'application/json' || previewContent) ? [{ - label: copySuccess ? t('files.preview.copied', 'Copied!') : t(''), + label: copySuccess ? t('In die Zwischenablage kopiert') : t(''), icon: copySuccess ? '✓' : , onClick: handleCopyContent, disabled: !previewContent && !previewUrl, @@ -292,7 +292,7 @@ export function ContentPreview({ - {t('common.retry', 'Retry')} + {t('Wiederholen')} ); diff --git a/src/components/ContentPreview/renderers/JsonRenderer.tsx b/src/components/ContentPreview/renderers/JsonRenderer.tsx index b458f46..51e0917 100644 --- a/src/components/ContentPreview/renderers/JsonRenderer.tsx +++ b/src/components/ContentPreview/renderers/JsonRenderer.tsx @@ -471,7 +471,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
- {preprocessedData.keys.length} {t('files.preview.json.properties', 'properties')} + {preprocessedData.keys.length} {t('Eigenschaften')}
{renderTable(preprocessedData, 0, 'root')} @@ -488,14 +488,14 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) { return (
- {t('files.preview.json.invalid', 'Raw Content (Invalid JSON)')}: {fileName} + {t('Ungültiges JSON')}: {fileName}
diff --git a/src/components/ContentPreview/renderers/LoadingRenderer.tsx b/src/components/ContentPreview/renderers/LoadingRenderer.tsx index bf4c40b..afe011a 100644 --- a/src/components/ContentPreview/renderers/LoadingRenderer.tsx +++ b/src/components/ContentPreview/renderers/LoadingRenderer.tsx @@ -7,7 +7,7 @@ export function LoadingRenderer() { return (
-

{t('files.preview.loading', 'Loading preview...')}

+

{t('Vorschau wird geladen...')}

); } diff --git a/src/components/ContentPreview/renderers/PdfRenderer.tsx b/src/components/ContentPreview/renderers/PdfRenderer.tsx index 9a1d8ee..5f57cbc 100644 --- a/src/components/ContentPreview/renderers/PdfRenderer.tsx +++ b/src/components/ContentPreview/renderers/PdfRenderer.tsx @@ -31,7 +31,7 @@ export function PdfRenderer({ previewUrl, previewContent, fileName, onError }: P
- {t('files.preview.pdfFileCorrupted', 'This file appears to be corrupted. It has a PDF extension but contains text content. Please re-upload the file if possible.')} + {t('Diese Datei scheint beschädigt zu sein. Sie hat eine PDF-Erweiterung, enthält aber Textinhalte. Bitte laden Sie die Datei erneut hoch, falls möglich.')}
diff --git a/src/components/ContentPreview/renderers/UnsupportedRenderer.tsx b/src/components/ContentPreview/renderers/UnsupportedRenderer.tsx index 631d62c..0c71258 100644 --- a/src/components/ContentPreview/renderers/UnsupportedRenderer.tsx +++ b/src/components/ContentPreview/renderers/UnsupportedRenderer.tsx @@ -12,14 +12,14 @@ export function UnsupportedRenderer({ previewUrl, fileName }: UnsupportedRendere return (
📄
-

{t('files.preview.unsupported', 'Preview not available for this file type')}

+

{t('Vorschau für diesen Dateityp nicht verfügbar')}

{fileName}

- {t('files.action.download', 'Download')} + {t('Herunterladen')}
); diff --git a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx index e3ef08e..8fb4419 100644 --- a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx +++ b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx @@ -1,11 +1,12 @@ /** * Automation2 Flow Editor - Data flow context for Data Picker and DynamicValueField. + * Extended with portTypeCatalog and systemVariables for the Typed Port System. */ import React, { createContext, useContext, useMemo } from 'react'; import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas'; import { getAvailableSources } from '../nodes/shared/dataFlowGraph'; -import type { NodeType } from '../../../api/workflowApi'; +import type { NodeType, PortSchema, SystemVariable } from '../../../api/workflowApi'; export interface Automation2DataFlowContextValue { currentNodeId: string; @@ -14,6 +15,8 @@ export interface Automation2DataFlowContextValue { nodeOutputsPreview: Record; nodeTypes: NodeType[]; language: string; + portTypeCatalog: Record; + systemVariables: Record; getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string; getAvailableSourceIds: () => string[]; } @@ -31,6 +34,8 @@ interface Automation2DataFlowProviderProps { nodeOutputsPreview: Record; nodeTypes: NodeType[]; language: string; + portTypeCatalog?: Record; + systemVariables?: Record; children: React.ReactNode; } @@ -41,6 +46,8 @@ export const Automation2DataFlowProvider: React.FC { const value = useMemo((): Automation2DataFlowContextValue | null => { @@ -52,11 +59,13 @@ export const Automation2DataFlowProvider: React.FC n.title ?? n.label ?? n.type ?? n.id, getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections), }; - }, [node?.id, nodes, connections, nodeOutputsPreview, nodeTypes, language]); + }, [node?.id, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables]); return ( diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx index e0fa29f..a94ebfc 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx @@ -43,7 +43,7 @@ import { syncCanvasStartNode, buildInvocationsForPrimaryKind, } from '../nodes/runtime/workflowStartSync'; -import { buildNodeOutputsPreview } from '../nodes/shared/outputPreviewRegistry'; +import { buildNodeOutputsPreview, setPortTypeCatalog as setRegistryCatalog } from '../nodes/shared/outputPreviewRegistry'; import { Automation2DataFlowProvider } from '../context/Automation2DataFlowContext'; import { usePrompt } from '../../../hooks/usePrompt'; import { EditorChatPanel } from './EditorChatPanel'; @@ -88,6 +88,8 @@ export const Automation2FlowEditor: React.FC = ({ const { prompt: promptInput, PromptDialog } = usePrompt(); const [nodeTypes, setNodeTypes] = useState([]); const [categories, setCategories] = useState([]); + const [portTypeCatalog, setPortTypeCatalog] = useState>({}); + const [systemVariables, setSystemVariables] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filter, setFilter] = useState(''); @@ -166,8 +168,8 @@ export const Automation2FlowEditor: React.FC = ({ const nodeOutputsPreview = useMemo( () => - buildNodeOutputsPreview(canvasNodes, executeResult?.nodeOutputs as Record | undefined), - [canvasNodes, executeResult?.nodeOutputs] + buildNodeOutputsPreview(canvasNodes, nodeTypes, executeResult?.nodeOutputs as Record | undefined), + [canvasNodes, nodeTypes, executeResult?.nodeOutputs] ); const applyGraphWithSync = useCallback( @@ -353,6 +355,11 @@ export const Automation2FlowEditor: React.FC = ({ const data = await fetchNodeTypes(request, instanceId, language); setNodeTypes(data.nodeTypes); setCategories(data.categories); + if (data.portTypeCatalog) { + setPortTypeCatalog(data.portTypeCatalog); + setRegistryCatalog(data.portTypeCatalog as never); + } + if (data.systemVariables) setSystemVariables(data.systemVariables); } catch (err: unknown) { setError(err instanceof Error ? err.message : String(err)); setNodeTypes([]); @@ -720,6 +727,8 @@ export const Automation2FlowEditor: React.FC = ({ nodeOutputsPreview={nodeOutputsPreview} nodeTypes={nodeTypes} language={language} + portTypeCatalog={portTypeCatalog as Record} + systemVariables={systemVariables as Record} > nt.id === sourceNode.type); + const tgtType = nodeTypes.find((nt) => nt.id === targetNode.type); + if (!srcType?.outputPorts || !tgtType?.inputPorts) return 'ok'; + + const srcPort = srcType.outputPorts[sourceOutputIdx]; + const tgtPort = tgtType.inputPorts[targetInputIdx]; + if (!srcPort || !tgtPort) return 'ok'; + + const srcSchema = srcPort.schema; + const accepts = tgtPort.accepts; + if (!accepts || accepts.length === 0) return 'ok'; + if (accepts.includes('Transit')) return 'ok'; + if (accepts.includes(srcSchema)) return 'ok'; + return 'warning'; +} + interface FlowCanvasProps { nodes: CanvasNode[]; connections: CanvasConnection[]; @@ -70,6 +94,7 @@ export const FlowCanvas: React.FC = ({ const [selectedNodeIds, setSelectedNodeIds] = useState>(new Set()); const selectedNodeId = selectedNodeIds.size === 1 ? [...selectedNodeIds][0] : null; const [selectedConnectionId, setSelectedConnectionId] = useState(null); + const [connectionWarnings, setConnectionWarnings] = useState>({}); const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; @@ -245,6 +270,18 @@ export const FlowCanvas: React.FC = ({ targetId: targetNodeId, targetHandle: targetHandleIndex, }; + + const srcNode = nodes.find((n) => n.id === connectingFrom.nodeId); + const tgtNode = nodes.find((n) => n.id === targetNodeId); + if (srcNode && tgtNode) { + const sourceOutputIdx = connectingFrom.handleIndex >= srcNode.inputs + ? connectingFrom.handleIndex - srcNode.inputs : 0; + const compat = _checkConnectionCompatibility(srcNode, sourceOutputIdx, tgtNode, targetHandleIndex, nodeTypes); + if (compat === 'warning') { + setConnectionWarnings((prev) => ({ ...prev, [newConn.id]: true })); + } + } + onConnectionsChange([...connections, newConn]); setConnectingFrom(null); setDragPos(null); @@ -568,6 +605,16 @@ export const FlowCanvas: React.FC = ({ > + + + {connections.map((c) => { const srcNode = nodes.find((n) => n.id === c.sourceId); @@ -578,6 +625,12 @@ export const FlowCanvas: React.FC = ({ const dx = tgt.x - src.x; const pathD = `M ${src.x} ${src.y} C ${src.x + Math.abs(dx) / 2} ${src.y}, ${tgt.x - Math.abs(dx) / 2} ${tgt.y}, ${tgt.x} ${tgt.y}`; const isSelected = selectedConnectionId === c.id; + const isWarning = connectionWarnings[c.id]; + const strokeColor = isSelected + ? 'var(--primary-color, #007bff)' + : isWarning + ? '#FF9800' + : 'var(--text-secondary, #666)'; return ( = ({ + {isWarning && !isSelected && ( + Type mismatch warning: output type may not match input type + )} ); })} diff --git a/src/components/FlowEditor/editor/NodeConfigPanel.tsx b/src/components/FlowEditor/editor/NodeConfigPanel.tsx index 5401bc3..217ae54 100644 --- a/src/components/FlowEditor/editor/NodeConfigPanel.tsx +++ b/src/components/FlowEditor/editor/NodeConfigPanel.tsx @@ -1,14 +1,14 @@ /** - * NodeConfigPanel - Configures parameters for input, ai, email, sharepoint nodes. - * Delegates to config components from nodes/configs. + * NodeConfigPanel - Generic parameter renderer for all node types. + * Renders each parameter using FRONTEND_TYPE_RENDERERS based on frontendType. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import type { CanvasNode } from './FlowCanvas'; -import type { NodeType } from '../../../api/workflowApi'; +import type { NodeType, NodeTypeParameter } from '../../../api/workflowApi'; import type { ApiRequestFunction } from '../../../api/workflowApi'; import { getLabel } from '../nodes/shared/utils'; -import { NODE_CONFIG_REGISTRY } from '../nodes/configs'; +import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers'; import styles from './Automation2FlowEditor.module.css'; interface NodeConfigPanelProps { @@ -22,8 +22,6 @@ interface NodeConfigPanelProps { request?: ApiRequestFunction; } -const CONFIGURABLE_PREFIXES = ['trigger.', 'input.', 'ai.', 'email.', 'sharepoint.', 'clickup.', 'flow.', 'file.', 'trustee.']; - export const NodeConfigPanel: React.FC = ({ node, nodeType, @@ -52,7 +50,6 @@ export const NodeConfigPanel: React.FC = ({ }; }, [node?.id]); - /** Do not call onParametersChange (parent setState) inside setParams updater — React forbids updating a parent during a child's state update. */ const updateParam = useCallback( (key: string, value: unknown) => { setParams((prev) => { @@ -73,21 +70,11 @@ export const NodeConfigPanel: React.FC = ({ [onParametersChange] ); - const isConfigurable = node && CONFIGURABLE_PREFIXES.some((p) => node.type.startsWith(p)); - if (!node || !isConfigurable) return null; - - const ConfigRenderer = NODE_CONFIG_REGISTRY[node.type]; - if (!ConfigRenderer) { - return ( -
-

{getLabel(nodeType?.label, language) || node.type}

-

No configuration for {node.type}

-
- ); - } + if (!node || !nodeType) return null; const isTrigger = node.type.startsWith('trigger.'); const showNameField = onNodeUpdate && !isTrigger; + const parameters = nodeType.parameters || []; return (
@@ -112,14 +99,22 @@ export const NodeConfigPanel: React.FC = ({ {getLabel(nodeType.description, language)}

)} - + {parameters.map((param: NodeTypeParameter) => { + const frontendType = param.frontendType || 'text'; + const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text; + return ( + updateParam(param.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + /> + ); + })}
); }; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx new file mode 100644 index 0000000..37213ae --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -0,0 +1,405 @@ +/** + * Generic FrontendType renderer registry. + * Maps frontendType strings to React components. + */ + +import type { ComponentType } from 'react'; +import type { NodeTypeParameter } from '../../../../api/workflowApi'; +import type { ApiRequestFunction } from '../../../../api/workflowApi'; + +export interface FieldRendererProps { + param: NodeTypeParameter; + value: unknown; + onChange: (value: unknown) => void; + allParams?: Record; + instanceId?: string; + request?: ApiRequestFunction; + nodeType?: string; +} + +export type FieldRendererComponent = ComponentType; + +// --------------------------------------------------------------------------- +// Inline renderers for standard types +// --------------------------------------------------------------------------- + +import React from 'react'; + +const TextInput: React.FC = ({ param, value, onChange }) => ( +
+ + onChange(e.target.value)} + placeholder={param.name} + style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }} + /> +
+); + +const TextareaInput: React.FC = ({ param, value, onChange }) => ( +
+ +