From 289568fd52fcf0f33a959f074d215853ae37e48c Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:09:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(workflows):=20advanced=20text=20mode=20?= =?UTF-8?q?=E2=80=94=20export/import=20the=20flow=20as=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 'Text' toggle in the editor toolbar swaps the canvas for the whole flow as pretty JSON ({name, trigger_type, enabled, nodes, edges}). Copy it to share or hand to an LLM, or paste a flow and 'Load into editor' (validates parse + one trigger; backend re-validates on Save). Imported nodes land at 0,0 — one 'Clean up layout' click arranges them. en + native de. --- frontend/src/i18n/locales/de.json | 7 ++ frontend/src/i18n/locales/en.json | 7 ++ .../admin/workflows/WorkflowEditorPage.tsx | 68 ++++++++++++++++++- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 05c92f2d..2db8a319 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -230,6 +230,13 @@ "namePlaceholder": "Workflow-Name", "when": "Wenn", "cleanUp": "Layout aufräumen", + "textView": "Text", + "canvasView": "Canvas", + "loadText": "In Editor laden", + "textLoaded": "Geladen – prüfen und speichern", + "textNeedsArrays": "Benötigt „nodes“- und „edges“-Arrays", + "textNeedsTrigger": "Benötigt genau einen Trigger-Knoten", + "textHint": "Der gesamte Ablauf als JSON – zum Teilen kopieren oder an ein LLM geben, oder einen Ablauf einfügen und in den Editor laden. Nach dem Import „Layout aufräumen“ klicken.", "saved": "Workflow gespeichert", "saveFailed": "Speichern fehlgeschlagen", "badJson": "Konfiguration ist kein gültiges JSON", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 357dcd0c..e45373f7 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -230,6 +230,13 @@ "namePlaceholder": "Workflow name", "when": "When", "cleanUp": "Clean up layout", + "textView": "Text", + "canvasView": "Canvas", + "loadText": "Load into editor", + "textLoaded": "Loaded — review and Save", + "textNeedsArrays": "Needs \"nodes\" and \"edges\" arrays", + "textNeedsTrigger": "Needs exactly one trigger node", + "textHint": "The whole flow as JSON — copy it to share or hand to an LLM, or paste a flow and load it into the editor. Click “Clean up layout” after importing.", "saved": "Workflow saved", "saveFailed": "Could not save", "badJson": "Config is not valid JSON", diff --git a/frontend/src/pages/admin/workflows/WorkflowEditorPage.tsx b/frontend/src/pages/admin/workflows/WorkflowEditorPage.tsx index 95f87d9f..8d68f089 100644 --- a/frontend/src/pages/admin/workflows/WorkflowEditorPage.tsx +++ b/frontend/src/pages/admin/workflows/WorkflowEditorPage.tsx @@ -18,7 +18,7 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import dagre from '@dagrejs/dagre'; -import { ArrowLeft, Save, Trash2, Wand2 } from 'lucide-react'; +import { ArrowLeft, Save, Trash2, Wand2, Code } from 'lucide-react'; import { Button, Loading } from '../../../components/common'; import { useAdminDarkMode } from '../../../contexts/AdminDarkModeContext'; import { workflowsService, type WorkflowNodeType } from '../../../services/workflows.service'; @@ -136,6 +136,47 @@ export const WorkflowEditorPage: React.FC = () => { setTimeout(() => rfRef.current?.fitView?.({ padding: 0.2, duration: 300 }), 60); }, [edges, setNodes]); + // --- Advanced text mode: the whole flow as JSON (share / LLM round-trip) --- + const [textMode, setTextMode] = useState(false); + const [text, setText] = useState(''); + const [textErr, setTextErr] = useState(null); + + const serializeFlow = () => JSON.stringify({ + name, + trigger_type: triggerType, + enabled, + nodes: nodes.map((n) => ({ + node_key: n.id, type: (n.data as any).nodeType, config: (n.data as any).config || {}, + pos_x: Math.round(n.position.x), pos_y: Math.round(n.position.y), + })), + edges: edges.map((e) => ({ from_node: e.source, from_handle: e.sourceHandle || null, to_node: e.target })), + }, null, 2); + + const openText = () => { setText(serializeFlow()); setTextErr(null); setTextMode(true); }; + + const applyText = () => { + let p: any; + try { p = JSON.parse(text); } catch (e) { setTextErr(t('workflows.editor.badJson', 'Config is not valid JSON') as string); return; } + if (!Array.isArray(p.nodes) || !Array.isArray(p.edges)) { setTextErr(t('workflows.editor.textNeedsArrays', 'Needs "nodes" and "edges" arrays') as string); return; } + if (p.nodes.filter((n: any) => n.type === 'trigger').length !== 1) { setTextErr(t('workflows.editor.textNeedsTrigger', 'Needs exactly one trigger node') as string); return; } + const tt = p.trigger_type || triggerType; + if (p.name != null) setName(p.name); + if (p.trigger_type) setTriggerType(p.trigger_type); + if (p.enabled != null) setEnabled(!!p.enabled); + setNodes(p.nodes.map((n: any) => ({ + id: n.node_key, type: 'wf', position: { x: n.pos_x || 0, y: n.pos_y || 0 }, + data: { nodeType: n.type, config: n.config || {}, triggerType: tt }, + }))); + setEdges(p.edges.map((e: any, i: number) => ({ + id: `e${i}`, source: e.from_node, target: e.to_node, sourceHandle: e.from_handle || undefined, label: e.from_handle || undefined, + }))); + setTextErr(null); + setTextMode(false); + toast.success(t('workflows.editor.textLoaded', 'Loaded — review and Save') as string); + }; + + const copyText = () => { navigator.clipboard?.writeText(text); toast.success(t('common.copied', 'Copied') as string); }; + useEffect(() => { if (!workflow) return; setName(workflow.name); @@ -230,13 +271,17 @@ export const WorkflowEditorPage: React.FC = () => { setEnabled(e.target.checked)} /> {t('workflows.enabled', 'Enabled')} -
+
+
+ {!textMode && (
{PALETTE.map((type) => (
+ )} + {textMode ? ( +
+

+ {t('workflows.editor.textHint', 'The whole flow as JSON — copy it to share or hand to an LLM, or paste a flow and load it into the editor. Click “Clean up layout” after importing.')} +

+