From fe7321c84d8aa48efcda16f5f0ae67a6228d912b Mon Sep 17 00:00:00 2001 From: Ida Date: Tue, 26 May 2026 10:47:26 +0200 Subject: [PATCH] fix: canvas loop bug and node placement --- .../editor/Automation2FlowEditor.tsx | 22 +++++++-- .../FlowEditor/editor/FlowCanvas.tsx | 49 ++++++++++++++++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx index 1e8c157..b9923ce 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx @@ -158,6 +158,7 @@ export const Automation2FlowEditor: React.FC = ({ in const [versions, setVersions] = useState([]); const [currentVersionId, setCurrentVersionId] = useState(null); const [versionLoading, setVersionLoading] = useState(false); + const didBootstrapEmptyCanvasRef = useRef(false); const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState(instanceId); @@ -598,8 +599,22 @@ export const Automation2FlowEditor: React.FC = ({ in useEffect(() => { if (loading || nodeTypes.length === 0) return; - if (currentWorkflowId || initialWorkflowId) return; - if (canvasNodes.length > 0) return; + if (currentWorkflowId || initialWorkflowId) { + didBootstrapEmptyCanvasRef.current = false; + return; + } + if (didBootstrapEmptyCanvasRef.current) return; + didBootstrapEmptyCanvasRef.current = true; + if (canvasNodes.length === 0 && canvasConnections.length === 0 && invocations.length === 0) { + return; + } + console.debug(`${LOG} bootstrapping empty canvas`, { + currentWorkflowId, + initialWorkflowId, + canvasNodes: canvasNodes.length, + canvasConnections: canvasConnections.length, + invocations: invocations.length, + }); applyGraphWithSync({ nodes: [], connections: [] }, [], { skipHistory: true, }); @@ -609,8 +624,9 @@ export const Automation2FlowEditor: React.FC = ({ in currentWorkflowId, initialWorkflowId, canvasNodes.length, + canvasConnections.length, + invocations.length, applyGraphWithSync, - t, ]); const toggleCategory = useCallback((id: string) => { diff --git a/src/components/FlowEditor/editor/FlowCanvas.tsx b/src/components/FlowEditor/editor/FlowCanvas.tsx index 5a8010b..77ad2c8 100644 --- a/src/components/FlowEditor/editor/FlowCanvas.tsx +++ b/src/components/FlowEditor/editor/FlowCanvas.tsx @@ -20,6 +20,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext'; import { AiBadge } from '../nodes/shared/AiBadge'; import { switchOutputLabel } from '../nodes/shared/graphUtils'; +const LOG = '[FlowCanvas]'; + export interface CanvasNode { id: string; type: string; @@ -842,6 +844,8 @@ export const FlowCanvas = forwardRef(function const onHistoryCheckpointRef = useRef(onHistoryCheckpoint); onHistoryCheckpointRef.current = onHistoryCheckpoint; + const onSelectionChangeRef = useRef(onSelectionChange); + onSelectionChangeRef.current = onSelectionChange; const emitHistoryCheckpoint = useCallback(() => { onHistoryCheckpointRef.current?.(); @@ -1019,12 +1023,19 @@ export const FlowCanvas = forwardRef(function ] ); + const lastEmittedSelectionRef = useRef<{ nodeId: string | null; signature: string | null }>({ + nodeId: null, + signature: null, + }); + useEffect(() => { - if (onSelectionChange) { - const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null; - onSelectionChange(node); - } - }, [selectedNodeId, nodes, onSelectionChange]); + const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null; + const signature = node ? JSON.stringify(node) : null; + const last = lastEmittedSelectionRef.current; + if (last.nodeId === selectedNodeId && last.signature === signature) return; + lastEmittedSelectionRef.current = { nodeId: selectedNodeId, signature }; + onSelectionChangeRef.current?.(node); + }, [selectedNodeId, nodes]); const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => { e.stopPropagation(); @@ -1088,6 +1099,11 @@ export const FlowCanvas = forwardRef(function const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); + console.debug(`${LOG} drop received`, { + types: Array.from(e.dataTransfer.types), + clientX: e.clientX, + clientY: e.clientY, + }); // 1) externe Drop-Targets (z. B. ``application/json+workflow`` aus UDB-FilesTab) if (onExternalDrop) { const reservedMimes = new Set([ @@ -1113,16 +1129,35 @@ export const FlowCanvas = forwardRef(function } // 2) Standard: Node-Type aus der NodeSidebar const raw = e.dataTransfer.getData('application/json'); - if (!raw || !containerRef.current) return; + if (!raw || !containerRef.current) { + console.debug(`${LOG} drop ignored`, { + hasRaw: Boolean(raw), + hasContainer: Boolean(containerRef.current), + }); + return; + } try { const { type } = JSON.parse(raw); const el = containerRef.current; const rect = el.getBoundingClientRect(); const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2; const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2; + console.debug(`${LOG} placing node from drop`, { + type, + raw, + dropX: x, + dropY: y, + panOffset, + zoom, + }); onDropNodeType(type, Math.max(0, x), Math.max(0, y)); emitHistoryCheckpoint(); - } catch (_) {} + } catch (error) { + console.debug(`${LOG} drop parse failed`, { + raw, + error, + }); + } }, [onDropNodeType, onExternalDrop, panOffset, zoom, emitHistoryCheckpoint] );