- Introduced new HTML and Subprocess nodes in ProcessBuilderComponents.vue, allowing users to add custom HTML content and execute subprocesses within the process flow. - Updated ProcessFlowNodes.js to include HtmlNode and SubprocessNode components with appropriate properties and rendering logic. - Enhanced ProcessFlowCanvas.vue to manage the new node types effectively, ensuring proper integration with existing flow functionalities. - Improved index.vue to support configuration modals for HTML and Subprocess nodes, enhancing user interaction and customization options. - Refactored process management logic to accommodate new node types, ensuring seamless integration and consistent user experience across the process builder.
1322 lines
43 KiB
JavaScript
1322 lines
43 KiB
JavaScript
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: ''
|
|
}),
|
|
|
|
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_filled';
|
|
} 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;
|
|
}
|
|
|
|
// 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'
|
|
}
|
|
};
|
|
|
|
// Create a deep copy to avoid reference issues
|
|
const nodeCopy = JSON.parse(JSON.stringify(newNode));
|
|
|
|
// 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;
|
|
|
|
return nodeCopy;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
// Clear selection if the deleted node was selected
|
|
if (this.selectedNodeId === nodeId) {
|
|
this.selectedNodeId = null;
|
|
}
|
|
|
|
this.saveToHistory('Delete node');
|
|
this.unsavedChanges = true;
|
|
|
|
return true; // Return success
|
|
}
|
|
|
|
return false; // Return failure
|
|
},
|
|
|
|
/**
|
|
* Add an edge to the current process
|
|
*/
|
|
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 || {}
|
|
};
|
|
|
|
this.currentProcess.edges.push(newEdge);
|
|
this.selectedEdgeId = newEdge.id;
|
|
this.saveToHistory('Add edge');
|
|
this.unsavedChanges = true;
|
|
|
|
return newEdge;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
});
|