/** * Automation2FlowEditor * * n8n-style flow builder with backend-driven node list. * Composes: NodeSidebar, FlowCanvas, NodeConfigPanel, CanvasHeader. */ import React, { useState, useEffect, useCallback } from 'react'; import { FaSpinner } from 'react-icons/fa'; import { useApiRequest } from '../../hooks/useApi'; import { fetchNodeTypes, executeGraph, fetchWorkflows, fetchWorkflow, createWorkflow, updateWorkflow, type NodeType, type NodeTypeCategory, type Automation2Graph, type Automation2Workflow, type ExecuteGraphResponse, } from '../../api/automation2Api'; import { FlowCanvas, type CanvasNode, type CanvasConnection } from './FlowCanvas'; import { NodeConfigPanel } from './NodeConfigPanel'; import { NodeSidebar } from './NodeSidebar'; import { CanvasHeader } from './CanvasHeader'; import { getCategoryIcon } from './utils'; import { fromApiGraph, toApiGraph } from './graphUtils'; import styles from './Automation2FlowEditor.module.css'; const LOG = '[Automation2]'; interface Automation2FlowEditorProps { instanceId: string; language?: string; /** When set, load this workflow on mount (e.g. from workflows list edit) */ initialWorkflowId?: string | null; } export const Automation2FlowEditor: React.FC = ({ instanceId, language = 'de', initialWorkflowId, }) => { const { request } = useApiRequest(); const [nodeTypes, setNodeTypes] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filter, setFilter] = useState(''); const [expandedCategories, setExpandedCategories] = useState>( new Set(['trigger', 'input', 'flow', 'data']) ); const [expandedIoMethods, setExpandedIoMethods] = useState>(new Set()); const [canvasNodes, setCanvasNodes] = useState([]); const [canvasConnections, setCanvasConnections] = useState([]); const [executing, setExecuting] = useState(false); const [executeResult, setExecuteResult] = useState(null); const [workflows, setWorkflows] = useState([]); const [currentWorkflowId, setCurrentWorkflowId] = useState(null); const [selectedNode, setSelectedNode] = useState(null); const [saving, setSaving] = useState(false); const handleFromApiGraph = useCallback( (graph: Automation2Graph) => { const { nodes, connections } = fromApiGraph(graph, nodeTypes); setCanvasNodes(nodes); setCanvasConnections(connections); }, [nodeTypes] ); const handleExecute = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { setExecuteResult({ success: false, error: 'Keine Nodes im Workflow.' }); return; } setExecuting(true); setExecuteResult(null); try { const result = await executeGraph( request, instanceId, graph, currentWorkflowId ?? undefined ); setExecuteResult(result); } catch (err: unknown) { setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) }); } finally { setExecuting(false); } }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId]); const loadWorkflows = useCallback(async () => { if (!instanceId) return; try { const items = await fetchWorkflows(request, instanceId); setWorkflows(items); } catch (e) { console.error(`${LOG} loadWorkflows failed`, e); } }, [instanceId, request]); const handleSave = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { setExecuteResult({ success: false, error: 'Keine Nodes zum Speichern.' }); return; } setSaving(true); try { if (currentWorkflowId) { await updateWorkflow(request, instanceId, currentWorkflowId, { graph }); setExecuteResult({ success: true } as ExecuteGraphResponse); } else { const label = prompt('Workflow-Name:', 'Neuer Workflow') || 'Neuer Workflow'; const created = await createWorkflow(request, instanceId, { label, graph }); setCurrentWorkflowId(created.id); setWorkflows((prev) => [...prev, created]); setExecuteResult({ success: true } as ExecuteGraphResponse); } } catch (err: unknown) { setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) }); } finally { setSaving(false); } }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId]); const handleLoad = useCallback( async (workflowId: string) => { try { const wf = await fetchWorkflow(request, instanceId, workflowId); if (wf.graph) handleFromApiGraph(wf.graph); } catch (err: unknown) { setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err), }); } }, [request, instanceId, handleFromApiGraph] ); const handleWorkflowSelect = useCallback( (workflowId: string | null) => { setCurrentWorkflowId(workflowId); if (workflowId) handleLoad(workflowId); else { setCanvasNodes([]); setCanvasConnections([]); setExecuteResult(null); } }, [handleLoad] ); const handleNew = useCallback(() => { setCanvasNodes([]); setCanvasConnections([]); setCurrentWorkflowId(null); setExecuteResult(null); }, []); const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record) => { setCanvasNodes((prev) => prev.map((n) => (n.id === nodeId ? { ...n, parameters } : n))); }, []); const loadNodeTypes = useCallback(async () => { if (!instanceId) return; setLoading(true); setError(null); try { const data = await fetchNodeTypes(request, instanceId, language); setNodeTypes(data.nodeTypes); setCategories(data.categories); } catch (err: unknown) { setError(err instanceof Error ? err.message : String(err)); setNodeTypes([]); setCategories([]); } finally { setLoading(false); } }, [instanceId, language, request]); useEffect(() => { loadNodeTypes(); }, [loadNodeTypes]); useEffect(() => { loadWorkflows(); }, [loadWorkflows]); useEffect(() => { if (initialWorkflowId && workflows.length > 0 && !currentWorkflowId) { handleWorkflowSelect(initialWorkflowId); } }, [initialWorkflowId, workflows, currentWorkflowId, handleWorkflowSelect]); const toggleCategory = useCallback((id: string) => { setExpandedCategories((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }, []); const toggleIoMethod = useCallback((method: string) => { setExpandedIoMethods((prev) => { const next = new Set(prev); if (next.has(method)) next.delete(method); else next.add(method); return next; }); }, []); const handleDropNodeType = useCallback( (nodeTypeId: string, x: number, y: number) => { const nt = nodeTypes.find((n) => n.id === nodeTypeId); if (!nt) return; const id = `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const label = typeof nt.label === 'string' ? nt.label : (nt.label as Record)?.[language] ?? nt.id; setCanvasNodes((prev) => [ ...prev, { id, type: nodeTypeId, x, y, label, title: label, color: nt.meta?.color, inputs: nt.inputs ?? 1, outputs: nt.outputs ?? 1, parameters: {}, }, ]); }, [nodeTypes, language] ); const renderSidebar = () => { if (loading) { return (

Nodes

Lade Node-Typen...

); } if (error) { return (

Nodes

{error}

); } return ( ); }; return (
{renderSidebar()}
0} executeResult={executeResult} />
node.title ?? node.label ?? node.type} getCategoryIcon={getCategoryIcon} onSelectionChange={setSelectedNode} />
{selectedNode?.type?.startsWith('input.') && ( nt.id === selectedNode.type)} language={language} onParametersChange={handleNodeParametersChange} /> )}
); }; export default Automation2FlowEditor;