Core Components
-
+
-
- {{ component.name }}
+
+ {{ component.name }}
@@ -246,6 +246,7 @@ const addComponent = (component) => {
.component-item {
@apply h-20;
+ min-height: 80px;
transition: all 0.15s ease-in-out;
}
@@ -256,4 +257,28 @@ const addComponent = (component) => {
.component-item:active {
transform: scale(0.97);
}
+
+/* Mobile touch-friendly adjustments */
+@media (max-width: 768px) {
+ .component-item {
+ @apply h-24;
+ min-height: 96px;
+ }
+}
+
+/* Touch device optimizations */
+@media (hover: none) and (pointer: coarse) {
+ .component-item {
+ min-height: 44px; /* iOS recommended touch target */
+ }
+
+ .component-item:hover {
+ transform: none; /* Disable hover effects on touch devices */
+ }
+
+ .component-item:active {
+ transform: scale(0.95);
+ background-color: #f3f4f6;
+ }
+}
\ No newline at end of file
diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue
index 363bdee..06e269a 100644
--- a/components/process-flow/ProcessFlowCanvas.vue
+++ b/components/process-flow/ProcessFlowCanvas.vue
@@ -24,6 +24,10 @@ const props = defineProps({
initialEdges: {
type: Array,
default: () => []
+ },
+ highlightedNodeId: {
+ type: String,
+ default: null
}
});
@@ -45,7 +49,9 @@ const {
project,
removeNodes,
removeEdges,
- updateNodeInternals
+ updateNodeInternals,
+ setViewport,
+ getViewport
} = useVueFlow({
defaultEdgeOptions: {
animated: true,
@@ -129,18 +135,39 @@ const onNodeClick = ({ node }) => {
// Handle edge click
const onEdgeClick = (event, edge) => {
+ console.log('Edge click received:', { event, edge });
+
+ // Handle different parameter formats Vue Flow might send
+ let actualEdge = edge;
+
+ // Sometimes Vue Flow passes the edge in the event object
+ if (!actualEdge && event && typeof event === 'object') {
+ if (event.edge) {
+ actualEdge = event.edge;
+ } else if (event.id) {
+ // The event itself might be the edge
+ actualEdge = event;
+ }
+ }
+
+ // Add comprehensive null check for edge
+ if (!actualEdge || !actualEdge.id) {
+ console.warn('Invalid edge data received in onEdgeClick:', { event, edge, actualEdge });
+ return;
+ }
+
// Create a simplified copy of the edge data
const edgeData = {
- id: edge.id,
- source: edge.source,
- target: edge.target,
- label: edge.label || '',
- sourceNode: nodes.value.find(node => node.id === edge.source),
- targetNode: nodes.value.find(node => node.id === edge.target)
+ id: actualEdge.id,
+ source: actualEdge.source,
+ target: actualEdge.target,
+ label: actualEdge.label || '',
+ sourceNode: nodes.value.find(node => node.id === actualEdge.source),
+ targetNode: nodes.value.find(node => node.id === actualEdge.target)
};
emit('edgeSelected', edgeData);
- emit('edgeClick', edge);
+ emit('edgeClick', actualEdge);
};
// Handle pane (background) click
@@ -180,6 +207,139 @@ onMounted(() => {
}, 100);
});
+// Center on a specific node
+const centerOnNode = (nodeId, nodePosition) => {
+ console.log('centerOnNode called with:', nodeId, nodePosition);
+
+ if (!nodePosition) {
+ // Try to find the node position from current nodes
+ const targetNode = nodes.value.find(n => n.id === nodeId);
+ if (targetNode) {
+ nodePosition = targetNode.position;
+ } else {
+ console.warn('Could not find node to highlight:', nodeId, 'Available nodes:', nodes.value.map(n => n.id));
+ return;
+ }
+ }
+
+ try {
+ // Use the already available setViewport from useVueFlow
+ // Calculate the center position
+ // Account for the container size and panel offsets
+ const containerRect = document.querySelector('.vue-flow')?.getBoundingClientRect();
+ const centerX = containerRect ? containerRect.width / 2 : 400;
+ const centerY = containerRect ? containerRect.height / 2 : 300;
+
+ // Calculate new viewport to center the node
+ const newViewport = {
+ x: centerX - nodePosition.x - 100, // Offset for left panel
+ y: centerY - nodePosition.y,
+ zoom: 1.2
+ };
+
+ console.log('Setting viewport to:', newViewport);
+ setViewport(newViewport, { duration: 800 });
+
+ } catch (error) {
+ console.error('Error in centerOnNode:', error);
+ }
+};
+
+// The centerOnNode method will be exposed along with other methods below
+
+// Watch for changes to highlightedNodeId and apply highlighting
+watch(() => props.highlightedNodeId, (newHighlightedId, oldHighlightedId) => {
+ console.log('Highlighting changed:', { new: newHighlightedId, old: oldHighlightedId });
+
+ // Remove highlight from previously highlighted node
+ if (oldHighlightedId) {
+ const oldNode = nodes.value.find(node => node.id === oldHighlightedId);
+ if (oldNode) {
+ // Use node data approach for highlighting
+ if (oldNode.data) {
+ oldNode.data.highlighted = false;
+ }
+ // Also try class approach
+ if (oldNode.class) {
+ oldNode.class = oldNode.class.replace(' highlighted-node', '').trim();
+ }
+
+ // Try to find and modify the actual DOM element
+ nextTick(() => {
+ const nodeElement = document.querySelector(`[data-id="${oldHighlightedId}"]`);
+ if (nodeElement) {
+ nodeElement.classList.remove('highlighted-node');
+ nodeElement.style.removeProperty('box-shadow');
+ nodeElement.style.removeProperty('z-index');
+ nodeElement.style.removeProperty('transition');
+
+ const customNode = nodeElement.querySelector('.custom-node');
+ if (customNode) {
+ customNode.classList.remove('highlighted-node');
+ customNode.style.removeProperty('box-shadow');
+ customNode.style.removeProperty('z-index');
+ customNode.style.removeProperty('transition');
+ }
+ }
+ });
+
+ console.log('Removed highlight from node:', oldHighlightedId);
+ }
+ }
+
+ // Add highlight to new highlighted node
+ if (newHighlightedId) {
+ const newNode = nodes.value.find(node => node.id === newHighlightedId);
+ if (newNode) {
+ // Use node data approach for highlighting
+ if (!newNode.data) {
+ newNode.data = {};
+ }
+ newNode.data.highlighted = true;
+
+ // Also try class approach
+ if (!newNode.class?.includes('highlighted-node')) {
+ newNode.class = (newNode.class || '').trim() + ' highlighted-node';
+ }
+
+ // Try to find and modify the actual DOM element directly with inline styles
+ nextTick(() => {
+ const nodeElement = document.querySelector(`[data-id="${newHighlightedId}"]`);
+ if (nodeElement) {
+ nodeElement.classList.add('highlighted-node');
+ nodeElement.setAttribute('data-highlighted', 'true');
+
+ // Apply subtle inline styles for immediate visual effect
+ nodeElement.style.boxShadow = '0 0 0 2px #3b82f6, 0 4px 12px rgba(59, 130, 246, 0.3)';
+ nodeElement.style.zIndex = '999';
+ nodeElement.style.transition = 'all 0.3s ease-in-out';
+
+ const customNode = nodeElement.querySelector('.custom-node');
+ if (customNode) {
+ customNode.classList.add('highlighted-node');
+ customNode.setAttribute('data-highlighted', 'true');
+
+ // Apply subtle styles to custom node as well
+ customNode.style.boxShadow = '0 0 0 2px #3b82f6, 0 4px 12px rgba(59, 130, 246, 0.3)';
+ customNode.style.zIndex = '999';
+ customNode.style.transition = 'all 0.3s ease-in-out';
+ }
+ console.log('Applied highlight to DOM element with inline styles:', nodeElement);
+ } else {
+ console.warn('Could not find DOM element for node:', newHighlightedId);
+ }
+ });
+
+ console.log('Added highlight to node:', newHighlightedId, 'data:', newNode.data);
+
+ // Force node update
+ updateNodeInternals([newHighlightedId]);
+ } else {
+ console.warn('Could not find node to highlight:', newHighlightedId, 'Available nodes:', nodes.value.map(n => ({ id: n.id, type: n.type })));
+ }
+ }
+});
+
// Watch for changes to initialNodes prop and update the canvas
watch(() => props.initialNodes, async (newNodes, oldNodes) => {
if (newNodes && Array.isArray(newNodes) && !isUpdatingNodes.value) {
@@ -525,7 +685,11 @@ defineExpose({
addNode,
removeNode,
fitView,
- syncCanvas
+ syncCanvas,
+ centerOnNode,
+ // Add Vue Flow save/restore methods
+ toObject,
+ fromObject
});
// Update an existing node
@@ -715,6 +879,174 @@ function syncCanvas(newNodes, newEdges) {
}
}, 50); // Small delay to allow any pending operations to complete
}
+
+// Add Vue Flow save method - captures complete flow state including viewport
+function toObject() {
+ try {
+ // Create flow object using only essential data, excluding Vue Flow internals
+ const flowObject = {
+ nodes: nodes.value.map(node => ({
+ id: node.id,
+ type: node.type,
+ label: node.label,
+ position: { ...node.position },
+ data: { ...node.data }
+ // Exclude: events, dragging, dimensions, handleBounds, computedPosition, etc.
+ })),
+ edges: edges.value.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 ? { ...edge.data } : {}
+ // Exclude: events, sourceNode, targetNode, sourceX, sourceY, targetX, targetY
+ })),
+ viewport: getViewport()
+ };
+
+ console.log('๐พ Captured clean flow state:', {
+ nodes: flowObject.nodes?.length || 0,
+ edges: flowObject.edges?.length || 0,
+ viewport: flowObject.viewport
+ });
+
+ return flowObject;
+ } catch (error) {
+ console.error('Error capturing flow state:', error);
+
+ // Return a safe fallback object
+ return {
+ nodes: [],
+ edges: [],
+ viewport: { x: 0, y: 0, zoom: 1 }
+ };
+ }
+}
+
+// Add Vue Flow restore method - properly restores complete flow state
+function fromObject(flowObject) {
+ if (!flowObject) {
+ console.warn('No flow object provided for restoration');
+ return Promise.resolve();
+ }
+
+ if (!flowObject.nodes && !flowObject.edges) {
+ console.warn('Flow object contains no nodes or edges');
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ try {
+ console.log('๐ Restoring complete flow state:', {
+ nodes: flowObject.nodes?.length || 0,
+ edges: flowObject.edges?.length || 0,
+ viewport: flowObject.viewport
+ });
+
+ // Clear updating flags to ensure clean restoration
+ isUpdatingNodes.value = false;
+ isUpdatingEdges.value = false;
+
+ // Wait for any pending operations to complete
+ setTimeout(async () => {
+ try {
+ // 1. First, clear existing state
+ if (nodes.value.length > 0) {
+ removeNodes([...nodes.value]);
+ }
+ if (edges.value.length > 0) {
+ removeEdges([...edges.value]);
+ }
+
+ // Wait for clearing to complete
+ await nextTick();
+
+ // 2. Restore nodes first
+ if (flowObject.nodes && Array.isArray(flowObject.nodes) && flowObject.nodes.length > 0) {
+ const nodesToRestore = flowObject.nodes.map(node => ({
+ id: node.id,
+ type: node.type,
+ label: node.label || '',
+ position: node.position || { x: 0, y: 0 },
+ data: node.data || {}
+ // Only include essential properties needed for Vue Flow
+ }));
+
+ console.log('Restoring nodes:', nodesToRestore.length, nodesToRestore.map(n => `${n.id} (${n.type})`));
+ addNodes(nodesToRestore);
+ await nextTick();
+
+ // Wait a bit more for nodes to be fully initialized
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ // 3. Restore edges after nodes are ready
+ if (flowObject.edges && Array.isArray(flowObject.edges) && flowObject.edges.length > 0 && nodes.value.length > 0) {
+ // Verify all edges have valid source and target nodes
+ const validEdges = flowObject.edges.filter(edge => {
+ const sourceExists = nodes.value.some(node => node.id === edge.source);
+ const targetExists = nodes.value.some(node => node.id === edge.target);
+
+ if (!sourceExists || !targetExists) {
+ console.warn(`Skipping invalid edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
+ return false;
+ }
+
+ return true;
+ });
+
+ if (validEdges.length > 0) {
+ console.log('Restoring edges:', validEdges.length);
+
+ // Clean edge data before adding
+ const cleanEdges = validEdges.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 || {}
+ // Only include essential properties
+ }));
+
+ addEdges(cleanEdges);
+ await nextTick();
+ }
+ }
+
+ // 4. Finally, restore viewport position and zoom
+ if (flowObject.viewport && typeof flowObject.viewport === 'object') {
+ const viewport = {
+ x: flowObject.viewport.x || 0,
+ y: flowObject.viewport.y || 0,
+ zoom: flowObject.viewport.zoom || 1
+ };
+ console.log('Restoring viewport:', viewport);
+ setViewport(viewport, { duration: 0 }); // No animation for restore
+ }
+
+ console.log('โ
Flow state restoration completed successfully');
+ resolve();
+
+ } catch (error) {
+ console.error('Error during flow state restoration:', error);
+ resolve(); // Resolve anyway to prevent hanging
+ }
+ }, 100); // Small delay to ensure clean state
+
+ } catch (error) {
+ console.error('Error in fromObject:', error);
+ resolve(); // Resolve anyway to prevent hanging
+ }
+ });
+}
@@ -923,4 +1255,36 @@ function syncCanvas(newNodes, newEdges) {
:deep(.vue-flow__node.selected) {
box-shadow: 0 0 0 2px #ff6b6b;
}
+
+
+/* Target both Vue Flow nodes and custom nodes - updated highlighting */
+:deep(.vue-flow__node.highlighted-node),
+:deep(.vue-flow__node[data-highlighted="true"]),
+:deep(.vue-flow__node .custom-node.highlighted-node),
+:deep(.vue-flow__node .custom-node[data-highlighted="true"]) {
+ animation: highlightPulse 2s ease-in-out infinite !important;
+ box-shadow: 0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.4) !important;
+ z-index: 999 !important;
+ transform: scale(1.05) !important;
+ transition: transform 0.3s ease-in-out !important;
+}
+
+/* Additional selector for direct custom node highlighting */
+:deep(.custom-node.highlighted-node),
+:deep(.custom-node[data-highlighted="true"]) {
+ animation: highlightPulse 2s ease-in-out infinite !important;
+ box-shadow: 0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.4) !important;
+ z-index: 999 !important;
+ transform: scale(1.05) !important;
+ transition: transform 0.3s ease-in-out !important;
+}
+
+@keyframes highlightPulse {
+ 0%, 100% {
+ box-shadow: 0 0 0 3px #3b82f6, 0 0 20px rgba(59, 130, 246, 0.4);
+ }
+ 50% {
+ box-shadow: 0 0 0 5px #60a5fa, 0 0 30px rgba(96, 165, 250, 0.6);
+ }
+}
\ No newline at end of file
diff --git a/components/process-flow/VariableManager.vue b/components/process-flow/VariableManager.vue
index 97a10ba..b3b8c2e 100644
--- a/components/process-flow/VariableManager.vue
+++ b/components/process-flow/VariableManager.vue
@@ -1,15 +1,15 @@
-
+
-
-
-
+
+
+
-
Variables
-
+
Variables
+
Define and manage variables to store and pass data within your process
@@ -31,22 +31,22 @@
-
+
-
+
-
+
@@ -126,32 +126,42 @@
:key="variable.name"
class="variable-item bg-white border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-sm transition-all duration-150 group"
>
-
-
-
-
-
{{ variable.name }}
+
+
+
+
+
+ {{ variable.name }}
+
-
-
-
-
+
+
+ {{ variable.description }}
+
+
+
+
+
+
+
Used in process
-
-
-
+
{{ getValuePreview(variable.value, variable.type) }}
-
-
-
-
-
- {{ variable.description }}
-
@@ -235,6 +240,169 @@
+
+
+
+
+
+
+
+
+
{{ selectedVariable.name }}
+
+
+ {{ selectedVariable.type }}
+
+ โข
+ {{ selectedVariable.scope || 'global' }} scope
+
+
+ {{ selectedVariable.description }}
+
+
+ Current Value:
+
+ {{ getValuePreview(selectedVariable.value, selectedVariable.type) }}
+
+
+
+
+
+
+
+
+
+
+
Usage Summary
+
+
+
+
{{ variableUsageDetails.totalUsages }}
+
Total References
+
+
+
{{ variableUsageDetails.nodeCount }}
+
Nodes Using Variable
+
+
+
{{ variableUsageDetails.nodeTypes.size }}
+
Different Node Types
+
+
+
+
+
+
+
+
+ Usage Details
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ usage.nodeLabel || `${usage.nodeType} Node` }}
+
+
+ {{ usage.nodeType }}
+
+
+
+
+
+
+ Location:
+ {{ usage.location }}
+
+
+
+
+ Context:
+ {{ usage.context }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit Variable
+
+
+
+ Close
+
+
+
+
+
+
+
+
+ Variable Not Used
+
+
+ The variable "{{ selectedVariable.name }}" is not currently being used in any nodes in your process.
+
+
+
+
+ Edit Variable
+
+
+ Close
+
+
+
+
+
{
}
};
+// Helper function to check variable usage in text/JSON content
+const checkTextForVariable = (text, variableName) => {
+ if (!text || typeof text !== 'string') return false;
+
+ // Check for various variable reference patterns
+ const patterns = [
+ `{${variableName}}`, // Template placeholder: {variableName}
+ `{{${variableName}}}`, // Handlebars style: {{variableName}}
+ `\${${variableName}}`, // JavaScript template literal: ${variableName}
+ `processVariables.${variableName}`, // Direct object access
+ `processVariables["${variableName}"]`, // Bracket notation with quotes
+ `processVariables['${variableName}']`, // Bracket notation with single quotes
+ `"${variableName}"`, // JSON string value
+ `'${variableName}'`, // Single quoted string
+ new RegExp(`\\b${variableName}\\b`, 'g') // Word boundary match
+ ];
+
+ return patterns.some(pattern => {
+ if (pattern instanceof RegExp) {
+ return pattern.test(text);
+ }
+ return text.includes(pattern);
+ });
+};
+
// Helper function to check where a variable is being used
const checkVariableUsage = (variableName) => {
const usageDetails = [];
@@ -566,53 +762,198 @@ const checkVariableUsage = (variableName) => {
processStore.currentProcess.nodes.forEach(node => {
if (!node.data) return;
- const nodeLabel = node.data.label || `${node.type} node`;
+ const nodeLabel = node.data.label || node.label || `${node.type} node`;
switch (node.type) {
case 'api':
+ // Check output and error variables
if (node.data.outputVariable === variableName) {
- usageDetails.push(`- ${nodeLabel}: API Output Variable`);
+ usageDetails.push(`- ${nodeLabel}: API Response Output Variable`);
isUsed = true;
}
if (node.data.errorVariable === variableName) {
- usageDetails.push(`- ${nodeLabel}: API Error Variable`);
+ usageDetails.push(`- ${nodeLabel}: API Error Output Variable`);
+ isUsed = true;
+ }
+
+ // Check API URL for variable references
+ if (node.data.apiUrl && checkTextForVariable(node.data.apiUrl, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: API URL contains variable reference`);
+ isUsed = true;
+ }
+
+ // Check request body for variable references
+ if (node.data.requestBody && checkTextForVariable(node.data.requestBody, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: API Request Body contains variable reference`);
+ isUsed = true;
+ }
+
+ // Check headers for variable references
+ if (node.data.headers && checkTextForVariable(node.data.headers, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: API Headers contain variable reference`);
+ isUsed = true;
+ }
+
+ // Check query parameters
+ if (node.data.queryParams && checkTextForVariable(node.data.queryParams, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: API Query Parameters contain variable reference`);
isUsed = true;
}
break;
case 'script':
+ // Check error variable
if (node.data.errorVariable === variableName) {
- usageDetails.push(`- ${nodeLabel}: Script Error Variable`);
+ usageDetails.push(`- ${nodeLabel}: Script Error Output Variable`);
isUsed = true;
}
+
+ // Check output variables
if (node.data.outputVariables && Array.isArray(node.data.outputVariables)) {
node.data.outputVariables.forEach(output => {
if (output.name === variableName) {
- usageDetails.push(`- ${nodeLabel}: Script Output Variable`);
+ usageDetails.push(`- ${nodeLabel}: Script Output Variable "${output.name}"`);
isUsed = true;
}
});
}
+
+ // Check input variables (can be array of strings or objects)
+ if (node.data.inputVariables && Array.isArray(node.data.inputVariables)) {
+ node.data.inputVariables.forEach(input => {
+ // Handle string array format
+ if (typeof input === 'string' && input === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Script Input Variable "${input}"`);
+ isUsed = true;
+ }
+ // Handle object format
+ else if (typeof input === 'object' && (input.name === variableName || input.variable === variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Script Input Variable "${input.name || input.variable}"`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check script code for variable references
+ if (node.data.scriptCode && checkTextForVariable(node.data.scriptCode, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Script Code contains variable reference`);
+ isUsed = true;
+ }
break;
case 'form':
- if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) {
- node.data.fieldMappings.forEach(mapping => {
+ // Check output mappings (form fields mapped to process variables)
+ if (node.data.outputMappings && Array.isArray(node.data.outputMappings)) {
+ node.data.outputMappings.forEach(mapping => {
if (mapping.processVariable === variableName) {
- usageDetails.push(`- ${nodeLabel}: Form Field Mapping`);
+ usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.formField}" outputs to variable`);
isUsed = true;
}
});
}
+
+ // Check input mappings (process variables mapped to form fields)
+ if (node.data.inputMappings && Array.isArray(node.data.inputMappings)) {
+ node.data.inputMappings.forEach(mapping => {
+ if (mapping.processVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Variable inputs to Form Field "${mapping.formField}"`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check legacy field mappings (for backward compatibility)
+ if (node.data.fieldMappings && Array.isArray(node.data.fieldMappings)) {
+ node.data.fieldMappings.forEach(mapping => {
+ if (mapping.processVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.fieldName}" mapped to variable`);
+ isUsed = true;
+ }
+ if (mapping.defaultValue && checkTextForVariable(mapping.defaultValue, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Form Field "${mapping.fieldName}" default value references variable`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check field conditions
+ if (node.data.fieldConditions && Array.isArray(node.data.fieldConditions)) {
+ node.data.fieldConditions.forEach((condition, index) => {
+ if (condition.variable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Form Field Condition ${index + 1}`);
+ isUsed = true;
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Form Field Condition ${index + 1} value references variable`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check form assignment conditions
+ if (node.data.assignmentConditions && Array.isArray(node.data.assignmentConditions)) {
+ node.data.assignmentConditions.forEach((condition, index) => {
+ if (condition.variable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Form Assignment Condition ${index + 1}`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check assignment variable
+ if (node.data.assignmentVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Form Assignment Variable`);
+ isUsed = true;
+ }
+
+ // Check assignee variable (legacy)
+ if (node.data.assigneeVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Form Assignee Variable`);
+ isUsed = true;
+ }
break;
+ case 'business-rule':
case 'businessRule':
- 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 => {
+ // Check rule groups and conditions
+ if (node.data.ruleGroups && Array.isArray(node.data.ruleGroups)) {
+ node.data.ruleGroups.forEach((group, groupIndex) => {
+ if (group.conditions && Array.isArray(group.conditions)) {
+ group.conditions.forEach((condition, condIndex) => {
if (condition.variable === variableName) {
- usageDetails.push(`- ${nodeLabel}: Business Rule Condition`);
+ usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1}`);
+ isUsed = true;
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1} value references variable`);
+ isUsed = true;
+ }
+ });
+ }
+
+ // Check actions
+ if (group.actions && Array.isArray(group.actions)) {
+ group.actions.forEach((action, actionIndex) => {
+ if (action.variable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} target variable`);
+ isUsed = true;
+ }
+ if (action.value && checkTextForVariable(String(action.value), variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} value references variable`);
+ isUsed = true;
+ }
+ });
+ }
+ });
+ }
+
+ // Check legacy conditions structure
+ if (node.data.conditions && Array.isArray(node.data.conditions)) {
+ node.data.conditions.forEach((conditionGroup, groupIndex) => {
+ if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
+ conditionGroup.conditions.forEach((condition, condIndex) => {
+ if (condition.variable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Business Rule Condition Group ${groupIndex + 1}, Condition ${condIndex + 1}`);
isUsed = true;
}
});
@@ -622,24 +963,58 @@ const checkVariableUsage = (variableName) => {
break;
case 'notification':
- const placeholderPattern = `{${variableName}}`;
- if (node.data.subject && node.data.subject.includes(placeholderPattern)) {
- usageDetails.push(`- ${nodeLabel}: Notification Subject`);
+ // Check recipient variables
+ if (node.data.recipientVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Notification Recipient Variable`);
isUsed = true;
}
- if (node.data.content && node.data.content.includes(placeholderPattern)) {
- usageDetails.push(`- ${nodeLabel}: Notification Content`);
+ if (node.data.recipientUser === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Notification Recipient User Variable`);
+ isUsed = true;
+ }
+ if (node.data.recipientEmail === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Notification Recipient Email Variable`);
+ isUsed = true;
+ }
+
+ // Check subject and message content for variable placeholders
+ if (node.data.subject && checkTextForVariable(node.data.subject, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Notification Subject contains variable reference`);
+ isUsed = true;
+ }
+ if (node.data.message && checkTextForVariable(node.data.message, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Notification Message contains variable reference`);
+ isUsed = true;
+ }
+ if (node.data.content && checkTextForVariable(node.data.content, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Notification Content contains variable reference`);
+ isUsed = true;
+ }
+
+ // Check dynamic content fields
+ if (node.data.dynamicSubject && checkTextForVariable(node.data.dynamicSubject, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Notification Dynamic Subject contains variable reference`);
+ isUsed = true;
+ }
+ if (node.data.dynamicMessage && checkTextForVariable(node.data.dynamicMessage, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Notification Dynamic Message contains variable reference`);
isUsed = true;
}
break;
case 'gateway':
+ case 'decision':
+ // Check gateway conditions
if (node.data.conditions && Array.isArray(node.data.conditions)) {
- node.data.conditions.forEach(conditionGroup => {
+ node.data.conditions.forEach((conditionGroup, groupIndex) => {
if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
- conditionGroup.conditions.forEach(condition => {
+ conditionGroup.conditions.forEach((condition, condIndex) => {
if (condition.variable === variableName) {
- usageDetails.push(`- ${nodeLabel}: Gateway Condition`);
+ usageDetails.push(`- ${nodeLabel}: Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1}`);
+ isUsed = true;
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ usageDetails.push(`- ${nodeLabel}: Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1} value references variable`);
isUsed = true;
}
});
@@ -647,22 +1022,475 @@ const checkVariableUsage = (variableName) => {
});
}
break;
+
+ default:
+ // For any custom node types, check common data fields
+ if (node.data.outputVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Output Variable`);
+ isUsed = true;
+ }
+ if (node.data.inputVariable === variableName) {
+ usageDetails.push(`- ${nodeLabel}: Input Variable`);
+ isUsed = true;
+ }
+
+ // Check any text fields for variable references
+ Object.keys(node.data).forEach(key => {
+ const value = node.data[key];
+ if (typeof value === 'string' && checkTextForVariable(value, variableName)) {
+ usageDetails.push(`- ${nodeLabel}: ${key} field contains variable reference`);
+ isUsed = true;
+ }
+ });
+ break;
}
});
return { isUsed, usageDetails };
};
-// Show variable usage details in an alert
-const showVariableUsage = (variable) => {
- const usageInfo = checkVariableUsage(variable.name);
+// Enhanced function to get detailed variable usage information
+const getDetailedVariableUsage = (variableName) => {
+ // Use the same node source as the working checkVariableUsage function
+ const nodes = processStore.currentProcess?.nodes || [];
+ const usages = [];
+ const nodeTypes = new Set();
+ let totalUsages = 0;
- if (usageInfo.isUsed) {
- const message = `Variable "${variable.name}" is currently being used in:\n\n${usageInfo.usageDetails.join('\n')}`;
- alert(message);
- } else {
- alert(`Variable "${variable.name}" is not currently being used in any nodes.`);
- }
+ const checkTextForVariable = (text, variable) => {
+ if (!text || typeof text !== 'string') return false;
+
+ // Use the same patterns as the working function
+ const patterns = [
+ `{${variable}}`, // Template placeholder: {variableName}
+ `{{${variable}}}`, // Handlebars style: {{variableName}}
+ `\${${variable}}`, // JavaScript template literal: ${variableName}
+ `processVariables.${variable}`, // Direct object access
+ `processVariables["${variable}"]`, // Bracket notation with quotes
+ `processVariables['${variable}']`, // Bracket notation with single quotes
+ `"${variable}"`, // JSON string value
+ `'${variable}'`, // Single quoted string
+ new RegExp(`\\b${variable}\\b`, 'g') // Word boundary match
+ ];
+
+ return patterns.some(pattern => {
+ if (pattern instanceof RegExp) {
+ return pattern.test(text);
+ }
+ return text.includes(pattern);
+ });
+ };
+
+ nodes.forEach(node => {
+ if (!node.data) return;
+
+ const nodeUsages = [];
+ const nodeLabel = node.data.label || node.label || `${node.type} node`;
+
+ // Form nodes
+ if (node.type === 'form') {
+ const data = node.data || {};
+
+ // Check output mappings (form fields mapped to process variables)
+ if (data.outputMappings && Array.isArray(data.outputMappings)) {
+ data.outputMappings.forEach((mapping, index) => {
+ if (mapping.processVariable === variableName) {
+ nodeUsages.push({
+ location: `Output Mapping #${index + 1}`,
+ context: `Field "${mapping.formField}" โ Variable "${mapping.processVariable}"`
+ });
+ }
+ });
+ }
+
+ // Check input mappings (process variables mapped to form fields)
+ if (data.inputMappings && Array.isArray(data.inputMappings)) {
+ data.inputMappings.forEach((mapping, index) => {
+ if (mapping.processVariable === variableName) {
+ nodeUsages.push({
+ location: `Input Mapping #${index + 1}`,
+ context: `Variable "${mapping.processVariable}" โ Field "${mapping.formField}"`
+ });
+ }
+ });
+ }
+
+ // Check legacy field mappings (for backward compatibility)
+ if (data.fieldMappings && Array.isArray(data.fieldMappings)) {
+ data.fieldMappings.forEach((mapping, index) => {
+ if (mapping.processVariable === variableName) {
+ nodeUsages.push({
+ location: `Field Mapping #${index + 1}`,
+ context: `Form Field "${mapping.fieldName}" mapped to variable`
+ });
+ }
+ if (mapping.defaultValue && checkTextForVariable(mapping.defaultValue, variableName)) {
+ nodeUsages.push({
+ location: `Field Mapping #${index + 1} Default`,
+ context: `Form Field "${mapping.fieldName}" default value references variable`
+ });
+ }
+ });
+ }
+
+ // Check assignment variable
+ if (data.assignmentVariable === variableName) {
+ nodeUsages.push({
+ location: 'Assignment Variable',
+ context: `Form result assigned to "${data.assignmentVariable}"`
+ });
+ }
+
+ // Check assignee variable (legacy)
+ if (data.assigneeVariable === variableName) {
+ nodeUsages.push({
+ location: 'Assignee Variable',
+ context: `Form assignee set to "${data.assigneeVariable}"`
+ });
+ }
+
+ // Check field conditions
+ if (data.fieldConditions && Array.isArray(data.fieldConditions)) {
+ data.fieldConditions.forEach((condition, index) => {
+ if (condition.variable === variableName) {
+ nodeUsages.push({
+ location: `Field Condition #${index + 1}`,
+ context: `Condition: ${condition.variable} ${condition.operator} ${condition.value}`
+ });
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ nodeUsages.push({
+ location: `Field Condition #${index + 1} Value`,
+ context: `Condition value references variable`
+ });
+ }
+ });
+ }
+
+ // Check form assignment conditions
+ if (data.assignmentConditions && Array.isArray(data.assignmentConditions)) {
+ data.assignmentConditions.forEach((condition, index) => {
+ if (condition.variable === variableName) {
+ nodeUsages.push({
+ location: `Assignment Condition #${index + 1}`,
+ context: `Assignment condition on variable`
+ });
+ }
+ });
+ }
+ }
+
+ // API nodes
+ else if (node.type === 'api') {
+ const data = node.data || {};
+
+ // Check output and error variables
+ if (data.outputVariable === variableName) {
+ nodeUsages.push({
+ location: 'API Output Variable',
+ context: `Response stored in "${data.outputVariable}"`
+ });
+ }
+
+ if (data.errorVariable === variableName) {
+ nodeUsages.push({
+ location: 'API Error Variable',
+ context: `Error stored in "${data.errorVariable}"`
+ });
+ }
+
+ // Check API URL for variable references
+ if (data.apiUrl && checkTextForVariable(data.apiUrl, variableName)) {
+ nodeUsages.push({
+ location: 'API URL',
+ context: `API URL contains variable reference`
+ });
+ }
+
+ // Check request body for variable references
+ if (data.requestBody && checkTextForVariable(data.requestBody, variableName)) {
+ nodeUsages.push({
+ location: 'Request Body',
+ context: `API Request Body contains variable reference`
+ });
+ }
+
+ // Check headers for variable references
+ if (data.headers && checkTextForVariable(data.headers, variableName)) {
+ nodeUsages.push({
+ location: 'Headers',
+ context: `API Headers contain variable reference`
+ });
+ }
+
+ // Check query parameters
+ if (data.queryParams && checkTextForVariable(data.queryParams, variableName)) {
+ nodeUsages.push({
+ location: 'Query Parameters',
+ context: `API Query Parameters contain variable reference`
+ });
+ }
+ }
+
+ // Script nodes
+ else if (node.type === 'script') {
+ const data = node.data || {};
+
+ // Check error variable
+ if (data.errorVariable === variableName) {
+ nodeUsages.push({
+ location: 'Script Error Variable',
+ context: `Error stored in "${data.errorVariable}"`
+ });
+ }
+
+ // Check output variables
+ if (data.outputVariables && Array.isArray(data.outputVariables)) {
+ data.outputVariables.forEach(output => {
+ if (output.name === variableName) {
+ nodeUsages.push({
+ location: 'Script Output Variables',
+ context: `Output variable: "${output.name}"`
+ });
+ }
+ });
+ }
+
+ // Check input variables (can be array of strings or objects)
+ if (data.inputVariables && Array.isArray(data.inputVariables)) {
+ data.inputVariables.forEach(input => {
+ // Handle string array format
+ if (typeof input === 'string' && input === variableName) {
+ nodeUsages.push({
+ location: 'Script Input Variables',
+ context: `Input variable: "${input}"`
+ });
+ }
+ // Handle object format
+ else if (typeof input === 'object' && (input.name === variableName || input.variable === variableName)) {
+ nodeUsages.push({
+ location: 'Script Input Variables',
+ context: `Input variable: "${input.name || input.variable}"`
+ });
+ }
+ });
+ }
+
+ // Check script code for variable references
+ if (data.scriptCode && checkTextForVariable(data.scriptCode, variableName)) {
+ nodeUsages.push({
+ location: 'Script Code',
+ context: 'Script code contains variable reference'
+ });
+ }
+ }
+
+ // Decision nodes
+ else if (node.type === 'gateway' || node.type === 'decision') {
+ const data = node.data || {};
+
+ // Check gateway conditions
+ if (data.conditions && Array.isArray(data.conditions)) {
+ data.conditions.forEach((conditionGroup, groupIndex) => {
+ if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
+ conditionGroup.conditions.forEach((condition, condIndex) => {
+ if (condition.variable === variableName) {
+ nodeUsages.push({
+ location: `Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1}`,
+ context: `${condition.variable} ${condition.operator} ${condition.value}`
+ });
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ nodeUsages.push({
+ location: `Decision Path "${conditionGroup.output || 'Path ' + (groupIndex + 1)}", Condition ${condIndex + 1} Value`,
+ context: `Condition value references variable`
+ });
+ }
+ });
+ }
+ });
+ }
+ }
+
+ // Business rule nodes
+ else if (node.type === 'business-rule' || node.type === 'businessRule') {
+ const data = node.data || {};
+
+ // Check rule groups and conditions
+ if (data.ruleGroups && Array.isArray(data.ruleGroups)) {
+ data.ruleGroups.forEach((group, groupIndex) => {
+ if (group.conditions && Array.isArray(group.conditions)) {
+ group.conditions.forEach((condition, condIndex) => {
+ if (condition.variable === variableName) {
+ nodeUsages.push({
+ location: `Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1}`,
+ context: `${condition.variable} ${condition.operator} ${condition.value}`
+ });
+ }
+ if (condition.value && checkTextForVariable(String(condition.value), variableName)) {
+ nodeUsages.push({
+ location: `Business Rule Group ${groupIndex + 1}, Condition ${condIndex + 1} Value`,
+ context: `Condition value references variable`
+ });
+ }
+ });
+ }
+
+ // Check actions
+ if (group.actions && Array.isArray(group.actions)) {
+ group.actions.forEach((action, actionIndex) => {
+ if (action.variable === variableName) {
+ nodeUsages.push({
+ location: `Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1}`,
+ context: `Set ${action.variable} = ${action.value}`
+ });
+ }
+ if (action.value && checkTextForVariable(String(action.value), variableName)) {
+ nodeUsages.push({
+ location: `Business Rule Group ${groupIndex + 1}, Action ${actionIndex + 1} Value`,
+ context: `Action value references variable`
+ });
+ }
+ });
+ }
+ });
+ }
+
+ // Check legacy conditions structure
+ if (data.conditions && Array.isArray(data.conditions)) {
+ data.conditions.forEach((conditionGroup, groupIndex) => {
+ if (conditionGroup.conditions && Array.isArray(conditionGroup.conditions)) {
+ conditionGroup.conditions.forEach((condition, condIndex) => {
+ if (condition.variable === variableName) {
+ nodeUsages.push({
+ location: `Business Rule Condition Group ${groupIndex + 1}, Condition ${condIndex + 1}`,
+ context: `${condition.variable} ${condition.operator} ${condition.value}`
+ });
+ }
+ });
+ }
+ });
+ }
+ }
+
+ // Notification nodes
+ else if (node.type === 'notification') {
+ const data = node.data || {};
+
+ // Check recipient variables
+ if (data.recipientVariable === variableName) {
+ nodeUsages.push({
+ location: 'Notification Recipient Variable',
+ context: `Recipient variable: "${data.recipientVariable}"`
+ });
+ }
+ if (data.recipientUser === variableName) {
+ nodeUsages.push({
+ location: 'Notification Recipient User Variable',
+ context: `Recipient user variable: "${data.recipientUser}"`
+ });
+ }
+ if (data.recipientEmail === variableName) {
+ nodeUsages.push({
+ location: 'Notification Recipient Email Variable',
+ context: `Recipient email variable: "${data.recipientEmail}"`
+ });
+ }
+
+ // Check subject and message content for variable placeholders
+ if (data.subject && checkTextForVariable(data.subject, variableName)) {
+ nodeUsages.push({
+ location: 'Notification Subject',
+ context: `Subject contains variable reference`
+ });
+ }
+ if (data.message && checkTextForVariable(data.message, variableName)) {
+ nodeUsages.push({
+ location: 'Notification Message',
+ context: `Message contains variable reference`
+ });
+ }
+ if (data.content && checkTextForVariable(data.content, variableName)) {
+ nodeUsages.push({
+ location: 'Notification Content',
+ context: `Content contains variable reference`
+ });
+ }
+
+ // Check dynamic content fields
+ if (data.dynamicSubject && checkTextForVariable(data.dynamicSubject, variableName)) {
+ nodeUsages.push({
+ location: 'Notification Dynamic Subject',
+ context: `Dynamic subject contains variable reference`
+ });
+ }
+ if (data.dynamicMessage && checkTextForVariable(data.dynamicMessage, variableName)) {
+ nodeUsages.push({
+ location: 'Notification Dynamic Message',
+ context: `Dynamic message contains variable reference`
+ });
+ }
+ }
+
+ // Default case - for any custom node types, check common data fields
+ else {
+ const data = node.data || {};
+
+ if (data.outputVariable === variableName) {
+ nodeUsages.push({
+ location: 'Output Variable',
+ context: `Output variable: "${data.outputVariable}"`
+ });
+ }
+ if (data.inputVariable === variableName) {
+ nodeUsages.push({
+ location: 'Input Variable',
+ context: `Input variable: "${data.inputVariable}"`
+ });
+ }
+
+ // Check any text fields for variable references
+ Object.keys(data).forEach(key => {
+ const value = data[key];
+ if (typeof value === 'string' && checkTextForVariable(value, variableName)) {
+ nodeUsages.push({
+ location: `${key} field`,
+ context: `${key} field contains variable reference`
+ });
+ }
+ });
+ }
+
+ // Add usages to the main list if any found
+ if (nodeUsages.length > 0) {
+ nodeTypes.add(node.type);
+ nodeUsages.forEach(usage => {
+ usages.push({
+ nodeId: node.id,
+ nodeType: node.type,
+ nodeLabel: nodeLabel,
+ ...usage
+ });
+ totalUsages++;
+ });
+ }
+ });
+
+ return {
+ usages,
+ totalUsages,
+ nodeCount: usages.reduce((acc, usage) => {
+ return acc.add(usage.nodeId);
+ }, new Set()).size,
+ nodeTypes
+ };
+};
+
+// Show variable usage details in modal
+const showVariableUsage = (variable) => {
+ selectedVariable.value = variable;
+ variableUsageDetails.value = getDetailedVariableUsage(variable.name);
+ showUsageModal.value = true;
};
const resetForm = () => {
@@ -800,6 +1628,91 @@ const getVariableIcon = (type) => {
return 'material-symbols:data-object';
}
};
+
+// Get color class for variable type icon
+const getTypeColorClass = (type) => {
+ switch (type) {
+ case 'string': return 'bg-blue-500';
+ case 'int':
+ case 'decimal': return 'bg-purple-500';
+ case 'object': return 'bg-green-500';
+ case 'datetime':
+ case 'date': return 'bg-amber-500';
+ case 'boolean': return 'bg-gray-500';
+ default: return 'bg-gray-500';
+ }
+};
+
+// Get node type icon
+const getNodeTypeIcon = (nodeType) => {
+ switch (nodeType) {
+ case 'form': return 'material-symbols:description';
+ case 'api': return 'material-symbols:api';
+ case 'script': return 'material-symbols:code';
+ case 'decision': return 'material-symbols:fork-right';
+ case 'businessRule': return 'material-symbols:rule';
+ case 'notification': return 'material-symbols:notifications';
+ case 'start': return 'material-symbols:play-circle';
+ case 'end': return 'material-symbols:stop-circle';
+ default: return 'material-symbols:device-hub';
+ }
+};
+
+// Get node type color class
+const getNodeTypeColor = (nodeType) => {
+ switch (nodeType) {
+ case 'form': return 'bg-blue-500';
+ case 'api': return 'bg-green-500';
+ case 'script': return 'bg-purple-500';
+ case 'decision': return 'bg-amber-500';
+ case 'businessRule': return 'bg-red-500';
+ case 'notification': return 'bg-orange-500';
+ case 'start': return 'bg-green-600';
+ case 'end': return 'bg-red-600';
+ default: return 'bg-gray-500';
+ }
+};
+
+// Navigate to a specific node in the canvas
+const navigateToNode = (nodeId) => {
+ console.log('VariableManager: navigateToNode called with:', nodeId);
+
+ // Close the modal first
+ showUsageModal.value = false;
+
+ // Find the node and center the view on it
+ const nodes = processStore.currentProcess?.nodes || [];
+ const node = nodes.find(n => n.id === nodeId);
+
+ console.log('VariableManager: Found node:', node);
+ console.log('VariableManager: Available nodes:', nodes.map(n => ({ id: n.id, type: n.type, label: n.data?.label })));
+
+ if (node) {
+ // Select the node in the process store
+ processStore.selectNode(nodeId);
+
+ // Emit a custom event that the parent can listen to for highlighting and navigation
+ const event = new CustomEvent('highlightNode', {
+ detail: { nodeId, node }
+ });
+
+ console.log('VariableManager: Dispatching event:', event.detail);
+ window.dispatchEvent(event);
+
+ console.log(`Navigating to node: ${nodeId}`, node);
+ } else {
+ console.warn(`Node with ID ${nodeId} not found in current process`);
+ console.log('Available nodes:', nodes);
+ }
+};
+
+// Edit variable from usage modal
+const editVariableFromUsage = () => {
+ if (selectedVariable.value) {
+ showUsageModal.value = false;
+ editVariable(selectedVariable.value);
+ }
+};
\ No newline at end of file
diff --git a/stores/processBuilder.js b/stores/processBuilder.js
index 1c03a7d..5f33582 100644
--- a/stores/processBuilder.js
+++ b/stores/processBuilder.js
@@ -247,8 +247,6 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
updatedAt: apiProcess.processModifiedDate
};
-
-
this.currentProcess = process;
// Variables are already loaded in process.variables, no need to sync with variable store
@@ -307,12 +305,29 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
// 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,
- nodes: this.currentProcess.nodes,
- edges: this.currentProcess.edges,
- viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
+ // 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 || {}
@@ -326,7 +341,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
nodeCount: processData.nodes.length,
edgeCount: processData.edges.length,
variableCount: Object.keys(processVariables).length,
- nodes: processData.nodes.map(n => ({ id: n.id, type: n.type, label: n.label })),
+ 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 }))
});
@@ -340,6 +356,9 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
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) {
@@ -357,6 +376,81 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
}
},
+ /**
+ * 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
+ };
+
+ console.log('๐งน Cleaned flow data:', {
+ originalNodes: flowData.nodes?.length || 0,
+ cleanNodes: cleanNodes.length,
+ originalEdges: flowData.edges?.length || 0,
+ cleanEdges: cleanEdges.length
+ });
+
+ return {
+ nodes: cleanNodes,
+ edges: cleanEdges,
+ viewport: cleanViewport
+ };
+ },
+
/**
* Delete a process
*/