Refactor Process Flow Canvas and Node Handling for Improved Stability

- Updated ProcessFlowCanvas.vue to ensure that existing node positions are preserved during updates, preventing unintended repositioning.
- Enhanced node addition logic in index.vue to handle drops from the canvas correctly, ensuring that nodes are added to the store without duplicating entries.
- Improved user experience by allowing position updates during drag operations while preventing conflicts during component additions.
- Added console logs for better debugging and tracking of node operations.
This commit is contained in:
Md Afiq Iskandar 2025-07-28 10:09:19 +08:00
parent 312e555361
commit ee91cd6c56
2 changed files with 74 additions and 58 deletions

View File

@ -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]);

View File

@ -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);
}