diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 695dc16..3aba1b0 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -308,13 +308,14 @@ onMounted(() => { // Setup window resize handler window.addEventListener("resize", resizeFlow); - // Only fit view initially if there are no existing nodes with positions + // FIXED: Only fit view initially if there are no existing nodes OR if nodes have no positions // This prevents repositioning existing nodes when the canvas mounts const hasExistingNodes = props.initialNodes.length > 0; const hasPositions = props.initialNodes.some( (node) => node.position && (node.position.x !== 0 || node.position.y !== 0) ); + // Only auto-fit if we have no nodes, or nodes have default/zero positions if (!hasExistingNodes || !hasPositions) { setTimeout(() => { fitView(); @@ -498,16 +499,18 @@ watch( addNodes([...nodesToAdd]); // Create a copy to avoid reactivity issues } - // Update existing nodes that have changed + // Update existing nodes that have changed - STRICTLY PRESERVE CANVAS POSITIONS newNodes.forEach((newNode) => { const existingNode = nodes.value.find((n) => n.id === newNode.id); if (existingNode) { - // Always update the existing node to ensure latest data is reflected - // This is critical for property changes to be visible in the canvas + // FIXED: NEVER update positions for existing nodes during sync + // The canvas is the source of truth for positions once a node exists + // Only update data and label, preserve the current canvas position Object.assign(existingNode, { label: newNode.label, data: { ...newNode.data }, - position: { ...newNode.position }, + // ALWAYS keep the current canvas position - never overwrite from store + position: existingNode.position, }); // Always call updateNodeInternals to force re-render @@ -516,8 +519,9 @@ watch( } }); - // Fit view only if we added new nodes and this is significant change - if (nodesToAdd.length > 0) { + // FIXED: Don't auto-fit view when adding individual nodes to prevent repositioning + // Only fit view if this is a bulk operation (like loading a process) with many nodes + if (nodesToAdd.length > 3) { await nextTick(); setTimeout(() => { fitView(); @@ -869,16 +873,16 @@ const onDrop = (event) => { newNode.connectable = false; } - // Add to Vue Flow for immediate visual feedback - addNodes([newNode]); - - // IMPORTANT: Also emit the node to be added to the process store - // This ensures the node persists in the application state and can be saved + // FIXED: Only emit to parent, don't add directly to canvas + // The parent will add to store, which will trigger canvas update through watchers + // This prevents double node creation (canvas + store) emit( "nodesChange", [{ type: "add", id: newNode.id, item: newNode }], nodes.value ); + + console.log('📦 Canvas: Drop handled, emitted to parent:', newNode.id); } catch (error) { console.error("Error handling drop:", error); } @@ -1414,15 +1418,18 @@ function syncCanvas(newNodes, newEdges) { addNodes([...nodesToAdd]); } - // Update existing nodes - force update all properties + // Update existing nodes - STRICTLY PRESERVE CANVAS POSITIONS newNodes.forEach((newNode) => { const existingNode = nodes.value.find((n) => n.id === newNode.id); if (existingNode) { - // Always update to ensure property changes are reflected + // FIXED: NEVER update positions for existing nodes during manual sync + // The canvas is the source of truth for positions once a node exists + // Only update data and label, preserve the current canvas position Object.assign(existingNode, { label: newNode.label, data: { ...newNode.data }, - position: { ...newNode.position }, + // ALWAYS keep the current canvas position - never overwrite from store + position: existingNode.position, }); // Always force re-render for property changes updateNodeInternals([newNode.id]); diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 85a7413..9597ea8 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -1360,12 +1360,8 @@ const handleConditionUpdate = async (conditions) => { const onNodesChange = (changes, currentNodes) => { if (!changes || !currentNodes) return; - // Skip processing during component addition to avoid conflicts - if (isAddingComponent.value) { - return; - } - - // Handle position changes (only when dragging is complete) + // Handle position changes FIRST (always allow position updates) + // This ensures user drag operations are saved to store const positionChanges = {}; const positionChangesList = changes .filter(change => change.type === 'position' && change.position && !change.dragging); @@ -1375,9 +1371,17 @@ const onNodesChange = (changes, currentNodes) => { }); if (Object.keys(positionChanges).length > 0) { + console.log('📍 User moved nodes - updating store:', Object.keys(positionChanges)); processStore.updateNodePositions(positionChanges); } + // Skip other processing during component addition to avoid conflicts + // But ALWAYS allow position updates above + if (isAddingComponent.value) { + console.log('🚫 Skipping other node changes during component addition'); + return; + } + // Handle node removals (from delete key or other removal actions) const removedNodes = changes .filter(change => change.type === 'remove') @@ -1394,14 +1398,37 @@ const onNodesChange = (changes, currentNodes) => { } } - // Handle node additions (this should be rare since we add nodes through the store first) - const addedNodes = changes - .filter(change => change.type === 'add') - .map(change => change.id); + // FIXED: Handle node additions from canvas drops (drag and drop from component panel) + const addedNodes = changes.filter(change => change.type === 'add'); if (addedNodes.length > 0) { - // These nodes are already in the canvas, so we don't need to add them to the store - // unless they're not already there + addedNodes.forEach(async (change) => { + // Check if this node came from a canvas drop event (has item property) + if (change.item) { + console.log('📦 Parent: Received drop event for node:', change.item.id); + + // Set flag to prevent interference + isAddingComponent.value = true; + + try { + // Add the node to store only (canvas already has it from the drop) + const addedNode = await processStore.addNode(change.item); + + if (addedNode) { + // Select the newly added node + onNodeSelected(addedNode); + console.log('✅ Parent: Successfully added dropped node to store:', addedNode.id); + } + } catch (error) { + console.error('❌ Parent: Error adding dropped node to store:', error); + } finally { + // Reset flag after a delay to ensure stability + setTimeout(() => { + isAddingComponent.value = false; + }, 200); + } + } + }); } // REMOVED: Don't overwrite selectedNodeData from canvas changes to preserve local edits @@ -1804,7 +1831,7 @@ const onAddComponent = async (component) => { try { isAddingComponent.value = true; - console.log('🎯 Adding component:', component.type); + console.log('🎯 Component Panel: Adding component:', component.type); // Create a new node from the component definition // The component structure from ProcessBuilderComponents is: @@ -1870,50 +1897,32 @@ const onAddComponent = async (component) => { }, 100); } - // Add the node to the process store - console.log('📝 Adding node to store:', newNode.id); + // UPDATED: Add to store first, canvas sync will happen automatically through watchers + console.log('📝 Component Panel: Adding node to store:', newNode.id); const addedNode = await processStore.addNode(newNode); if (!addedNode) { - console.error('❌ Failed to add node to store'); + console.error('❌ Component Panel: Failed to add node to store'); toast.error('Failed to add component to store. Please try again.'); return; } - // Wait for store update and next render cycle + // Wait for store update and canvas sync through watchers await nextTick(); - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Select the newly added node after it's stable + onNodeSelected(addedNode); + + console.log('✅ Component Panel: Successfully added node:', addedNode.id, 'at position:', addedNode.position); - // Add the node directly to the canvas using Vue Flow's addNode method - if (processFlowCanvas.value) { - try { - console.log('🎨 Adding node to canvas:', addedNode.id); - - // Use Vue Flow's addNode method directly to avoid any sync issues - if (processFlowCanvas.value.addNode) { - processFlowCanvas.value.addNode(addedNode); - } - - // Wait for the node to be added to the canvas - await nextTick(); - await new Promise(resolve => setTimeout(resolve, 100)); - - // Select the newly added node after it's stable - onNodeSelected(addedNode); - - console.log('✅ Successfully added new node:', addedNode.id, 'at position:', addedNode.position); - } catch (error) { - console.error('❌ Error adding node to canvas:', error); - toast.error('Failed to add component to canvas. Please try again.'); - } - } } catch (error) { - console.error('❌ Error adding component:', error); + console.error('❌ Component Panel: Error adding component:', error); toast.error('Failed to add component. Please try again.'); } finally { // Reset the flag after a longer delay to ensure canvas is stable setTimeout(() => { - console.log('🏁 Component addition completed, resetting flag'); + console.log('🏁 Component Panel: Addition completed, resetting flag'); isAddingComponent.value = false; }, 300); }