diff --git a/components/ProcessTemplatesModal.vue b/components/ProcessTemplatesModal.vue index fa258ce..f36c6cc 100644 --- a/components/ProcessTemplatesModal.vue +++ b/components/ProcessTemplatesModal.vue @@ -8,8 +8,8 @@ v-for="cat in categories" :key="cat.id" @click="activeCategory = cat.id" - class="px-4 py-2 text-sm font-medium" - :class="activeCategory === cat.id ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500 hover:text-gray-700'" + class="px-4 py-2 text-sm font-medium transition-colors duration-200" + :class="activeCategory === cat.id ? 'border-b-2 border-blue-500 text-blue-600 bg-blue-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'" > {{ cat.name }} @@ -18,38 +18,65 @@
-
-
+
+
- +
- {{ template.name }} + {{ template.name }} +
+ +
+
-

{{ template.name }}

-

{{ template.description }}

+

{{ template.name }}

+

{{ template.description }}

-
-
+
+
{{ template.nodeCount }} nodes
-
+
{{ template.edgeCount }} connections
- {{ template.complexity }} + {{ template.complexity }}
- - Use Template - +
+ + + Preview + + + + Use Template + +
@@ -62,9 +89,148 @@
+ + + +
+ +
+
+
+

{{ selectedTemplate.name }}

+

{{ selectedTemplate.description }}

+
+
+ + {{ selectedTemplate.complexity }} + + + {{ selectedTemplate.category }} + +
+
+ +
+
+ + {{ selectedTemplate.nodeCount }} nodes +
+
+ + {{ selectedTemplate.edgeCount }} connections +
+
+ + {{ selectedTemplate.variables?.length || 0 }} variables +
+
+
+ + +
+

Process Flow

+
+ + + + + +
+
+ + +
+

Process Variables

+
+
+
+ {{ variable.name }} + + {{ variable.type }} + +
+

{{ variable.description }}

