/** * Automation2FlowEditor * * n8n-style flow builder with backend-driven node list. * Workflow configuration (gear): primary start kind + invocations; canvas start node stays in sync. */ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { FaSpinner } from 'react-icons/fa'; import { useApiRequest } from '../../../hooks/useApi'; import { fetchNodeTypes, executeGraph, fetchWorkflows, fetchWorkflow, createWorkflow, updateWorkflow, fetchVersions, createDraftVersion, publishVersion, unpublishVersion, archiveVersion, createTemplateFromWorkflow, copyTemplate, importWorkflowFromFile, type NodeType, type NodeTypeCategory, type Automation2Graph, type Automation2Workflow, type ExecuteGraphResponse, type WorkflowEntryPoint, type AutoVersion, type AutoTemplateScope, } from '../../../api/workflowApi'; import { FlowCanvas, computeAutoLayout, type CanvasNode, type CanvasConnection } from './FlowCanvas'; import { NodeConfigPanel } from './NodeConfigPanel'; import { NodeSidebar } from './NodeSidebar'; import { CanvasHeader } from './CanvasHeader'; import { WorkflowConfigurationModal } from './WorkflowConfigurationModal'; import { TemplatePicker } from './TemplatePicker'; import { getCategoryIcon } from '../nodes/shared/utils'; import { fromApiGraph, toApiGraph } from '../nodes/shared/graphUtils'; import { syncCanvasStartNode, buildInvocationsForPrimaryKind, } from '../nodes/runtime/workflowStartSync'; import { buildNodeOutputsPreview, setPortTypeCatalog as setRegistryCatalog } from '../nodes/shared/outputPreviewRegistry'; import { findGraphErrors } from '../nodes/shared/paramValidation'; import { getLabel as getParamLabel } from '../nodes/shared/utils'; import { Automation2DataFlowProvider } from '../context/Automation2DataFlowContext'; import { usePrompt } from '../../../hooks/usePrompt'; import { EditorChatPanel } from './EditorChatPanel'; import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './EditorChatPanel'; import { EditorWorkflowChatList } from './EditorWorkflowChatList'; import { RunTracingPanel } from './RunTracingPanel'; import { UnifiedDataBar } from '../../../components/UnifiedDataBar'; import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar'; import styles from './Automation2FlowEditor.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; import { useToast } from '../../../contexts/ToastContext'; import { useFeatureStore } from '../../../stores/featureStore'; const LOG = '[Automation2]'; const _buildDefaultInvocations = (runLabel: string): WorkflowEntryPoint[] => buildInvocationsForPrimaryKind('manual', [], runLabel); interface Automation2FlowEditorProps { instanceId: string; mandateId?: string; language?: string; /** When set, load this workflow on mount (e.g. from workflows list edit) */ initialWorkflowId?: string | null; pendingFiles?: PendingFile[]; onRemovePendingFile?: (fileId: string) => void; dataSources?: EditorDataSource[]; featureDataSources?: EditorFeatureDataSource[]; onFileSelect?: (fileId: string, fileName?: string) => void; onSourcesChanged?: () => void; } export const Automation2FlowEditor: React.FC = ({ instanceId, mandateId, language = 'de', initialWorkflowId, pendingFiles, onRemovePendingFile, dataSources, featureDataSources, onFileSelect, onSourcesChanged, }) => { const { t } = useLanguage(); const { showError } = useToast(); const { request } = useApiRequest(); const { prompt: promptInput, PromptDialog } = usePrompt(); const [nodeTypes, setNodeTypes] = useState([]); const [categories, setCategories] = useState([]); const [portTypeCatalog, setPortTypeCatalog] = useState>({}); const [systemVariables, setSystemVariables] = useState>({}); const [formFieldTypes, setFormFieldTypes] = 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', 'ai', 'email', 'sharepoint', 'clickup', 'trustee']) ); 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 [invocations, setInvocations] = useState(() => _buildDefaultInvocations(t('Jetzt ausführen')) ); const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false); const [leftPanelOpen, setLeftPanelOpen] = useState(true); const [tracingRunId, setTracingRunId] = useState(null); const [tracingNodeStatuses, setTracingNodeStatuses] = useState>({}); const [rightTab, setRightTab] = useState<'nodes' | 'tracing'>('nodes'); type LeftTab = UdbTab | 'ai'; const [udbTab, setUdbTab] = useState('ai'); const udbContext: UdbContext = useMemo(() => ({ instanceId, mandateId: mandateId || '', featureInstanceId: instanceId, surface: 'graphEditor', }), [instanceId, mandateId]); const [versions, setVersions] = useState([]); const [currentVersionId, setCurrentVersionId] = useState(null); const [versionLoading, setVersionLoading] = useState(false); const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState(instanceId); const featureStore = useFeatureStore(); const targetInstanceOptions = useMemo(() => { const allInstances = featureStore.getAllInstances(); return allInstances .filter((inst) => inst.mandateId === mandateId || !mandateId) .map((inst) => ({ id: inst.id, label: inst.instanceLabel || inst.featureCode || inst.id })); }, [featureStore, mandateId]); const [leftPanelWidth, setLeftPanelWidth] = useState(() => { try { const v = parseInt(localStorage.getItem('flowEditor.leftPanelWidth') ?? ''); return v >= 240 && v <= 600 ? v : 340; } catch { return 340; } }); const [sidebarWidth, setSidebarWidth] = useState(() => { try { const v = parseInt(localStorage.getItem('flowEditor.sidebarWidth') ?? ''); return v >= 200 && v <= 500 ? v : 280; } catch { return 280; } }); // Verbose schema toggle: shows the static type-reference block (input/output // schema) and parameter type-badges in NodeConfigPanel. Only the // CanvasHeader exposes the toggle (sysadmin-only); persisted to localStorage. const [verboseSchema, setVerboseSchema] = useState(() => { try { return localStorage.getItem('flowEditor.verboseSchema') === '1'; } catch { return false; } }); useEffect(() => { try { localStorage.setItem('flowEditor.verboseSchema', verboseSchema ? '1' : '0'); } catch { /* ignore */ } }, [verboseSchema]); const resizingRef = useRef<{ target: 'left' | 'right'; startX: number; startW: number } | null>(null); useEffect(() => { const _onMouseMove = (e: MouseEvent) => { if (!resizingRef.current) return; const { target, startX, startW } = resizingRef.current; const delta = e.clientX - startX; if (target === 'left') { setLeftPanelWidth(Math.max(240, Math.min(600, startW + delta))); } else { setSidebarWidth(Math.max(200, Math.min(500, startW - delta))); } }; const _onMouseUp = () => { if (!resizingRef.current) return; const { target } = resizingRef.current; resizingRef.current = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; if (target === 'left') { setLeftPanelWidth((w) => { try { localStorage.setItem('flowEditor.leftPanelWidth', String(w)); } catch {} return w; }); } else { setSidebarWidth((w) => { try { localStorage.setItem('flowEditor.sidebarWidth', String(w)); } catch {} return w; }); } }; document.addEventListener('mousemove', _onMouseMove); document.addEventListener('mouseup', _onMouseUp); return () => { document.removeEventListener('mousemove', _onMouseMove); document.removeEventListener('mouseup', _onMouseUp); }; }, []); const _startResize = useCallback((target: 'left' | 'right', e: React.MouseEvent) => { e.preventDefault(); resizingRef.current = { target, startX: e.clientX, startW: target === 'left' ? leftPanelWidth : sidebarWidth }; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }, [leftPanelWidth, sidebarWidth]); const sidebarExcludedCategories = useMemo(() => new Set(['trigger']), []); const nodeOutputsPreview = useMemo( () => buildNodeOutputsPreview(canvasNodes, nodeTypes, executeResult?.nodeOutputs as Record | undefined), [canvasNodes, nodeTypes, executeResult?.nodeOutputs] ); // Phase-4 Schicht-4 — Per-node required-but-unbound errors used by both the // canvas error badges and the Run-button gate. Graph-level: Save stays // unconditional (Schicht-4 invariant: WIP must always be persistable). const nodeErrors = useMemo( () => findGraphErrors( canvasNodes, nodeTypes, (p) => getParamLabel(p.description, language) || p.name, ), [canvasNodes, nodeTypes, language] ); const hasGraphErrors = useMemo(() => Object.keys(nodeErrors).length > 0, [nodeErrors]); const firstErrorNodeId = useMemo(() => Object.keys(nodeErrors)[0] ?? null, [nodeErrors]); const applyGraphWithSync = useCallback( (graph: Automation2Graph | null | undefined, wfInvocations: WorkflowEntryPoint[] | undefined) => { const inv = wfInvocations?.length ? wfInvocations : _buildDefaultInvocations(t('Jetzt ausführen')); setInvocations(inv); if (!graph?.nodes?.length) { const synced = syncCanvasStartNode([], [], inv, nodeTypes, language); setCanvasNodes(synced.nodes); setCanvasConnections(synced.connections); return; } const { nodes, connections } = fromApiGraph(graph, nodeTypes); const synced = syncCanvasStartNode(nodes, connections, inv, nodeTypes, language); setCanvasNodes(synced.nodes); setCanvasConnections(synced.connections); }, [nodeTypes, language, t] ); const handleFromApiGraph = useCallback( (graph: Automation2Graph, wfInvocations?: WorkflowEntryPoint[]) => { applyGraphWithSync(graph, wfInvocations); }, [applyGraphWithSync] ); const handleExecute = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { setExecuteResult({ success: false, error: t('Keine Nodes im Workflow.') }); return; } // Phase-4 Schicht-4: Run blockiert bei Pflicht-Fehlern. Save bleibt offen. if (Object.keys(nodeErrors).length > 0) { const firstId = Object.keys(nodeErrors)[0]; const firstNode = canvasNodes.find((n) => n.id === firstId); if (firstNode) setSelectedNode(firstNode); setExecuteResult({ success: false, error: t('Workflow hat Pflicht-Felder ohne Quelle. Bitte erst beheben.') + (firstNode ? ` (${firstNode.title ?? firstNode.label ?? firstNode.type})` : ''), }); return; } setExecuting(true); setExecuteResult(null); try { const ep = currentWorkflowId ? invocations[0]?.id : undefined; const result = await executeGraph(request, instanceId, graph, currentWorkflowId ?? undefined, { ...(ep ? { entryPointId: ep } : {}), }); setExecuteResult(result); if (result.runId) { setTracingRunId(result.runId); setRightTab('tracing'); } } catch (err: unknown) { setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) }); } finally { setExecuting(false); } }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, invocations, t, nodeErrors]); const handleSave = useCallback(async () => { const graph = toApiGraph(canvasNodes, canvasConnections); if (graph.nodes.length === 0) { setExecuteResult({ success: false, error: t('Keine Nodes zum Speichern.') }); return; } // Phase-4 Schicht-4 / AC 9: Save bleibt bei Pflicht-Fehlern erlaubt, // aber wir berichten die Anzahl in einem nicht-blockierenden Warning, // damit der User die WIP-Lücken nicht stillschweigend persistiert. const errorCount = Object.values(nodeErrors).reduce( (acc, list) => acc + list.length, 0, ); const errorNodeCount = Object.keys(nodeErrors).length; const _buildSaveResult = (): ExecuteGraphResponse => ({ success: true, warning: errorCount > 0 ? t('Gespeichert mit {n} Pflicht-Fehlern in {m} Nodes.') .replace('{n}', String(errorCount)) .replace('{m}', String(errorNodeCount)) : undefined, }); setSaving(true); try { if (currentWorkflowId) { await updateWorkflow(request, instanceId, currentWorkflowId, { graph, invocations, targetFeatureInstanceId }); setExecuteResult(_buildSaveResult()); } else { const label = await promptInput(t('Workflow-Name:'), { title: t('Workflow speichern'), defaultValue: t('Neuer Workflow'), placeholder: t('Name des Workflows'), }); if (!label) { setSaving(false); return; } const created = await createWorkflow(request, instanceId, { label: label.trim() || t('Neuer Workflow'), graph, invocations, targetFeatureInstanceId, }); setCurrentWorkflowId(created.id); if (created.invocations?.length) setInvocations(created.invocations); setWorkflows((prev) => [...prev, created]); setExecuteResult(_buildSaveResult()); } } catch (err: unknown) { setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) }); } finally { setSaving(false); } }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t, nodeErrors, targetFeatureInstanceId]); const handleLoad = useCallback( async (workflowId: string) => { try { const wf = await fetchWorkflow(request, instanceId, workflowId); if (wf.graph) { handleFromApiGraph(wf.graph, wf.invocations); } else { applyGraphWithSync({ nodes: [], connections: [] }, wf.invocations); } setTargetFeatureInstanceId(wf.targetFeatureInstanceId ?? instanceId); setWorkflows((prev) => { const idx = prev.findIndex((w) => w.id === workflowId); if (idx === -1) return [...prev, wf]; const next = prev.slice(); next[idx] = { ...prev[idx], ...wf }; return next; }); } catch (err: unknown) { const status = (err as { response?: { status?: number } })?.response?.status; if (status === 404) { setWorkflows((prev) => prev.filter((w) => w.id !== workflowId)); setCurrentWorkflowId((prev) => (prev === workflowId ? null : prev)); setExecuteResult(null); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); try { const result = await fetchWorkflows(request, instanceId); setWorkflows(Array.isArray(result) ? result : result.items); } catch (refreshErr) { console.error(`${LOG} workflows refresh failed`, refreshErr); } return; } setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err), }); } }, [request, instanceId, handleFromApiGraph, applyGraphWithSync, t] ); const handleWorkflowSelect = useCallback( (workflowId: string | null) => { setCurrentWorkflowId(workflowId); if (workflowId) handleLoad(workflowId); else { setExecuteResult(null); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); } }, [handleLoad, applyGraphWithSync, t] ); const handleNew = useCallback(() => { setCurrentWorkflowId(null); setExecuteResult(null); applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); }, [applyGraphWithSync, t]); const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record) => { setCanvasNodes((prev) => prev.map((n) => { if (n.id !== nodeId) return n; const next = { ...n, parameters }; if (n.type === 'flow.switch' && 'cases' in parameters) { const cases = (parameters.cases as unknown[]) ?? []; next.outputs = Math.max(1, cases.length); } return next; }) ); }, []); const handleMergeNodeParameters = useCallback((nodeId: string, patch: Record) => { setCanvasNodes((prev) => prev.map((n) => { if (n.id !== nodeId) return n; const merged = { ...(n.parameters ?? {}), ...patch }; const next = { ...n, parameters: merged }; if (n.type === 'flow.switch' && 'cases' in merged) { const cases = (merged.cases as unknown[]) ?? []; next.outputs = Math.max(1, cases.length); } return next; }) ); }, []); const handleNodeUpdate = useCallback( (nodeId: string, updates: Partial>) => { setCanvasNodes((prev) => prev.map((n) => (n.id === nodeId ? { ...n, ...updates } : n)) ); }, [] ); const handleApplyWorkflowConfiguration = useCallback( (next: WorkflowEntryPoint[]) => { setInvocations(next); setCanvasNodes((nodes) => { const r = syncCanvasStartNode(nodes, canvasConnections, next, nodeTypes, language); setCanvasConnections(r.connections); return r.nodes; }); }, [canvasConnections, nodeTypes, language] ); 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); if (data.portTypeCatalog) { setPortTypeCatalog(data.portTypeCatalog); setRegistryCatalog(data.portTypeCatalog as never); } if (data.systemVariables) setSystemVariables(data.systemVariables); if (data.formFieldTypes) setFormFieldTypes(data.formFieldTypes); } catch (err: unknown) { setError(err instanceof Error ? err.message : String(err)); setNodeTypes([]); setCategories([]); } finally { setLoading(false); } }, [instanceId, language, request]); const loadWorkflows = useCallback(async () => { if (!instanceId) return; try { const result = await fetchWorkflows(request, instanceId); setWorkflows(Array.isArray(result) ? result : result.items); } catch (e) { console.error(`${LOG} loadWorkflows failed`, e); } }, [instanceId, request]); useEffect(() => { loadNodeTypes(); }, [loadNodeTypes]); useEffect(() => { loadWorkflows(); }, [loadWorkflows]); const lastAppliedInitialRef = useRef(undefined); useEffect(() => { if (!initialWorkflowId || workflows.length === 0 || nodeTypes.length === 0) return; if (lastAppliedInitialRef.current === initialWorkflowId) return; lastAppliedInitialRef.current = initialWorkflowId; handleWorkflowSelect(initialWorkflowId); }, [initialWorkflowId, workflows, handleWorkflowSelect, nodeTypes.length]); useEffect(() => { if (loading || nodeTypes.length === 0) return; if (currentWorkflowId || initialWorkflowId) return; if (canvasNodes.length > 0) return; applyGraphWithSync({ nodes: [], connections: [] }, _buildDefaultInvocations(t('Jetzt ausführen'))); }, [ loading, nodeTypes.length, currentWorkflowId, initialWorkflowId, canvasNodes.length, applyGraphWithSync, t, ]); 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 handleDropNodeType = useCallback( (nodeTypeId: string, x: number, y: number) => { if (nodeTypeId.startsWith('trigger.')) return; 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 loadVersions = useCallback(async () => { if (!instanceId || !currentWorkflowId) { setVersions([]); return; } try { const v = await fetchVersions(request, instanceId, currentWorkflowId); setVersions(v); } catch (e) { console.error(`${LOG} loadVersions failed`, e); } }, [instanceId, currentWorkflowId, request]); useEffect(() => { loadVersions(); }, [loadVersions]); const handleVersionSelect = useCallback( (versionId: string | null) => { setCurrentVersionId(versionId); if (versionId) { const v = versions.find((ver) => ver.id === versionId); if (v?.graph) { handleFromApiGraph(v.graph, v.invocations); } } }, [versions, handleFromApiGraph] ); const handlePublishVersion = useCallback( async (versionId: string) => { if (!instanceId) return; setVersionLoading(true); try { await publishVersion(request, instanceId, versionId); await loadVersions(); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } finally { setVersionLoading(false); } }, [request, instanceId, loadVersions] ); const handleUnpublishVersion = useCallback( async (versionId: string) => { if (!instanceId) return; setVersionLoading(true); try { await unpublishVersion(request, instanceId, versionId); await loadVersions(); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } finally { setVersionLoading(false); } }, [request, instanceId, loadVersions] ); const handleArchiveVersion = useCallback( async (versionId: string) => { if (!instanceId) return; setVersionLoading(true); try { await archiveVersion(request, instanceId, versionId); await loadVersions(); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } finally { setVersionLoading(false); } }, [request, instanceId, loadVersions] ); const handleCreateDraft = useCallback(async () => { if (!instanceId || !currentWorkflowId) return; setVersionLoading(true); try { const draft = await createDraftVersion(request, instanceId, currentWorkflowId); await loadVersions(); setCurrentVersionId(draft.id); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } finally { setVersionLoading(false); } }, [request, instanceId, currentWorkflowId, loadVersions]); // Template: save current workflow as template const [templateSaving, setTemplateSaving] = useState(false); const handleSaveAsTemplate = useCallback( async (scope: AutoTemplateScope) => { if (!instanceId || !currentWorkflowId) return; setTemplateSaving(true); try { await createTemplateFromWorkflow(request, instanceId, currentWorkflowId, scope); setExecuteResult({ success: true, error: undefined } as unknown as ExecuteGraphResponse); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } finally { setTemplateSaving(false); } }, [request, instanceId, currentWorkflowId] ); // Template: new workflow from template const [templatePickerOpen, setTemplatePickerOpen] = useState(false); const handleNewFromTemplate = useCallback( async (templateId: string) => { if (!instanceId) return; try { const wf = await copyTemplate(request, instanceId, templateId); setWorkflows((prev) => [...prev, wf]); setCurrentWorkflowId(wf.id); if (wf.graph) handleFromApiGraph(wf.graph, wf.invocations); setTemplatePickerOpen(false); } catch (e: unknown) { setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) }); } }, [request, instanceId, handleFromApiGraph] ); const handleTargetInstanceChange = useCallback(async (newTargetId: string) => { setTargetFeatureInstanceId(newTargetId || null); if (currentWorkflowId && newTargetId) { try { await updateWorkflow(request, instanceId, currentWorkflowId, { targetFeatureInstanceId: newTargetId }); } catch (e: unknown) { console.error(`${LOG} target instance update failed`, e); } } }, [request, instanceId, currentWorkflowId]); const handleWorkflowRename = useCallback(async (workflowId: string, newName: string) => { try { await updateWorkflow(request, instanceId, workflowId, { label: newName }); setWorkflows((prev) => prev.map((w) => w.id === workflowId ? { ...w, label: newName } : w)); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); console.error(`${LOG} rename failed`, e); showError(t('Workflow umbenennen fehlgeschlagen: {msg}', { msg })); } }, [request, instanceId, showError, t]); const handleAutoLayout = useCallback(() => { setCanvasNodes((prev) => computeAutoLayout(prev, canvasConnections)); }, [canvasConnections]); const _sidebarStyle = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]); const renderSidebar = () => { if (loading) { return (

{t('Knoten')}

{t('Lade Nodetypen…')}

); } if (error) { return (

{t('Knoten')}

{error}

); } return ( ); }; const configurableSelected = selectedNode && [ 'input.', 'ai.', 'email.', 'sharepoint.', 'clickup.', 'trigger.', 'flow.', 'file.', 'trustee.', 'context.', 'data.', 'redmine.', ].some((p) => selectedNode.type.startsWith(p)); return (
{/* Left panel: Workspace (Chats / Dateien / Quellen) */} {leftPanelOpen && (<>
{(['ai', 'chats', 'files', 'sources'] as const).map((tab) => ( ))}
{/* KI-Panel bleibt gemountet, damit der Chatverlauf beim Tab-Wechsel (Chats / Dateien / Quellen) erhalten bleibt. Nur per CSS umblenden. `key={currentWorkflowId}` setzt den Verlauf sauber zurück, wenn der Nutzer einen anderen Workflow wählt. */}
{ if (currentWorkflowId) handleLoad(currentWorkflowId); }} pendingFiles={pendingFiles} onRemovePendingFile={onRemovePendingFile} dataSources={dataSources} featureDataSources={featureDataSources} />
{udbTab === 'chats' && ( )} {(udbTab === 'files' || udbTab === 'sources') && ( setUdbTab(tab as LeftTab)} hideTabs={['chats']} onFileSelect={onFileSelect} onSourcesChanged={onSourcesChanged} onWorkflowImportedFromFile={async (workflowId) => { await loadWorkflows(); handleWorkflowSelect(workflowId); }} /> )}
_startResize('left', e)} /> )} {/* Canvas area - center */}
setWorkflowSettingsOpen(true)} onToggleChat={() => setLeftPanelOpen((prev) => !prev)} saving={saving} executing={executing} hasNodes={canvasNodes.length > 0} executeBlockedReason={ hasGraphErrors ? t('Pflicht-Felder ohne Quelle vorhanden. Klicken markiert die erste betroffene Node.') : null } onExecuteBlockedClick={() => { if (firstErrorNodeId) { const n = canvasNodes.find((x) => x.id === firstErrorNodeId); if (n) setSelectedNode(n); } }} executeResult={executeResult} versions={versions} currentVersionId={currentVersionId} onVersionSelect={handleVersionSelect} onPublishVersion={handlePublishVersion} onUnpublishVersion={handleUnpublishVersion} onArchiveVersion={handleArchiveVersion} onCreateDraft={handleCreateDraft} versionLoading={versionLoading} onSaveAsTemplate={handleSaveAsTemplate} templateSaving={templateSaving} onNewFromTemplate={() => setTemplatePickerOpen(true)} onWorkflowRename={handleWorkflowRename} onAutoLayout={handleAutoLayout} verboseSchema={verboseSchema} onVerboseSchemaChange={setVerboseSchema} targetFeatureInstanceId={targetFeatureInstanceId} onTargetInstanceChange={handleTargetInstanceChange} targetInstanceOptions={targetInstanceOptions} />
node.title ?? node.label ?? node.type} getCategoryIcon={getCategoryIcon} onSelectionChange={setSelectedNode} highlightedNodeIds={tracingRunId ? tracingNodeStatuses : undefined} nodeErrors={nodeErrors} onExternalDrop={async (mime, payload) => { if (mime !== 'application/json+workflow' || !instanceId) return false; const p = payload as { files?: Array<{ id: string }> } | undefined; const fileId = p?.files?.[0]?.id; if (!fileId) return false; try { const result = await importWorkflowFromFile(request, instanceId, { fileId }); await loadWorkflows(); if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id); return true; } catch (e) { console.error(`${LOG} workflow drop import failed`, e); return false; } }} />
{configurableSelected && selectedNode && ( } systemVariables={systemVariables as Record} formFieldTypes={formFieldTypes} instanceId={instanceId} request={request} > nt.id === selectedNode.type)} language={language} onParametersChange={handleNodeParametersChange} onMergeNodeParameters={handleMergeNodeParameters} onNodeUpdate={handleNodeUpdate} instanceId={instanceId} request={request} verboseSchema={verboseSchema} /> )}
{/* Right panel: Nodes + Tracing tabs */}
_startResize('right', e)} />
{rightTab === 'nodes' ? ( renderSidebar() ) : ( { const node = canvasNodes.find((n) => n.id === nodeId); if (node) setSelectedNode(node); }} onActiveStepsChange={setTracingNodeStatuses} /> )}
setWorkflowSettingsOpen(false)} invocations={invocations} onApply={handleApplyWorkflowConfiguration} /> setTemplatePickerOpen(false)} onSelect={handleNewFromTemplate} instanceId={instanceId} request={request} />
); }; export default Automation2FlowEditor;