import { defineStore } from 'pinia'; import { v4 as uuidv4 } from 'uuid'; import { useVariableStore } from './variableStore'; export const useProcessBuilderStore = defineStore('processBuilder', { state: () => ({ processes: [], // Only populated from database via fetchProcesses() currentProcess: null, selectedNodeId: null, selectedEdgeId: null, history: [], historyIndex: -1, unsavedChanges: false, lastChangeDescription: '', isModifyingNodes: false // Flag to prevent interference during node operations }), getters: { /** * Get the current process object */ process: (state) => { return state.currentProcess; }, /** * Get the selected node */ selectedNode: (state) => { if (!state.currentProcess || !state.selectedNodeId) return null; return state.currentProcess.nodes.find(node => node.id === state.selectedNodeId); }, /** * Get the selected edge */ selectedEdge: (state) => { if (!state.currentProcess || !state.selectedEdgeId) return null; return state.currentProcess.edges.find(edge => edge.id === state.selectedEdgeId); }, /** * Check if there are unsaved changes */ hasUnsavedChanges: (state) => { return state.unsavedChanges; }, /** * Check if undo is available */ canUndo: (state) => { return state.historyIndex > 0; }, /** * Check if redo is available */ canRedo: (state) => { return state.historyIndex < state.history.length - 1; } }, actions: { /** * Create a new process */ async createProcess(name, description = '') { try { const processData = { processName: name, processDescription: description, nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 }, variables: {}, settings: {}, permissions: {}, createdBy: 1 // TODO: Get from auth store }; const response = await $fetch('/api/process/create', { method: 'POST', body: processData }); if (response.success) { const process = { id: response.process.processUUID, name: response.process.processName, description: response.process.processDescription, nodes: response.process.processDefinition.nodes || [], edges: response.process.processDefinition.edges || [], variables: response.process.processVariables || {}, settings: response.process.processSettings || {}, permissions: response.process.processPermissions || {}, createdAt: response.process.processCreatedDate, updatedAt: response.process.processModifiedDate }; // Set as current process but DON'T add to processes array // The processes array should only be populated from fetchProcesses() this.currentProcess = process; this.unsavedChanges = false; // Clear any existing variables useVariableStore().clearProcessVariables(); return process; } else { throw new Error(response.error || 'Failed to create process'); } } catch (error) { console.error('Error creating process:', error); throw error; } }, /** * Load a process */ async loadProcess(processId) { try { const response = await $fetch(`/api/process/${processId}`); if (response.success) { const apiProcess = response.process; const definition = apiProcess.processDefinition; let nodes = definition.nodes || []; let edges = definition.edges || []; // If nodes array is empty but edges contain node data, extract nodes from edges if (nodes.length === 0 && edges.length > 0) { const nodeMap = new Map(); // First try to extract unique nodes from edge sourceNode and targetNode if they exist edges.forEach(edge => { if (edge.sourceNode) { nodeMap.set(edge.sourceNode.id, { id: edge.sourceNode.id, type: edge.sourceNode.type, label: edge.sourceNode.data?.label || edge.sourceNode.label, position: edge.sourceNode.position, data: edge.sourceNode.data || {} }); } if (edge.targetNode) { nodeMap.set(edge.targetNode.id, { id: edge.targetNode.id, type: edge.targetNode.type, label: edge.targetNode.data?.label || edge.targetNode.label, position: edge.targetNode.position, data: edge.targetNode.data || {} }); } }); // If no nodes were extracted from embedded data, create placeholder nodes from edge references if (nodeMap.size === 0) { const nodeIds = new Set(); edges.forEach(edge => { if (edge.source) nodeIds.add(edge.source); if (edge.target) nodeIds.add(edge.target); }); let nodePosition = { x: 100, y: 100 }; const spacing = 200; nodeIds.forEach((nodeId, index) => { // Determine node type from ID prefix let nodeType = 'task'; // default let label = nodeId; let icon = 'schedule'; if (nodeId.includes('start-')) { nodeType = 'start'; label = 'Start Point'; icon = 'play-circle-rounded'; } else if (nodeId.includes('end-')) { nodeType = 'end'; label = 'End Point'; icon = 'stop-circle'; } else if (nodeId.includes('form-')) { nodeType = 'form'; label = 'Form'; icon = 'description'; } else if (nodeId.includes('api-')) { nodeType = 'api'; label = 'API Call'; icon = 'api'; } else if (nodeId.includes('gateway-')) { nodeType = 'gateway'; label = 'Decision Point'; icon = 'call-split'; } else if (nodeId.includes('script-')) { nodeType = 'script'; label = 'Script'; icon = 'code'; } nodeMap.set(nodeId, { id: nodeId, type: nodeType, label: label, position: { x: nodePosition.x + (index % 3) * spacing, y: nodePosition.y + Math.floor(index / 3) * spacing }, data: { label: label, description: `Recovered ${nodeType} node`, icon: icon } }); }); console.warn('Process had edges but no nodes. Created placeholder nodes from edge references:', Array.from(nodeIds)); } // Convert to array nodes = Array.from(nodeMap.values()); // Clean up edges to remove embedded node data (Vue Flow doesn't need it) edges = edges.map(edge => ({ id: edge.id, source: edge.source, target: edge.target, label: edge.label || '', type: edge.type || 'smoothstep', animated: edge.animated !== undefined ? edge.animated : true, data: edge.data || {} })); } const process = { id: apiProcess.processUUID, name: apiProcess.processName, description: apiProcess.processDescription, nodes: nodes, edges: edges, viewport: definition.viewport || { x: 0, y: 0, zoom: 1 }, variables: apiProcess.processVariables || {}, settings: apiProcess.processSettings || {}, permissions: apiProcess.processPermissions || {}, createdAt: apiProcess.processCreatedDate, updatedAt: apiProcess.processModifiedDate }; this.currentProcess = process; // Variables are already loaded in process.variables, no need to sync with variable store this.unsavedChanges = false; return { success: true, process }; } else { const errorMessage = response.error || 'Failed to load process'; console.error('Load process failed:', errorMessage); return { success: false, error: errorMessage }; } } catch (error) { const errorMessage = error.data?.error || error.message || 'Network error occurred'; console.error('Error loading process:', errorMessage); return { success: false, error: errorMessage }; } }, /** * Set the current process from the processes list */ setCurrentProcess(processId) { const process = this.processes.find(p => p.id === processId); if (process) { this.currentProcess = JSON.parse(JSON.stringify(process)); // Deep clone this.selectedNodeId = null; this.selectedEdgeId = null; this.clearHistory(); this.unsavedChanges = false; } }, /** * Update the current process with new data */ updateCurrentProcess(processUpdates) { if (!this.currentProcess) return; this.currentProcess = { ...this.currentProcess, ...processUpdates, updatedAt: new Date().toISOString() }; this.unsavedChanges = true; this.saveToHistory('Update process settings'); }, /** * Save the current process */ async saveProcess() { if (!this.currentProcess) return false; try { // Get variables from the current process, not from variable store const processVariables = this.currentProcess.variables || {}; // Check if we need to capture Vue Flow state (when Vue Flow is available) let flowState = null; // Try to get the Vue Flow state from the canvas component // This will be set by the main process builder page when saving if (this.currentProcess.flowState) { flowState = this.currentProcess.flowState; console.log('💾 Using captured Vue Flow state for save:', { nodes: flowState.nodes?.length || 0, edges: flowState.edges?.length || 0, viewport: flowState.viewport }); } else { console.log('💾 No Vue Flow state captured, using store data'); } const processData = { processName: this.currentProcess.name, processDescription: this.currentProcess.description, // Use Vue Flow state if available, otherwise fallback to store data nodes: flowState?.nodes || this.currentProcess.nodes, edges: flowState?.edges || this.currentProcess.edges, viewport: flowState?.viewport || this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 }, variables: processVariables, settings: this.currentProcess.settings || {}, permissions: this.currentProcess.permissions || {} // Note: processStatus is intentionally NOT included here to preserve current status // Status changes should only happen through explicit publish/unpublish actions }; // Debug logging to see what we're actually sending console.log('💾 Saving process data:', { processId: this.currentProcess.id, nodeCount: processData.nodes.length, edgeCount: processData.edges.length, variableCount: Object.keys(processVariables).length, viewport: processData.viewport, nodes: processData.nodes.map(n => ({ id: n.id, type: n.type, label: n.label, position: n.position })), edges: processData.edges.map(e => ({ id: e.id, source: e.source, target: e.target })) }); const response = await $fetch(`/api/process/${this.currentProcess.id}`, { method: 'PUT', body: processData }); if (response.success) { // Update local state with server response const apiProcess = response.process; this.currentProcess.updatedAt = apiProcess.processModifiedDate; // Clear the temporary flowState after successful save delete this.currentProcess.flowState; // Update in processes array if it exists there const index = this.processes.findIndex(p => p.id === this.currentProcess.id); if (index !== -1) { this.processes[index] = { ...this.currentProcess }; } this.unsavedChanges = false; return true; } else { throw new Error(response.error || 'Failed to save process'); } } catch (error) { console.error('Error saving process:', error); return false; } }, /** * Set Vue Flow state for saving */ setFlowStateForSave(flowState) { if (this.currentProcess && flowState) { this.currentProcess.flowState = flowState; console.log('📊 Vue Flow state captured for save:', { nodes: flowState.nodes?.length || 0, edges: flowState.edges?.length || 0, viewport: flowState.viewport }); } }, /** * Clean flow data to remove Vue Flow internal properties */ cleanFlowData(flowData) { if (!flowData) return { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } }; // Clean nodes - keep only essential properties const cleanNodes = (flowData.nodes || []).map(node => ({ id: node.id, type: node.type, label: node.label || node.data?.label || '', position: node.position || { x: 0, y: 0 }, data: { label: node.data?.label || node.label || '', description: node.data?.description || '', // Include other essential data properties but exclude Vue Flow internals ...(node.data && typeof node.data === 'object' ? Object.fromEntries( Object.entries(node.data).filter(([key]) => !['events', 'dimensions', 'handleBounds', 'computedPosition'].includes(key) ) ) : {} ) } })); // Clean edges - keep only essential properties const cleanEdges = (flowData.edges || []).map(edge => ({ id: edge.id, source: edge.source, target: edge.target, sourceHandle: edge.sourceHandle, targetHandle: edge.targetHandle, label: edge.label || '', type: edge.type || 'smoothstep', animated: edge.animated !== undefined ? edge.animated : true, data: edge.data && typeof edge.data === 'object' ? { ...edge.data } : {} // Exclude: sourceNode, targetNode, sourceX, sourceY, targetX, targetY, events })); // Clean viewport const cleanViewport = { x: flowData.viewport?.x || 0, y: flowData.viewport?.y || 0, zoom: flowData.viewport?.zoom || 1 }; return { nodes: cleanNodes, edges: cleanEdges, viewport: cleanViewport }; }, /** * Delete a process */ async deleteProcess(processId) { try { const response = await $fetch(`/api/process/${processId}`, { method: 'DELETE' }); if (response.success) { // Remove from local processes array (since we're filtering out deleted ones) const index = this.processes.findIndex(p => p.id === processId); if (index !== -1) { this.processes.splice(index, 1); } // Clear current process if it's the one being deleted if (this.currentProcess && this.currentProcess.id === processId) { this.currentProcess = null; this.selectedNodeId = null; this.selectedEdgeId = null; this.clearHistory(); } return true; } else { throw new Error(response.error || 'Failed to delete process'); } } catch (error) { console.error('Error deleting process:', error); return false; } }, /** * Restore a deleted process */ async restoreProcess(processId) { try { const response = await $fetch(`/api/process/${processId}/restore`, { method: 'POST' }); if (response.success) { // Refresh the processes list to reflect the change // Don't modify local array directly since status filtering might be complex return true; } else { throw new Error(response.error || 'Failed to restore process'); } } catch (error) { console.error('Error restoring process:', error); throw error; } }, /** * Fetch all processes from database */ async fetchProcesses(options = {}) { try { const queryParams = new URLSearchParams(); if (options.page) queryParams.append('page', options.page); if (options.limit) queryParams.append('limit', options.limit); if (options.status) queryParams.append('status', options.status); if (options.category) queryParams.append('category', options.category); if (options.search) queryParams.append('search', options.search); if (options.isTemplate !== undefined) queryParams.append('isTemplate', options.isTemplate); if (options.sortBy) queryParams.append('sortBy', options.sortBy); if (options.sortOrder) queryParams.append('sortOrder', options.sortOrder); const url = `/api/process${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const response = await $fetch(url); if (response.success) { // Replace the entire processes array with fresh data from database this.processes = response.data.processes.map(apiProcess => ({ id: apiProcess.processUUID, name: apiProcess.processName, description: apiProcess.processDescription, category: apiProcess.processCategory, priority: apiProcess.processPriority, owner: apiProcess.processOwner, status: apiProcess.processStatus, isTemplate: apiProcess.isTemplate, templateCategory: apiProcess.templateCategory, createdAt: apiProcess.processCreatedDate, updatedAt: apiProcess.processModifiedDate, creator: apiProcess.creator })); return response.data; } else { throw new Error(response.error || 'Failed to fetch processes'); } } catch (error) { console.error('Error fetching processes:', error); throw error; } }, /** * Publish a process */ async publishProcess(processId) { try { const response = await $fetch(`/api/process/${processId}/publish`, { method: 'POST' }); if (response.success) { // Update local state if process exists in the array const process = this.processes.find(p => p.id === processId); if (process) { process.status = 'published'; process.updatedAt = response.process.processModifiedDate; } // Update current process if it's the same one if (this.currentProcess && this.currentProcess.id === processId) { this.currentProcess.status = 'published'; this.currentProcess.updatedAt = response.process.processModifiedDate; } return true; } else { throw new Error(response.error || 'Failed to publish process'); } } catch (error) { console.error('Error publishing process:', error); throw error; } }, /** * Duplicate a process */ async duplicateProcess(processId, newName = null, asTemplate = false) { try { const response = await $fetch(`/api/process/${processId}/duplicate`, { method: 'POST', body: { newName, asTemplate, createdBy: 1 // TODO: Get from auth store } }); if (response.success) { const apiProcess = response.process; const newProcess = { id: apiProcess.processUUID, name: apiProcess.processName, description: apiProcess.processDescription, category: apiProcess.processCategory, priority: apiProcess.processPriority, owner: apiProcess.processOwner, status: apiProcess.processStatus, isTemplate: apiProcess.isTemplate, templateCategory: apiProcess.templateCategory, createdAt: apiProcess.processCreatedDate, updatedAt: apiProcess.processModifiedDate, creator: apiProcess.creator }; // DON'T add to processes array - let fetchProcesses() handle that // The manage page should call fetchProcesses() after duplication return newProcess; } else { throw new Error(response.error || 'Failed to duplicate process'); } } catch (error) { console.error('Error duplicating process:', error); throw error; } }, /** * Clear the processes list (useful when switching contexts) */ clearProcesses() { this.processes = []; }, /** * Clear the current process (useful when starting fresh) */ clearCurrentProcess() { this.currentProcess = null; this.selectedNodeId = null; this.selectedEdgeId = null; this.clearHistory(); this.unsavedChanges = false; }, /** * Add a node to the current process */ addNode(node) { if (!this.currentProcess) return; // Check if node already exists to prevent duplicates const existingNode = this.currentProcess.nodes.find(n => n.id === node.id); if (existingNode) { console.warn('Node already exists in store:', node.id); return existingNode; } // Set flag to prevent interference during node addition this.isModifyingNodes = true; try { // Create a new node with proper data structure const newNode = { id: node.id || uuidv4(), type: node.type, label: node.label || node.data?.label || '', position: node.position || { x: 100, y: 100 }, data: { ...node.data, label: node.data?.label || node.label || '', // 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' } }; // Create a deep copy to avoid reference issues const nodeCopy = JSON.parse(JSON.stringify(newNode)); // 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; } }, /** * Update a node in the current process */ updateNode(nodeId, updates) { if (!this.currentProcess) return; const node = this.currentProcess.nodes.find(n => n.id === nodeId); if (node) { Object.assign(node, updates); this.saveToHistory('Update node'); this.unsavedChanges = true; } }, /** * Delete a node from the current process */ deleteNode(nodeId) { if (!this.currentProcess) return; // 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; } this.saveToHistory('Delete node'); this.unsavedChanges = true; console.log('🗑️ Store: Deleted node', nodeId, 'Total nodes:', this.currentProcess.nodes.length); return true; // Return success } return false; // Return failure } finally { // Reset flag after operation completes this.isModifyingNodes = false; } }, /** * Add an edge to the current process */ addEdge(edge) { if (!this.currentProcess) return; // Set flag to prevent interference during edge addition this.isModifyingNodes = 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 || {} }; // 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; } }, /** * Update an edge in the current process */ updateEdge(edgeIdOrObject, updates) { if (!this.currentProcess) return; // Handle different parameter formats let edgeId, edgeUpdates; if (typeof edgeIdOrObject === 'string') { // Called with (id, updates) edgeId = edgeIdOrObject; edgeUpdates = updates || {}; } else if (typeof edgeIdOrObject === 'object') { // Called with an edge object edgeId = edgeIdOrObject.id; if (updates) { // Called with (edge, updates) edgeUpdates = updates; } else { // Called with just the edge object containing updates edgeUpdates = { ...edgeIdOrObject }; delete edgeUpdates.id; // Don't update the ID } } else { return; // Invalid parameters } const edge = this.currentProcess.edges.find(e => e.id === edgeId); if (edge) { Object.assign(edge, edgeUpdates); this.saveToHistory('Update edge'); this.unsavedChanges = true; } }, /** * Delete an edge from the current process */ deleteEdge(edgeId) { if (!this.currentProcess) return; const index = this.currentProcess.edges.findIndex(e => e.id === edgeId); if (index !== -1) { this.currentProcess.edges.splice(index, 1); if (this.selectedEdgeId === edgeId) { this.selectedEdgeId = null; } this.saveToHistory('Delete edge'); this.unsavedChanges = true; } }, /** * Update node positions after drag */ updateNodePositions(nodePositions) { if (!this.currentProcess) return; Object.entries(nodePositions).forEach(([nodeId, position]) => { const node = this.currentProcess.nodes.find(n => n.id === nodeId); if (node) { node.position = position; } }); this.saveToHistory('Move nodes'); this.unsavedChanges = true; }, /** * Select a node */ selectNode(nodeId) { this.selectedNodeId = nodeId; this.selectedEdgeId = null; }, /** * Select an edge */ selectEdge(edgeId) { this.selectedEdgeId = edgeId; this.selectedNodeId = null; }, /** * Clear selection */ clearSelection() { this.selectedNodeId = null; this.selectedEdgeId = null; }, /** * Save the current state to history */ saveToHistory(actionName) { // Remove any future states if we're in the middle of the history if (this.historyIndex < this.history.length - 1) { this.history = this.history.slice(0, this.historyIndex + 1); } // Add current state to history this.history.push({ state: JSON.parse(JSON.stringify(this.currentProcess)), action: actionName, timestamp: new Date().toISOString() }); // Move history pointer this.historyIndex = this.history.length - 1; // Limit history size if (this.history.length > 50) { this.history.shift(); this.historyIndex--; } }, /** * Undo the last action */ undo() { if (this.historyIndex > 0) { this.historyIndex--; this.currentProcess = JSON.parse(JSON.stringify(this.history[this.historyIndex].state)); this.unsavedChanges = true; } }, /** * Redo the last undone action */ redo() { if (this.historyIndex < this.history.length - 1) { this.historyIndex++; this.currentProcess = JSON.parse(JSON.stringify(this.history[this.historyIndex].state)); this.unsavedChanges = true; } }, /** * Clear history */ clearHistory() { this.history = []; this.historyIndex = -1; }, /** * Get process version history */ async getProcessHistory(processId) { try { const response = await $fetch(`/api/process/${processId}/history`); if (response.success) { return response.data; } else { throw new Error(response.error || 'Failed to get process history'); } } catch (error) { console.error('Error getting process history:', error); throw error; } }, /** * Get specific process version details */ async getProcessVersion(processId, versionId) { try { const response = await $fetch(`/api/process/${processId}/version/${versionId}`); if (response.success) { return response.data; } else { throw new Error(response.error || 'Failed to get process version'); } } catch (error) { console.error('Error getting process version:', error); throw error; } }, /** * Restore process to a previous version */ async restoreProcessVersion(processId, version) { try { const requestData = { historyId: version.historyID, versionNumber: version.versionNumber, restoredBy: 1 // TODO: Get from auth store }; const response = await $fetch(`/api/process/${processId}/restore`, { method: 'POST', body: requestData }); if (response.success) { // Update local state with restored process if (this.currentProcess && this.currentProcess.id === processId) { await this.loadProcess(processId); } return response; } else { throw new Error(response.error || 'Failed to restore process version'); } } catch (error) { console.error('Error restoring process version:', error); throw error; } }, /** * Load process version for preview (without changing current process) */ async loadProcessVersionPreview(processId, versionId) { try { const response = await $fetch(`/api/process/${processId}/version/${versionId}`); if (response.success) { return response.data; } else { throw new Error(response.error || 'Failed to load process version preview'); } } catch (error) { console.error('Error loading process version preview:', error); throw error; } }, /** * Set change description for next save */ setChangeDescription(description) { this.lastChangeDescription = description; }, /** * Enhanced save process with change tracking */ async saveProcessWithDescription(changeDescription = '') { if (!this.currentProcess) return false; try { const processData = { processName: this.currentProcess.name, processDescription: this.currentProcess.description, nodes: this.currentProcess.nodes, edges: this.currentProcess.edges, viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 }, variables: this.currentProcess.variables || {}, settings: this.currentProcess.settings || {}, permissions: this.currentProcess.permissions || {}, changeDescription: changeDescription || this.lastChangeDescription, savedBy: 1 // TODO: Get from auth store }; const response = await $fetch(`/api/process/${this.currentProcess.id}`, { method: 'PUT', body: processData }); if (response.success) { // Update local state with server response const apiProcess = response.process; this.currentProcess.updatedAt = apiProcess.processModifiedDate; this.currentProcess.version = apiProcess.processVersion; // Update in processes array if it exists there const index = this.processes.findIndex(p => p.id === this.currentProcess.id); if (index !== -1) { this.processes[index] = { ...this.currentProcess }; } this.unsavedChanges = false; this.lastChangeDescription = ''; return response; } else { throw new Error(response.error || 'Failed to save process'); } } catch (error) { console.error('Error saving process:', error); throw error; } }, // Variable management methods /** * Add or update a variable in the current process */ addProcessVariable(variable) { if (!this.currentProcess) return; if (!this.currentProcess.variables) { this.currentProcess.variables = {}; } this.currentProcess.variables[variable.name] = { name: variable.name, type: variable.type || 'string', scope: variable.scope || 'global', value: variable.value, description: variable.description || '' }; this.unsavedChanges = true; }, /** * Update an existing variable in the current process */ updateProcessVariable(name, updates) { if (!this.currentProcess || !this.currentProcess.variables || !this.currentProcess.variables[name]) { return; } this.currentProcess.variables[name] = { ...this.currentProcess.variables[name], ...updates }; this.unsavedChanges = true; }, /** * Delete a variable from the current process */ deleteProcessVariable(name) { if (!this.currentProcess || !this.currentProcess.variables) { return; } delete this.currentProcess.variables[name]; this.unsavedChanges = true; }, /** * Get all variables from the current process */ getProcessVariables() { if (!this.currentProcess || !this.currentProcess.variables) { return []; } return Object.values(this.currentProcess.variables); }, /** * Get a specific variable from the current process */ getProcessVariable(name) { if (!this.currentProcess || !this.currentProcess.variables) { return null; } return this.currentProcess.variables[name] || null; }, /** * Update all references to a variable when its name changes * This ensures data integrity across all nodes that reference the variable */ updateVariableReferences(oldVariableName, newVariableName) { if (!this.currentProcess || !this.currentProcess.nodes || oldVariableName === newVariableName) { return; } console.log(`Updating variable references from "${oldVariableName}" to "${newVariableName}"`); this.currentProcess.nodes.forEach(node => { if (!node.data) return; switch (node.type) { case 'api': // Update API node output and error variables if (node.data.outputVariable === oldVariableName) { node.data.outputVariable = newVariableName; console.log(`Updated API node ${node.id} outputVariable`); } if (node.data.errorVariable === oldVariableName) { node.data.errorVariable = newVariableName; console.log(`Updated API node ${node.id} errorVariable`); } break; case 'script': // Update script node output variables and error variable if (node.data.errorVariable === oldVariableName) { node.data.errorVariable = newVariableName; console.log(`Updated Script node ${node.id} errorVariable`); } if (node.data.outputVariables && Array.isArray(node.data.outputVariables)) { node.data.outputVariables.forEach(output => { if (output.name === oldVariableName) { output.name = newVariableName; console.log(`Updated Script node ${node.id} outputVariable`); } }); } break; case 'form': // Update form node field mappings (both input and output mappings) if (node.data.inputMappings && Array.isArray(node.data.inputMappings)) { node.data.inputMappings.forEach(mapping => { if (mapping.processVariable === oldVariableName) { mapping.processVariable = newVariableName; console.log(`Updated Form node ${node.id} input mapping`); } }); } if (node.data.outputMappings && Array.isArray(node.data.outputMappings)) { node.data.outputMappings.forEach(mapping => { if (mapping.processVariable === oldVariableName) { mapping.processVariable = newVariableName; console.log(`Updated Form node ${node.id} output mapping`); } }); } if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) { node.data.fieldMappings.forEach(mapping => { if (mapping.processVariable === oldVariableName) { mapping.processVariable = newVariableName; console.log(`Updated Form node ${node.id} field mapping`); } }); } break; case 'businessRule': // Update business rule conditions (handle both ruleGroups and conditions structures) if (node.data.ruleGroups && Array.isArray(node.data.ruleGroups)) { node.data.ruleGroups.forEach(ruleGroup => { if (ruleGroup.conditions && Array.isArray(ruleGroup.conditions)) { ruleGroup.conditions.forEach(condition => { if (condition.variable === oldVariableName) { condition.variable = newVariableName; console.log(`Updated Business Rule node ${node.id} ruleGroup condition variable`); } }); } }); } if (node.data.conditions && Array.isArray(node.data.conditions)) { node.data.conditions.forEach(conditionGroup => { if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) { conditionGroup.conditions.forEach(condition => { if (condition.variable === oldVariableName) { condition.variable = newVariableName; console.log(`Updated Business Rule node ${node.id} condition variable`); } }); } }); } break; case 'notification': // Update notification content that may contain variable placeholders if (node.data.subject) { const oldPlaceholder = `{${oldVariableName}}`; const newPlaceholder = `{${newVariableName}}`; if (node.data.subject.includes(oldPlaceholder)) { node.data.subject = node.data.subject.replace(new RegExp(`\\{${oldVariableName}\\}`, 'g'), newPlaceholder); console.log(`Updated Notification node ${node.id} subject`); } } if (node.data.content) { const oldPlaceholder = `{${oldVariableName}}`; const newPlaceholder = `{${newVariableName}}`; if (node.data.content.includes(oldPlaceholder)) { node.data.content = node.data.content.replace(new RegExp(`\\{${oldVariableName}\\}`, 'g'), newPlaceholder); console.log(`Updated Notification node ${node.id} content`); } } break; case 'gateway': // Update gateway conditions if (node.data.conditions && Array.isArray(node.data.conditions)) { node.data.conditions.forEach(conditionGroup => { if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) { conditionGroup.conditions.forEach(condition => { if (condition.variable === oldVariableName) { condition.variable = newVariableName; console.log(`Updated Gateway node ${node.id} condition variable`); } }); } }); } break; default: // For any other node types, check if there are variable references in the data const nodeDataStr = JSON.stringify(node.data); if (nodeDataStr.includes(oldVariableName)) { console.log(`Warning: Node ${node.id} (type: ${node.type}) may contain references to variable "${oldVariableName}" that need manual review`); } break; } }); this.unsavedChanges = true; }, /** * Rename a variable and update all its references */ renameProcessVariable(oldName, newName) { if (!this.currentProcess || !this.currentProcess.variables || oldName === newName) { return false; } // Check if new name already exists if (this.currentProcess.variables[newName]) { console.error(`Variable "${newName}" already exists`); return false; } // Get the variable data const variableData = this.currentProcess.variables[oldName]; if (!variableData) { console.error(`Variable "${oldName}" not found`); return false; } // Update all references in nodes this.updateVariableReferences(oldName, newName); // Update the variable in the variables object delete this.currentProcess.variables[oldName]; this.currentProcess.variables[newName] = { ...variableData, name: newName }; // Force reactivity update by creating a new object reference this.currentProcess.variables = { ...this.currentProcess.variables }; // Force update of the entire process to trigger reactivity in all components this.currentProcess = { ...this.currentProcess }; console.log(`Successfully renamed variable from "${oldName}" to "${newName}"`); this.unsavedChanges = true; return true; } } });