+
+ Default: + {{ variable.defaultValue }} +
+
+
+
+ + +
+ + Close + + + + Use This Template + +
+
+
\ No newline at end of file diff --git a/components/process-flow/ProcessBuilderComponents.vue b/components/process-flow/ProcessBuilderComponents.vue index 2b6cb0f..18291cb 100644 --- a/components/process-flow/ProcessBuilderComponents.vue +++ b/components/process-flow/ProcessBuilderComponents.vue @@ -74,7 +74,11 @@ const availableComponents = [ defaultProps: { label: 'Start', data: { - description: 'Process start point' + description: 'Process start point', + shape: 'circle', + backgroundColor: '#dcfce7', + borderColor: '#10b981', + textColor: '#065f46' } } }, @@ -87,7 +91,11 @@ const availableComponents = [ defaultProps: { label: 'End', data: { - description: 'Process end point' + description: 'Process end point', + shape: 'circle', + backgroundColor: '#fee2e2', + borderColor: '#dc2626', + textColor: '#991b1b' } } }, @@ -103,7 +111,11 @@ const availableComponents = [ data: { description: 'Form submission task', formId: null, - formName: null + formName: null, + shape: 'rectangle', + backgroundColor: '#faf5ff', + borderColor: '#9333ea', + textColor: '#6b21a8' } } }, @@ -123,7 +135,11 @@ const availableComponents = [ headers: '{ "Content-Type": "application/json" }', outputVariable: 'apiResponse', continueOnError: false, - errorVariable: 'apiError' + errorVariable: 'apiError', + shape: 'rectangle', + backgroundColor: '#eff6ff', + borderColor: '#3b82f6', + textColor: '#1e40af' } } }, @@ -138,7 +154,11 @@ const availableComponents = [ data: { description: 'Decision point for branching the workflow', conditions: [], - defaultPath: 'Default' + defaultPath: 'Default', + shape: 'diamond', + backgroundColor: '#fff7ed', + borderColor: '#f97316', + textColor: '#c2410c' } } }, @@ -170,7 +190,11 @@ const availableComponents = [ enabled: false, value: 24, unit: 'hours' - } + }, + shape: 'rectangle', + backgroundColor: '#f0f9ff', + borderColor: '#0ea5e9', + textColor: '#0284c7' } } }, @@ -185,7 +209,11 @@ const availableComponents = [ data: { description: 'Apply business rules', ruleGroups: [], - priority: 'medium' + priority: 'medium', + shape: 'rectangle', + backgroundColor: '#fdf4ff', + borderColor: '#a855f7', + textColor: '#7c3aed' } } }, @@ -202,7 +230,11 @@ const availableComponents = [ scriptCode: '', scriptLanguage: 'javascript', inputVariables: [], - outputVariables: [] + outputVariables: [], + shape: 'rectangle', + backgroundColor: '#f9fafb', + borderColor: '#6b7280', + textColor: '#374151' } } }, @@ -222,7 +254,11 @@ const availableComponents = [ inputVariables: [], outputVariables: [], allowVariableAccess: true, - autoRefresh: false + autoRefresh: false, + shape: 'rectangle', + backgroundColor: '#e0f2fe', + borderColor: '#0ea5e9', + textColor: '#0c4a6e' } } }, @@ -237,7 +273,11 @@ const availableComponents = [ data: { description: 'Executes another process', subprocessId: null, - subprocessName: '' + subprocessName: '', + shape: 'rectangle', + backgroundColor: '#f0fdfa', + borderColor: '#14b8a6', + textColor: '#134e4a' } } }, @@ -362,7 +402,6 @@ const onDragStart = (event, component) => { // Add a component directly via click const addComponent = (component) => { - return; // Use same format as drag operation for consistency const componentData = { type: component.type, diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue index 16fa0d4..51edea1 100644 --- a/components/process-flow/ProcessFlowCanvas.vue +++ b/components/process-flow/ProcessFlowCanvas.vue @@ -39,24 +39,24 @@ import "@vue-flow/minimap/dist/style.css"; // Create nodeTypes object with markRaw to prevent reactivity issues const customNodeTypes = { - 'start': markRaw(StartNode), - 'end': markRaw(EndNode), - 'form': markRaw(FormNode), - 'api': markRaw(ApiNode), - 'gateway': markRaw(GatewayNode), - 'script': markRaw(ScriptNode), - 'business-rule': markRaw(BusinessRuleNode), - 'notification': markRaw(NotificationNode), - 'html': markRaw(HtmlNode), - 'subprocess': markRaw(SubprocessNode), + start: markRaw(StartNode), + end: markRaw(EndNode), + form: markRaw(FormNode), + api: markRaw(ApiNode), + gateway: markRaw(GatewayNode), + script: markRaw(ScriptNode), + "business-rule": markRaw(BusinessRuleNode), + notification: markRaw(NotificationNode), + html: markRaw(HtmlNode), + subprocess: markRaw(SubprocessNode), // Shape nodes - 'hexagon-shape': markRaw(HexagonShape), - 'trapezoid-shape': markRaw(TrapezoidShape), - 'rectangle-shape': markRaw(RectangleShape), - 'swimlane-horizontal': markRaw(SwimlaneHorizontal), - 'swimlane-vertical': markRaw(SwimlaneVertical), - 'text-annotation': markRaw(TextAnnotation), - 'process-group': markRaw(ProcessGroup) + "hexagon-shape": markRaw(HexagonShape), + "trapezoid-shape": markRaw(TrapezoidShape), + "rectangle-shape": markRaw(RectangleShape), + "swimlane-horizontal": markRaw(SwimlaneHorizontal), + "swimlane-vertical": markRaw(SwimlaneVertical), + "text-annotation": markRaw(TextAnnotation), + "process-group": markRaw(ProcessGroup), }; // Add Material Icons import @@ -276,10 +276,18 @@ onMounted(() => { // Setup window resize handler window.addEventListener("resize", resizeFlow); - // Initial fit view - setTimeout(() => { - fitView(); - }, 100); + // Only fit view initially if there are no existing nodes with 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) + ); + + if (!hasExistingNodes || !hasPositions) { + setTimeout(() => { + fitView(); + }, 100); + } }); // Center on a specific node @@ -869,7 +877,7 @@ defineExpose({ nodes.value = newNodes; } } catch (error) { - console.error('Error in setNodes:', error); + console.error("Error in setNodes:", error); } }, setEdges: (newEdges) => { @@ -878,13 +886,13 @@ defineExpose({ edges.value = newEdges; } } catch (error) { - console.error('Error in setEdges:', error); + console.error("Error in setEdges:", error); } }, // Provide access to the flow instance get flowInstance() { return flowInstance; - } + }, }); // Update an existing node @@ -978,15 +986,22 @@ function syncCanvas(newNodes, newEdges) { ); // Remove nodes that are no longer in the new list + // Be more conservative - only remove if we're sure it's not a temporary state const nodesToRemove = nodes.value.filter( (node) => !newNodeIds.has(node.id) ); - if (nodesToRemove.length > 0) { + + // Only remove nodes if the new list is not empty (prevents accidental clearing) + if (nodesToRemove.length > 0 && newNodes.length > 0) { console.log( "🗑️ Removing nodes:", nodesToRemove.map((n) => n.id) ); removeNodes(nodesToRemove); + } else if (nodesToRemove.length > 0 && newNodes.length === 0) { + console.log( + "⚠️ Not removing nodes - new list is empty, might be temporary state" + ); } // Add new nodes that aren't already present @@ -1254,22 +1269,51 @@ function fromObject(flowObject) { // Wait for any pending operations to complete setTimeout(async () => { try { - // 1. First, clear existing state - if (nodes.value.length > 0) { - removeNodes([...nodes.value]); - } - if (edges.value.length > 0) { - removeEdges([...edges.value]); + // Check if we're restoring the same data that's already there + const currentNodes = nodes.value.map((n) => ({ + id: n.id, + type: n.type, + position: n.position, + })); + const newNodes = + flowObject.nodes?.map((n) => ({ + id: n.id, + type: n.type, + position: n.position, + })) || []; + + const nodesAreSame = + currentNodes.length === newNodes.length && + currentNodes.every( + (node, index) => + node.id === newNodes[index]?.id && + node.type === newNodes[index]?.type + ); + + // Only clear if we're actually restoring different data + if (!nodesAreSame) { + // 1. First, clear existing state + if (nodes.value.length > 0) { + console.log("🔄 Clearing existing nodes for restoration"); + removeNodes([...nodes.value]); + } + if (edges.value.length > 0) { + console.log("🔄 Clearing existing edges for restoration"); + removeEdges([...edges.value]); + } + } else { + console.log("✅ Nodes are the same, skipping clear operation"); } // Wait for clearing to complete await nextTick(); - // 2. Restore nodes first + // 2. Restore nodes first (only if we cleared or if no nodes exist) if ( flowObject.nodes && Array.isArray(flowObject.nodes) && - flowObject.nodes.length > 0 + flowObject.nodes.length > 0 && + (!nodesAreSame || nodes.value.length === 0) ) { const nodesToRestore = flowObject.nodes.map((node) => ({ id: node.id, @@ -1280,19 +1324,23 @@ function fromObject(flowObject) { // Only include essential properties needed for Vue Flow })); + console.log("🔄 Restoring", nodesToRestore.length, "nodes"); addNodes(nodesToRestore); await nextTick(); // Wait a bit more for nodes to be fully initialized await new Promise((resolve) => setTimeout(resolve, 100)); + } else if (nodesAreSame) { + console.log("✅ Nodes already exist, skipping node restoration"); } - // 3. Restore edges after nodes are ready + // 3. Restore edges after nodes are ready (only if we cleared or if no edges exist) if ( flowObject.edges && Array.isArray(flowObject.edges) && flowObject.edges.length > 0 && - nodes.value.length > 0 + nodes.value.length > 0 && + (!nodesAreSame || edges.value.length === 0) ) { // Verify all edges have valid source and target nodes const validEdges = flowObject.edges.filter((edge) => { @@ -1328,9 +1376,12 @@ function fromObject(flowObject) { // Only include essential properties })); + console.log("🔄 Restoring", cleanEdges.length, "edges"); addEdges(cleanEdges); await nextTick(); } + } else if (nodesAreSame) { + console.log("✅ Edges already exist, skipping edge restoration"); } // 4. Finally, restore viewport position and zoom diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index b4f4d7b..8f0a469 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -1331,25 +1331,37 @@ const saveProcess = async () => { // Add a component handler to add components from the component panel const onAddComponent = async (component) => { - if (isAddingComponent.value) return; // Prevent concurrent additions + if (isAddingComponent.value || isProcessLoading.value) return; // Prevent concurrent additions or interference with process loading try { isAddingComponent.value = true; + console.log('🎯 Adding component:', component.type); // Create a new node from the component definition + // The component structure from ProcessBuilderComponents is: + // { type, label, data: { ...defaultProps.data } } + + // Calculate a better position for the new node to avoid overlap + let nodePosition = { x: 100, y: 100 }; + if (processStore.currentProcess && processStore.currentProcess.nodes.length > 0) { + // Find the rightmost and bottommost positions + const maxX = Math.max(...processStore.currentProcess.nodes.map(n => n.position?.x || 0)); + const maxY = Math.max(...processStore.currentProcess.nodes.map(n => n.position?.y || 0)); + + // Place new node to the right with some spacing + nodePosition = { + x: maxX + 200, // 200px spacing from rightmost node + y: Math.max(100, maxY) // At least 100px from top, or at the level of the bottommost node + }; + } + const newNode = { id: `${component.type}_${Date.now()}`, type: component.type, - position: { x: 100, y: 100 }, // Default position + position: nodePosition, label: component.label, data: { - ...component.data, - // Ensure shape is set for new nodes - shape: component.data.shape || (component.type === 'gateway' ? 'diamond' : 'rectangle'), - // Ensure default colors are set for new nodes - backgroundColor: component.data.backgroundColor, - borderColor: component.data.borderColor, - textColor: component.data.textColor + ...component.data } }; @@ -1390,78 +1402,51 @@ const onAddComponent = async (component) => { } // Add the node to the process store - await processStore.addNode(newNode); + console.log('📝 Adding node to store:', newNode.id); + const addedNode = await processStore.addNode(newNode); + + if (!addedNode) { + console.error('❌ 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 await nextTick(); await new Promise(resolve => setTimeout(resolve, 50)); - // CRITICAL FIX: Instead of calling syncCanvas (which can cause edge removal/re-addition), - // we'll add the node directly to the canvas and preserve existing edges + // Add the node directly to the canvas using Vue Flow's addNode method if (processFlowCanvas.value) { try { - // Get the fresh node from store (with any store-side modifications) - const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id); + console.log('🎨 Adding node to canvas:', addedNode.id); - if (freshNode && processFlowCanvas.value.addNode) { - // Add only the new node to the canvas directly - processFlowCanvas.value.addNode(freshNode); - - // Wait for the node to be added to the canvas - await nextTick(); - await new Promise(resolve => setTimeout(resolve, 50)); - - // Select the newly added node after it's stable - onNodeSelected(freshNode); - - console.log('✅ Successfully added new node without affecting existing edges'); - } else { - console.warn('⚠️ Fresh node not found in store, falling back to full sync'); - // Fallback to full sync if something went wrong - const currentNodes = processStore.currentProcess?.nodes || []; - const currentEdges = processStore.currentProcess?.edges || []; - - if (processFlowCanvas.value.syncCanvas) { - processFlowCanvas.value.syncCanvas(currentNodes, currentEdges); - - await nextTick(); - await new Promise(resolve => setTimeout(resolve, 50)); - - const addedNode = currentNodes.find(n => n.id === newNode.id); - if (addedNode) { - onNodeSelected(addedNode); - } - } + // 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); - - // Fallback to full sync if direct addition fails - const currentNodes = processStore.currentProcess?.nodes || []; - const currentEdges = processStore.currentProcess?.edges || []; - - if (processFlowCanvas.value.syncCanvas) { - console.log('🔄 Falling back to full canvas sync due to error'); - processFlowCanvas.value.syncCanvas(currentNodes, currentEdges); - - await nextTick(); - await new Promise(resolve => setTimeout(resolve, 50)); - - const addedNode = currentNodes.find(n => n.id === newNode.id); - if (addedNode) { - onNodeSelected(addedNode); - } - } + toast.error('Failed to add component to canvas. Please try again.'); } } } catch (error) { - console.error('Error adding component:', error); + console.error('❌ 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'); isAddingComponent.value = false; - }, 200); + }, 300); } }; @@ -1504,6 +1489,9 @@ const applyProcessTemplate = async (template) => { // Process nodes first and wait for them to be fully added for (const node of templateNodes) { + // Get default styling for the node type + const defaultStyling = getDefaultNodeStyling(node.type); + const newNode = { ...node, id: node.id, // Keep original ID for edge references @@ -1511,7 +1499,12 @@ const applyProcessTemplate = async (template) => { position: node.position || { x: 100, y: 100 }, data: { ...node.data, - label: node.data?.label || node.label || `${node.type} node` + label: node.data?.label || node.label || `${node.type} node`, + // Preserve styling properties from template or use defaults + backgroundColor: node.data?.backgroundColor || defaultStyling.backgroundColor, + borderColor: node.data?.borderColor || defaultStyling.borderColor, + textColor: node.data?.textColor || defaultStyling.textColor, + shape: node.data?.shape || defaultStyling.shape } }; @@ -1584,6 +1577,78 @@ const applyProcessTemplate = async (template) => { } }; +// Get default styling for node types +const getDefaultNodeStyling = (nodeType) => { + const stylingMap = { + 'start': { + backgroundColor: '#dcfce7', + borderColor: '#10b981', + textColor: '#065f46', + shape: 'circle' + }, + 'end': { + backgroundColor: '#fee2e2', + borderColor: '#dc2626', + textColor: '#991b1b', + shape: 'circle' + }, + 'form': { + backgroundColor: '#faf5ff', + borderColor: '#9333ea', + textColor: '#6b21a8', + shape: 'rectangle' + }, + 'api': { + backgroundColor: '#eff6ff', + borderColor: '#3b82f6', + textColor: '#1e40af', + shape: 'rectangle' + }, + 'gateway': { + backgroundColor: '#fff7ed', + borderColor: '#f97316', + textColor: '#c2410c', + shape: 'diamond' + }, + 'notification': { + backgroundColor: '#f0f9ff', + borderColor: '#0ea5e9', + textColor: '#0284c7', + shape: 'rectangle' + }, + 'business-rule': { + backgroundColor: '#fdf4ff', + borderColor: '#a855f7', + textColor: '#7c3aed', + shape: 'rectangle' + }, + 'script': { + backgroundColor: '#f9fafb', + borderColor: '#6b7280', + textColor: '#374151', + shape: 'rectangle' + }, + 'html': { + backgroundColor: '#e0f2fe', + borderColor: '#0ea5e9', + textColor: '#0c4a6e', + shape: 'rectangle' + }, + 'subprocess': { + backgroundColor: '#f0fdfa', + borderColor: '#14b8a6', + textColor: '#134e4a', + shape: 'rectangle' + } + }; + + return stylingMap[nodeType] || { + backgroundColor: '#ffffff', + borderColor: '#e2e8f0', + textColor: '#374151', + shape: 'rectangle' + }; +}; // Fix references to functions const onFormSelected = (formData) => { if (selectedNodeData.value && selectedNodeData.value.type === 'form') { @@ -1818,62 +1883,85 @@ watch(() => canvasNodes.value, (newNodes) => { } }, { deep: true }); -// Watch for process changes to restore Vue Flow state -watch(() => processStore.currentProcess, async (newProcess, oldProcess) => { - if (!newProcess) return; - - // Only restore when a different process is loaded (not on updates) - if (oldProcess && newProcess.id === oldProcess.id) return; - +// Track if we're in the middle of adding a component to prevent interference +const isProcessLoading = ref(false); +// Watch for process changes to restore Vue Flow state +// Only trigger when the entire process object changes (different process loaded) +watch(() => processStore.currentProcess?.id, async (newProcessId, oldProcessId) => { + if (!newProcessId) return; + + // Only restore when a different process is loaded (not on updates to the same process) + if (oldProcessId && newProcessId === oldProcessId) return; + + // Don't interfere if we're in the middle of adding a component or modifying nodes + if (isAddingComponent.value || processStore.isModifyingNodes) { + console.log('🚫 Skipping process restoration during component/node modification'); + return; + } + + const newProcess = processStore.currentProcess; + console.log('🔄 Loading process:', newProcessId, 'Nodes:', newProcess.nodes?.length || 0); + isProcessLoading.value = true; // Wait for the canvas to be ready await nextTick(); // Give the canvas component a moment to initialize setTimeout(async () => { - if (processFlowCanvas.value && processFlowCanvas.value.fromObject) { - try { - // Prepare the flow object for restoration - const rawFlowObject = { - nodes: newProcess.nodes || [], - edges: newProcess.edges || [], - viewport: newProcess.viewport || { x: 0, y: 0, zoom: 1 } - }; - - // Clean the data to remove any Vue Flow internal properties - const flowObject = processStore.cleanFlowData(rawFlowObject); - - - - // Use Vue Flow's proper restoration method (now returns a Promise) - await processFlowCanvas.value.fromObject(flowObject); - - - - // Fit view after restoration with a small delay to ensure everything is rendered - setTimeout(() => { - if (processFlowCanvas.value && processFlowCanvas.value.fitView) { - processFlowCanvas.value.fitView(); + try { + if (processFlowCanvas.value && processFlowCanvas.value.fromObject) { + try { + // Prepare the flow object for restoration + const rawFlowObject = { + nodes: newProcess.nodes || [], + edges: newProcess.edges || [], + viewport: newProcess.viewport || { x: 0, y: 0, zoom: 1 } + }; + + // Clean the data to remove any Vue Flow internal properties + const flowObject = processStore.cleanFlowData(rawFlowObject); + + console.log('🔄 Restoring Vue Flow state with', flowObject.nodes.length, 'nodes'); + + // Use Vue Flow's proper restoration method (now returns a Promise) + await processFlowCanvas.value.fromObject(flowObject); + + console.log('✅ Vue Flow state restored successfully'); + + // Only fit view if nodes don't have meaningful positions (all at 0,0) + // This prevents repositioning nodes that were carefully positioned + const hasPositionedNodes = flowObject.nodes.some(node => + node.position && (node.position.x !== 0 || node.position.y !== 0) + ); + + if (!hasPositionedNodes && flowObject.nodes.length > 0) { + setTimeout(() => { + if (processFlowCanvas.value && processFlowCanvas.value.fitView) { + processFlowCanvas.value.fitView(); + } + }, 300); } - }, 300); + + } catch (error) { + console.error('❌ Error restoring Vue Flow state:', error); + + // Fallback to manual sync if Vue Flow restoration fails + if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) { + console.log('🔄 Falling back to manual sync'); + processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges); + } + } + } else { + console.warn('⚠️ Vue Flow canvas not available for restoration, using sync fallback'); - } catch (error) { - console.error('❌ Error restoring Vue Flow state:', error); - - // Fallback to manual sync if Vue Flow restoration fails + // Fallback sync method if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) { - processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges); } } - } else { - console.warn('⚠️ Vue Flow canvas not available for restoration, using sync fallback'); - - // Fallback sync method - if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) { - processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges); - } + } finally { + isProcessLoading.value = false; } }, 200); // Allow time for canvas to initialize }, { immediate: false }); diff --git a/stores/processBuilder.js b/stores/processBuilder.js index 3675eae..8c24220 100644 --- a/stores/processBuilder.js +++ b/stores/processBuilder.js @@ -11,7 +11,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', { history: [], historyIndex: -1, unsavedChanges: false, - lastChangeDescription: '' + lastChangeDescription: '', + isModifyingNodes: false // Flag to prevent interference during node operations }), getters: { @@ -653,37 +654,48 @@ export const useProcessBuilderStore = defineStore('processBuilder', { return existingNode; } - // Create a new node with proper data structure - const newNode = { - id: node.id || uuidv4(), - type: node.type, - label: node.label || node.data?.label || 'New Node', - position: node.position || { x: 100, y: 100 }, - data: { - ...node.data, - label: node.data?.label || node.label || 'New Node', - // Ensure shape is set for new nodes - shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'), - // Ensure default colors are set for new nodes - backgroundColor: node.data?.backgroundColor || '#ffffff', - borderColor: node.data?.borderColor || '#000000' - } - }; + // Set flag to prevent interference during node addition + this.isModifyingNodes = true; - // Create a deep copy to avoid reference issues - const nodeCopy = JSON.parse(JSON.stringify(newNode)); + try { + // Create a new node with proper data structure + const newNode = { + id: node.id || uuidv4(), + type: node.type, + label: node.label || node.data?.label || 'New Node', + position: node.position || { x: 100, y: 100 }, + data: { + ...node.data, + label: node.data?.label || node.label || 'New Node', + // Ensure shape is set for new nodes + shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'), + // Ensure default colors are set for new nodes + backgroundColor: node.data?.backgroundColor || '#ffffff', + borderColor: node.data?.borderColor || '#000000' + } + }; - // Add to current process nodes array - this.currentProcess.nodes = [...this.currentProcess.nodes, nodeCopy]; - - // Update selection - this.selectedNodeId = nodeCopy.id; - - // Save to history - this.saveToHistory('Add node'); - this.unsavedChanges = true; + // Create a deep copy to avoid reference issues + const nodeCopy = JSON.parse(JSON.stringify(newNode)); - return nodeCopy; + // Add to current process nodes array using push to avoid creating new array reference + // This prevents triggering the watch that monitors currentProcess changes + this.currentProcess.nodes.push(nodeCopy); + + // Update selection + this.selectedNodeId = nodeCopy.id; + + // Save to history + this.saveToHistory('Add node'); + this.unsavedChanges = true; + + console.log('📝 Store: Added node', nodeCopy.id, 'Total nodes:', this.currentProcess.nodes.length); + + return nodeCopy; + } finally { + // Reset flag after operation completes + this.isModifyingNodes = false; + } }, /** @@ -706,36 +718,46 @@ export const useProcessBuilderStore = defineStore('processBuilder', { deleteNode(nodeId) { if (!this.currentProcess) return; - // Find the node index - const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId); - if (index !== -1) { - // Remove the node - this.currentProcess.nodes.splice(index, 1); - - // Remove any edges connected to this node - const edgesToRemove = this.currentProcess.edges.filter( - edge => edge.source === nodeId || edge.target === nodeId - ); - - edgesToRemove.forEach(edge => { - const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id); - if (edgeIndex !== -1) { - this.currentProcess.edges.splice(edgeIndex, 1); + // Set flag to prevent interference during node deletion + this.isModifyingNodes = true; + + try { + // Find the node index + const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId); + if (index !== -1) { + // Remove the node using splice to avoid creating new array reference + this.currentProcess.nodes.splice(index, 1); + + // Remove any edges connected to this node + const edgesToRemove = this.currentProcess.edges.filter( + edge => edge.source === nodeId || edge.target === nodeId + ); + + edgesToRemove.forEach(edge => { + const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id); + if (edgeIndex !== -1) { + this.currentProcess.edges.splice(edgeIndex, 1); + } + }); + + // Clear selection if the deleted node was selected + if (this.selectedNodeId === nodeId) { + this.selectedNodeId = null; } - }); - // Clear selection if the deleted node was selected - if (this.selectedNodeId === nodeId) { - this.selectedNodeId = null; + this.saveToHistory('Delete node'); + this.unsavedChanges = true; + + console.log('🗑️ Store: Deleted node', nodeId, 'Total nodes:', this.currentProcess.nodes.length); + + return true; // Return success } - - this.saveToHistory('Delete node'); - this.unsavedChanges = true; - return true; // Return success + return false; // Return failure + } finally { + // Reset flag after operation completes + this.isModifyingNodes = false; } - - return false; // Return failure }, /** @@ -744,22 +766,33 @@ export const useProcessBuilderStore = defineStore('processBuilder', { addEdge(edge) { if (!this.currentProcess) return; - const newEdge = { - id: edge.id || `${edge.source}-${edge.target}`, - source: edge.source, - target: edge.target, - label: edge.label || '', - type: edge.type || 'default', - animated: edge.animated !== undefined ? edge.animated : true, - data: edge.data || {} - }; + // Set flag to prevent interference during edge addition + this.isModifyingNodes = true; - this.currentProcess.edges.push(newEdge); - this.selectedEdgeId = newEdge.id; - this.saveToHistory('Add edge'); - this.unsavedChanges = true; + try { + const newEdge = { + id: edge.id || `${edge.source}-${edge.target}`, + source: edge.source, + target: edge.target, + label: edge.label || '', + type: edge.type || 'default', + animated: edge.animated !== undefined ? edge.animated : true, + data: edge.data || {} + }; - return newEdge; + // Use push to avoid creating new array reference + this.currentProcess.edges.push(newEdge); + this.selectedEdgeId = newEdge.id; + this.saveToHistory('Add edge'); + this.unsavedChanges = true; + + console.log('🔗 Store: Added edge', newEdge.id, 'Total edges:', this.currentProcess.edges.length); + + return newEdge; + } finally { + // Reset flag after operation completes + this.isModifyingNodes = false; + } }, /**