- Added a computed property for dynamic shape classes in custom node components (ApiNode, BusinessRuleNode, FormNode, GatewayNode, HtmlNode, NotificationNode, ScriptNode, SubprocessNode) to support various shapes. - Updated the class bindings in the template to include the new shape classes, improving visual representation of nodes based on their defined shapes. - Adjusted styles in `nodeStyles.js` to accommodate new shape types (hexagon, trapezoid) and ensure proper rendering without conflicting base styles. - Enhanced the process builder interface by refining node styles and ensuring consistent behavior across different node types.
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;
|
|
}
|
|
}
|
|
});
|