Enhance Process Builder with Mobile Responsiveness and Node Highlighting
- Implemented mobile responsive states for the process builder, allowing for better usability on smaller screens by auto-hiding panels and providing toggle functionality. - Added node highlighting capabilities to improve user interaction, enabling users to easily identify and navigate to specific nodes within the process flow. - Updated the ProcessFlowCanvas component to support centering on highlighted nodes, enhancing the visual experience during node selection. - Improved overall layout and interaction design for mobile and tablet devices, ensuring a seamless user experience across different screen sizes.
This commit is contained in:
parent
75e5e6f97e
commit
b2692bec73
@ -101,7 +101,7 @@ watch(
|
||||
class="modal-dialog"
|
||||
:class="dialogClass"
|
||||
:style="{
|
||||
width: size == 'sm' ? '300px' : size == 'md' ? '500px' : '800px',
|
||||
width: size == 'sm' ? '300px' : size == 'md' ? '500px' : size == 'lg' ? '800px' : size == 'xl' ? '1000px' : '1200px',
|
||||
}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
|
@ -497,7 +497,7 @@ const getPathSummary = (group) => {
|
||||
<div class="flex items-center">
|
||||
<!-- Condition count badge -->
|
||||
<div class="text-xs text-gray-500 bg-white px-2 py-0.5 rounded-full border mr-2">
|
||||
{{ group.conditions.length }} condition{{ group.conditions.length !== 1 ? 's' : '' }}
|
||||
{{ (group.conditions || []).length }} condition{{ (group.conditions || []).length !== 1 ? 's' : '' }}
|
||||
</div>
|
||||
|
||||
<!-- Collapse/expand button -->
|
||||
@ -557,7 +557,7 @@ const getPathSummary = (group) => {
|
||||
<!-- Individual conditions -->
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, conditionIndex) in group.conditions"
|
||||
v-for="(condition, conditionIndex) in (group.conditions || [])"
|
||||
:key="condition.id"
|
||||
class="condition-item p-3 border rounded-md bg-white shadow-sm hover:shadow-md transition-shadow duration-200"
|
||||
>
|
||||
@ -565,7 +565,7 @@ const getPathSummary = (group) => {
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5 class="text-xs font-medium">Condition {{ conditionIndex + 1 }}</h5>
|
||||
<button
|
||||
v-if="group.conditions.length > 1"
|
||||
v-if="(group.conditions || []).length > 1"
|
||||
@click.stop="removeConditionFromGroup(groupIndex, conditionIndex)"
|
||||
class="text-gray-400 hover:text-red-500"
|
||||
title="Remove condition"
|
||||
|
@ -19,18 +19,18 @@
|
||||
<!-- Core Components -->
|
||||
<div class="component-category mb-6">
|
||||
<h3 class="text-gray-700 text-sm font-medium px-3 mb-2">Core Components</h3>
|
||||
<div class="grid grid-cols-2 gap-1 px-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 px-2">
|
||||
<div
|
||||
v-for="component in getComponentsByCategory('Core')"
|
||||
:key="component.type"
|
||||
class="component-item rounded p-2 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200"
|
||||
class="component-item rounded p-3 flex flex-col items-center justify-center cursor-grab hover:bg-gray-100 transition-colors border border-gray-200 touch-manipulation"
|
||||
:class="{ 'hidden': !matchesSearch(component) }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart($event, component)"
|
||||
@click="addComponent(component)"
|
||||
>
|
||||
<Icon :name="component.icon" class="mb-1 w-5 h-5 text-gray-600" />
|
||||
<span class="text-xs text-gray-600 text-center">{{ component.name }}</span>
|
||||
<Icon :name="component.icon" class="mb-2 w-6 h-6 text-gray-600" />
|
||||
<span class="text-xs text-gray-600 text-center leading-tight">{{ component.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
383
docs/process-builder/BUSINESS_RULES_TROUBLESHOOTING.md
Normal file
383
docs/process-builder/BUSINESS_RULES_TROUBLESHOOTING.md
Normal file
@ -0,0 +1,383 @@
|
||||
# Business Rules Node Troubleshooting Guide
|
||||
|
||||
## Overview
|
||||
The Business Rules node allows you to define complex conditional logic in your process flows. This guide addresses common issues with variable dropdowns and operator selection.
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### 1. Variable Dropdown Not Populating
|
||||
|
||||
**Problem**: The variable dropdown in business rules conditions shows "Select variable" but no options appear.
|
||||
|
||||
**Root Causes**:
|
||||
- Process variables are not properly loaded in the store
|
||||
- Variable types are not correctly defined
|
||||
- The `gatewayAvailableVariables` computed property is not updating
|
||||
|
||||
**Solutions**:
|
||||
|
||||
#### A. Check Variable Store State
|
||||
```javascript
|
||||
// In browser console, check if variables are loaded:
|
||||
console.log('Current process variables:', processStore.currentProcess?.variables);
|
||||
console.log('Gateway available variables:', gatewayAvailableVariables.value);
|
||||
```
|
||||
|
||||
#### B. Ensure Proper Variable Types
|
||||
Variables must use these specific types that match the VariableBrowser component:
|
||||
- `string` - for text values
|
||||
- `int` - for whole numbers
|
||||
- `decimal` - for decimal numbers
|
||||
- `boolean` - for true/false values
|
||||
- `date` - for date values
|
||||
- `datetime` - for date-time values
|
||||
- `object` - for JSON objects
|
||||
|
||||
**Incorrect**:
|
||||
```json
|
||||
{
|
||||
"myVariable": {
|
||||
"type": "number", // ❌ Should be "int" or "decimal"
|
||||
"value": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Correct**:
|
||||
```json
|
||||
{
|
||||
"myVariable": {
|
||||
"type": "int", // ✅ Correct type
|
||||
"value": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### C. Force Component Re-render
|
||||
If variables exist but dropdown doesn't update, the issue is with reactivity. The component uses a `variablesUpdateKey` to force re-renders:
|
||||
|
||||
```javascript
|
||||
// This key changes when variables are updated
|
||||
const variablesUpdateKey = computed(() => {
|
||||
// Creates hash of variable names to detect changes
|
||||
const variableNames = Object.keys(processStore.currentProcess.variables).sort().join(',');
|
||||
return `vars-${variableNames.length}-${hash}`;
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Operator Dropdown Not Showing
|
||||
|
||||
**Problem**: After selecting a variable, the operator dropdown remains empty.
|
||||
|
||||
**Root Cause**: The `getOperatorsForType()` function cannot determine the variable type.
|
||||
|
||||
**Solution**: Ensure the variable has a proper type. The function expects these types:
|
||||
|
||||
```javascript
|
||||
const getOperatorsForType = (variableType) => {
|
||||
switch (variableType?.toLowerCase()) {
|
||||
case 'string':
|
||||
return [
|
||||
{ value: 'eq', label: '= (Equal to)' },
|
||||
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||
{ value: 'contains', label: 'Contains' },
|
||||
{ value: 'not_contains', label: 'Does not contain' },
|
||||
{ value: 'starts_with', label: 'Starts with' },
|
||||
{ value: 'ends_with', label: 'Ends with' },
|
||||
{ value: 'is_empty', label: 'Is empty' },
|
||||
{ value: 'is_not_empty', label: 'Is not empty' }
|
||||
];
|
||||
case 'int':
|
||||
case 'decimal':
|
||||
return [
|
||||
{ value: 'eq', label: '= (Equal to)' },
|
||||
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||
{ value: 'gt', label: '> (Greater than)' },
|
||||
{ value: 'gte', label: '≥ (Greater than or equal to)' },
|
||||
{ value: 'lt', label: '< (Less than)' },
|
||||
{ value: 'lte', label: '≤ (Less than or equal to)' },
|
||||
{ value: 'between', label: 'Between (inclusive)' }
|
||||
];
|
||||
case 'boolean':
|
||||
return [
|
||||
{ value: 'eq', label: '= (Equal to)' },
|
||||
{ value: 'is_true', label: 'Is True' },
|
||||
{ value: 'is_false', label: 'Is False' }
|
||||
];
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
return [
|
||||
{ value: 'eq', label: '= (Equal to)' },
|
||||
{ value: 'neq', label: '≠ (Not equal to)' },
|
||||
{ value: 'gt', label: '> (After)' },
|
||||
{ value: 'gte', label: '≥ (On or after)' },
|
||||
{ value: 'lt', label: '< (Before)' },
|
||||
{ value: 'lte', label: '≤ (On or before)' },
|
||||
{ value: 'between', label: 'Between dates' }
|
||||
];
|
||||
default:
|
||||
return [
|
||||
{ value: 'eq', label: '= (Equal to)' },
|
||||
{ value: 'neq', label: '≠ (Not equal to)' }
|
||||
];
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Variable Browser Component Issues
|
||||
|
||||
**Problem**: VariableBrowser component doesn't show variables even when they exist.
|
||||
|
||||
**Debugging Steps**:
|
||||
|
||||
1. **Check Props**: Ensure `availableVariables` prop is passed correctly:
|
||||
```vue
|
||||
<VariableBrowser
|
||||
v-model="condition.variable"
|
||||
:availableVariables="props.availableVariables" <!-- ✅ Correct prop passing -->
|
||||
:allowCreate="true"
|
||||
@change="updateConditionVariable(groupIndex, condIndex)"
|
||||
/>
|
||||
```
|
||||
|
||||
2. **Verify Variable Structure**: Each variable must have this structure:
|
||||
```javascript
|
||||
{
|
||||
name: "variableName",
|
||||
type: "string|int|decimal|boolean|date|datetime|object",
|
||||
scope: "global|local",
|
||||
description: "Variable description",
|
||||
currentValue: null // Optional current value for preview
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check Grouping Logic**: The VariableBrowser groups variables by type:
|
||||
```javascript
|
||||
const groupedVariables = computed(() => {
|
||||
const types = {
|
||||
string: { label: 'Text Variables', variables: [] },
|
||||
int: { label: 'Integer Variables', variables: [] },
|
||||
decimal: { label: 'Decimal Variables', variables: [] },
|
||||
boolean: { label: 'Boolean Variables', variables: [] },
|
||||
date: { label: 'Date Variables', variables: [] },
|
||||
datetime: { label: 'DateTime Variables', variables: [] },
|
||||
object: { label: 'Object Variables', variables: [] }
|
||||
};
|
||||
|
||||
// Variables are grouped by type
|
||||
variables.forEach(variable => {
|
||||
const type = variable.type || 'string';
|
||||
if (types[type]) {
|
||||
types[type].variables.push(variable);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(types).filter(group => group.variables.length > 0);
|
||||
});
|
||||
```
|
||||
|
||||
## Correct Business Rule Structure
|
||||
|
||||
### Complete Working Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "business-rule-123",
|
||||
"type": "business-rule",
|
||||
"label": "Eligibility Check",
|
||||
"position": { "x": 400, "y": 200 },
|
||||
"data": {
|
||||
"label": "Eligibility Check",
|
||||
"description": "Check if applicant meets eligibility criteria",
|
||||
"ruleGroups": [
|
||||
{
|
||||
"id": "group-1",
|
||||
"name": "Age and Status Check",
|
||||
"operator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "condition-1-1",
|
||||
"variable": "applicantAge",
|
||||
"operator": "greater_than",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"id": "condition-1-2",
|
||||
"variable": "maritalStatus",
|
||||
"operator": "equals",
|
||||
"value": "married"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "action-1-1",
|
||||
"type": "set_variable",
|
||||
"variable": "isEligible",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputVariable": "ruleResult",
|
||||
"errorVariable": "ruleError"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Required Process Variables
|
||||
|
||||
```json
|
||||
{
|
||||
"applicantAge": {
|
||||
"name": "applicantAge",
|
||||
"type": "int",
|
||||
"scope": "global",
|
||||
"value": 25,
|
||||
"description": "Applicant's age in years"
|
||||
},
|
||||
"maritalStatus": {
|
||||
"name": "maritalStatus",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "married",
|
||||
"description": "Applicant's marital status"
|
||||
},
|
||||
"isEligible": {
|
||||
"name": "isEligible",
|
||||
"type": "boolean",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "Whether applicant is eligible"
|
||||
},
|
||||
"ruleResult": {
|
||||
"name": "ruleResult",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "Business rule execution result"
|
||||
},
|
||||
"ruleError": {
|
||||
"name": "ruleError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "Business rule execution error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### 1. Browser Console Debugging
|
||||
```javascript
|
||||
// Check if variables are loaded
|
||||
console.log('Process variables:', processStore.currentProcess?.variables);
|
||||
|
||||
// Check available variables for business rules
|
||||
console.log('Available variables:', gatewayAvailableVariables.value);
|
||||
|
||||
// Check selected node data
|
||||
console.log('Selected node:', selectedNodeData.value);
|
||||
|
||||
// Debug operator dropdown issue
|
||||
const testVariable = 'selectStatusperkahwinan';
|
||||
const foundVar = gatewayAvailableVariables.value.find(v => v.name === testVariable);
|
||||
console.log('Found variable:', foundVar);
|
||||
console.log('Variable type:', foundVar?.type);
|
||||
|
||||
// Test getOperatorsForType function (if available in component)
|
||||
if (typeof getOperatorsForType === 'function') {
|
||||
console.log('Operators for string:', getOperatorsForType('string'));
|
||||
console.log('Operators for boolean:', getOperatorsForType('boolean'));
|
||||
console.log('Operators for decimal:', getOperatorsForType('decimal'));
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Component Key Debugging
|
||||
If the component isn't updating, check the key attribute:
|
||||
```vue
|
||||
<BusinessRuleNodeConfigurationModal
|
||||
:key="`business-rule-${selectedNodeData.id}-${variablesUpdateKey}`"
|
||||
<!-- This key should change when variables update -->
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. Variable Type Validation
|
||||
```javascript
|
||||
// Validate variable types
|
||||
const validateVariableTypes = (variables) => {
|
||||
const validTypes = ['string', 'int', 'decimal', 'boolean', 'date', 'datetime', 'object'];
|
||||
|
||||
Object.values(variables).forEach(variable => {
|
||||
if (!validTypes.includes(variable.type)) {
|
||||
console.error(`Invalid variable type: ${variable.type} for ${variable.name}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always define variable types correctly** - Use the exact types expected by the VariableBrowser
|
||||
2. **Include unique IDs** - Each condition and action should have a unique ID
|
||||
3. **Test incrementally** - Add one condition at a time to isolate issues
|
||||
4. **Use descriptive names** - Make variable and rule names clear and meaningful
|
||||
5. **Check browser console** - Most issues will show up in console errors
|
||||
|
||||
## Operator Values Reference
|
||||
|
||||
Use these exact operator values in your JSON definitions:
|
||||
|
||||
### String Operators
|
||||
- `eq` - Equal to
|
||||
- `neq` - Not equal to
|
||||
- `contains` - Contains substring
|
||||
- `not_contains` - Does not contain substring
|
||||
- `starts_with` - Starts with
|
||||
- `ends_with` - Ends with
|
||||
- `is_empty` - Is empty
|
||||
- `is_not_empty` - Is not empty
|
||||
|
||||
### Number Operators (int/decimal)
|
||||
- `eq` - Equal to
|
||||
- `neq` - Not equal to
|
||||
- `gt` - Greater than
|
||||
- `gte` - Greater than or equal to
|
||||
- `lt` - Less than
|
||||
- `lte` - Less than or equal to
|
||||
- `between` - Between (inclusive)
|
||||
|
||||
### Boolean Operators
|
||||
- `eq` - Equal to
|
||||
- `is_true` - Is True
|
||||
- `is_false` - Is False
|
||||
|
||||
### Date/DateTime Operators
|
||||
- `eq` - Equal to
|
||||
- `neq` - Not equal to
|
||||
- `gt` - After
|
||||
- `gte` - On or after
|
||||
- `lt` - Before
|
||||
- `lte` - On or before
|
||||
- `between` - Between dates
|
||||
|
||||
## Common Error Messages
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "Variable not found" | Variable name doesn't match exactly | Check spelling and case sensitivity |
|
||||
| "No operators available" | Variable type not recognized | Ensure variable type is one of the supported types |
|
||||
| "Dropdown not populating" | Variables not loaded or wrong structure | Check variable store and structure |
|
||||
| "Component not updating" | Reactivity issue | Check component key and force re-render |
|
||||
| "Operator dropdown empty" | Using wrong operator values | Use correct operator values from reference above |
|
||||
|
||||
## Final Checklist
|
||||
|
||||
Before reporting issues, verify:
|
||||
|
||||
- [ ] All variables have correct types (`string`, `int`, `decimal`, `boolean`, `date`, `datetime`, `object`)
|
||||
- [ ] Process variables are loaded in the store
|
||||
- [ ] Component receives `availableVariables` prop
|
||||
- [ ] Each condition/action has unique ID
|
||||
- [ ] Browser console shows no errors
|
||||
- [ ] Variable names match exactly between definition and usage
|
833
docs/process-builder/PROCESS_FLOW_CREATION_GUIDE.md
Normal file
833
docs/process-builder/PROCESS_FLOW_CREATION_GUIDE.md
Normal file
@ -0,0 +1,833 @@
|
||||
# Process Flow Creation Guide
|
||||
|
||||
## Overview
|
||||
This guide explains how to create process flows using the node-based process builder. Process flows are defined as JSON structures containing nodes and edges that represent business processes.
|
||||
|
||||
## Process Definition Structure
|
||||
|
||||
### Root Structure
|
||||
```json
|
||||
{
|
||||
"nodes": [...],
|
||||
"edges": [...],
|
||||
"viewport": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"zoom": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Node Types
|
||||
|
||||
### 1. Start Node
|
||||
**Purpose**: Entry point for the process
|
||||
```json
|
||||
{
|
||||
"id": "start-{timestamp}",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"position": {"x": 210, "y": 180},
|
||||
"data": {
|
||||
"label": "Start",
|
||||
"description": "Process start point"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Form Node
|
||||
**Purpose**: User task requiring form completion
|
||||
```json
|
||||
{
|
||||
"id": "form-{timestamp}",
|
||||
"type": "form",
|
||||
"label": "Form Name",
|
||||
"position": {"x": 405, "y": 135},
|
||||
"data": {
|
||||
"label": "Borang Maklumat Peribadi",
|
||||
"formId": 1,
|
||||
"formName": "Borang Maklumat Peribadi",
|
||||
"formUuid": "9f08fc8f-b170-478a-85fd-fa3364401533",
|
||||
"description": "Form: Borang Maklumat Peribadi",
|
||||
"assignmentType": "public|role|user",
|
||||
"assignedRoles": [],
|
||||
"assignedUsers": [],
|
||||
"assignmentVariable": "",
|
||||
"assignmentVariableType": "user_id",
|
||||
"inputMappings": [
|
||||
{
|
||||
"formField": "reference_number",
|
||||
"processVariable": "applicationId"
|
||||
}
|
||||
],
|
||||
"outputMappings": [
|
||||
{
|
||||
"formField": "text_3",
|
||||
"processVariable": "text3"
|
||||
}
|
||||
],
|
||||
"fieldConditions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Assignment Types:**
|
||||
- `public`: Anyone can complete the form
|
||||
- `role`: Assigned to specific roles
|
||||
- `user`: Assigned to specific users
|
||||
|
||||
### 3. API Node
|
||||
**Purpose**: External API calls
|
||||
```json
|
||||
{
|
||||
"id": "api-{timestamp}",
|
||||
"type": "api",
|
||||
"label": "Submit Profile API",
|
||||
"position": {"x": 600, "y": 135},
|
||||
"data": {
|
||||
"label": "Submit Profile API",
|
||||
"description": "Submit user profile to external system",
|
||||
"apiUrl": "https://jsonplaceholder.typicode.com/posts",
|
||||
"apiMethod": "GET|POST|PUT|DELETE|PATCH",
|
||||
"headers": "{ \"Content-Type\": \"application/json\" }",
|
||||
"requestBody": "{\n \"applicantName\": \"{text3}\",\n \"idType\": \"{select1}\"\n}",
|
||||
"outputVariable": "apiResponse",
|
||||
"errorVariable": "apiError",
|
||||
"continueOnError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Methods**: GET, POST, PUT, DELETE, PATCH
|
||||
**Variable References**: Use `{variableName}` syntax in URL, headers, and body
|
||||
|
||||
### 4. Script Node
|
||||
**Purpose**: Data processing and transformation
|
||||
```json
|
||||
{
|
||||
"id": "script-{timestamp}",
|
||||
"type": "script",
|
||||
"label": "Process API Response",
|
||||
"position": {"x": 800, "y": 135},
|
||||
"data": {
|
||||
"label": "Process API Response",
|
||||
"description": "Transform API response for document verification step",
|
||||
"scriptLanguage": "javascript",
|
||||
"scriptCode": "// Extract important data from API response\nconst apiData = processVariables.apiResponse;\n\nif (apiData && apiData.data) {\n processVariables.applicationId = apiData.data.id || 'APP-' + Date.now();\n}",
|
||||
"inputVariables": ["apiResponse", "text3", "radioPendidikan"],
|
||||
"outputVariables": [
|
||||
{
|
||||
"name": "applicationId",
|
||||
"type": "string",
|
||||
"description": "Generated application ID"
|
||||
}
|
||||
],
|
||||
"errorVariable": "scriptError",
|
||||
"continueOnError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Script Languages**: javascript, python
|
||||
**Variable Access**: Use `processVariables.variableName` to access/set variables
|
||||
|
||||
### 5. Decision Node (Gateway)
|
||||
**Purpose**: Conditional branching
|
||||
```json
|
||||
{
|
||||
"id": "gateway-{timestamp}",
|
||||
"type": "gateway",
|
||||
"label": "Check Documents",
|
||||
"position": {"x": 700, "y": 200},
|
||||
"data": {
|
||||
"label": "Check Documents",
|
||||
"description": "Route based on document requirements",
|
||||
"gatewayType": "exclusive",
|
||||
"conditions": [
|
||||
{
|
||||
"id": "condition-1",
|
||||
"label": "Documents Required",
|
||||
"variable": "documentsRequired",
|
||||
"operator": "equals",
|
||||
"value": true,
|
||||
"targetNode": "form-verification"
|
||||
},
|
||||
{
|
||||
"id": "condition-2",
|
||||
"label": "No Documents",
|
||||
"variable": "documentsRequired",
|
||||
"operator": "equals",
|
||||
"value": false,
|
||||
"targetNode": "end-approved"
|
||||
}
|
||||
],
|
||||
"defaultPath": "end-rejected"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Gateway Types**: exclusive, inclusive, parallel
|
||||
**Operators**: equals, not_equals, greater_than, less_than, contains
|
||||
|
||||
### 6. Business Rule Node
|
||||
**Purpose**: Complex business logic evaluation
|
||||
```json
|
||||
{
|
||||
"id": "business-rule-{timestamp}",
|
||||
"type": "business-rule",
|
||||
"label": "Eligibility Rules",
|
||||
"position": {"x": 900, "y": 200},
|
||||
"data": {
|
||||
"label": "Eligibility Rules",
|
||||
"description": "Evaluate application eligibility",
|
||||
"ruleGroups": [
|
||||
{
|
||||
"id": "group-1",
|
||||
"name": "Age Requirements",
|
||||
"operator": "AND",
|
||||
"conditions": [
|
||||
{
|
||||
"variable": "applicantAge",
|
||||
"operator": "greater_than",
|
||||
"value": 18
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "set_variable",
|
||||
"variable": "ageEligible",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputVariable": "ruleResult",
|
||||
"errorVariable": "ruleError"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Notification Node
|
||||
**Purpose**: Send notifications
|
||||
```json
|
||||
{
|
||||
"id": "notification-{timestamp}",
|
||||
"type": "notification",
|
||||
"label": "Send Approval Email",
|
||||
"position": {"x": 1000, "y": 200},
|
||||
"data": {
|
||||
"label": "Send Approval Email",
|
||||
"description": "Notify applicant of approval",
|
||||
"notificationType": "email|sms|push",
|
||||
"recipientType": "variable|role|user|email",
|
||||
"recipientValue": "applicantEmail",
|
||||
"subject": "Application Approved - {applicationId}",
|
||||
"message": "Dear {applicantName},\n\nYour application {applicationId} has been approved.",
|
||||
"template": "approval-template",
|
||||
"errorVariable": "notificationError"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notification Types**: email, sms, push, internal
|
||||
**Recipient Types**: variable, role, user, email
|
||||
|
||||
### 8. End Node
|
||||
**Purpose**: Process termination
|
||||
```json
|
||||
{
|
||||
"id": "end-{timestamp}",
|
||||
"type": "end",
|
||||
"label": "End",
|
||||
"position": {"x": 1200, "y": 180},
|
||||
"data": {
|
||||
"label": "End",
|
||||
"description": "Process completion point"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Edges (Connections)
|
||||
|
||||
### Basic Edge
|
||||
```json
|
||||
{
|
||||
"id": "{sourceId}-{targetId}-{timestamp}",
|
||||
"source": "start-1751870920411",
|
||||
"target": "form-1751870928350",
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"label": "",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Edge (from Gateway)
|
||||
```json
|
||||
{
|
||||
"id": "gateway-condition-{timestamp}",
|
||||
"source": "gateway-1751870920411",
|
||||
"target": "form-1751870928350",
|
||||
"type": "smoothstep",
|
||||
"animated": true,
|
||||
"label": "Documents Required",
|
||||
"data": {
|
||||
"condition": "documentsRequired == true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Process Variables
|
||||
|
||||
### Variable Definition Structure
|
||||
Process variables are stored as a JSON object where each variable has a specific structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"variableName": {
|
||||
"name": "variableName",
|
||||
"type": "string|number|boolean|object|array",
|
||||
"scope": "global|local",
|
||||
"value": null,
|
||||
"description": "Variable description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Process Variables Format
|
||||
Based on the actual database structure, here's the complete format for process variables:
|
||||
|
||||
```json
|
||||
{
|
||||
"text3": {
|
||||
"name": "text3",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "Ahmad Bin Ali",
|
||||
"description": "Form data from text_3 in Borang Maklumat Peribadi"
|
||||
},
|
||||
"select1": {
|
||||
"name": "select1",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "staff",
|
||||
"description": "Form data from select_1 in Borang Maklumat Peribadi"
|
||||
},
|
||||
"apiResponse": {
|
||||
"name": "apiResponse",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": {
|
||||
"data": {
|
||||
"id": "APP-2024-001234",
|
||||
"score": 85,
|
||||
"status": "pending_verification",
|
||||
"profileId": "PROF-567890",
|
||||
"riskLevel": "low",
|
||||
"documentCount": 3,
|
||||
"submissionDate": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
"status": "success",
|
||||
"message": "Profile submitted successfully"
|
||||
},
|
||||
"description": "API response from Submit Profile Data"
|
||||
},
|
||||
"radioJantina": {
|
||||
"name": "radioJantina",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "Lelaki",
|
||||
"description": "Form data from radio_jantina in Borang Maklumat Peribadi"
|
||||
},
|
||||
"documentsRequired": {
|
||||
"name": "documentsRequired",
|
||||
"type": "boolean",
|
||||
"scope": "global",
|
||||
"value": true,
|
||||
"description": "Whether documents verification is required"
|
||||
},
|
||||
"applicationId": {
|
||||
"name": "applicationId",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "APP-1751871528249",
|
||||
"description": "Generated application ID from script processing"
|
||||
},
|
||||
"apiError": {
|
||||
"name": "apiError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "API error from Submit Profile Data"
|
||||
},
|
||||
"scriptError": {
|
||||
"name": "scriptError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "Error information from script execution"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Variable Types and Examples
|
||||
|
||||
#### String Variables
|
||||
```json
|
||||
{
|
||||
"applicantName": {
|
||||
"name": "applicantName",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "Ahmad Bin Ali",
|
||||
"description": "Applicant's full name"
|
||||
},
|
||||
"verificationLevel": {
|
||||
"name": "verificationLevel",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "enhanced",
|
||||
"description": "Level of verification required (standard/enhanced)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Number Variables
|
||||
```json
|
||||
{
|
||||
"profileScore": {
|
||||
"name": "profileScore",
|
||||
"type": "number",
|
||||
"scope": "global",
|
||||
"value": 85,
|
||||
"description": "Profile evaluation score from API"
|
||||
},
|
||||
"requestAmount": {
|
||||
"name": "requestAmount",
|
||||
"type": "number",
|
||||
"scope": "global",
|
||||
"value": 1500.50,
|
||||
"description": "Requested loan amount"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Boolean Variables
|
||||
```json
|
||||
{
|
||||
"documentsRequired": {
|
||||
"name": "documentsRequired",
|
||||
"type": "boolean",
|
||||
"scope": "global",
|
||||
"value": true,
|
||||
"description": "Whether documents verification is required"
|
||||
},
|
||||
"nextStepReady": {
|
||||
"name": "nextStepReady",
|
||||
"type": "boolean",
|
||||
"scope": "global",
|
||||
"value": false,
|
||||
"description": "Whether ready for next step in process"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Object Variables
|
||||
```json
|
||||
{
|
||||
"apiResponse": {
|
||||
"name": "apiResponse",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": {
|
||||
"data": {
|
||||
"id": "APP-2024-001234",
|
||||
"score": 85,
|
||||
"status": "pending_verification"
|
||||
},
|
||||
"status": "success",
|
||||
"message": "Profile submitted successfully"
|
||||
},
|
||||
"description": "Complete API response object"
|
||||
},
|
||||
"userProfile": {
|
||||
"name": "userProfile",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": {
|
||||
"name": "Ahmad Bin Ali",
|
||||
"email": "ahmad@example.com",
|
||||
"phone": "+60123456789",
|
||||
"address": {
|
||||
"street": "Jalan Bangsar",
|
||||
"city": "Kuala Lumpur",
|
||||
"postcode": "59000"
|
||||
}
|
||||
},
|
||||
"description": "User profile information"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Array Variables
|
||||
```json
|
||||
{
|
||||
"assignedRoles": {
|
||||
"name": "assignedRoles",
|
||||
"type": "array",
|
||||
"scope": "global",
|
||||
"value": ["manager", "supervisor", "admin"],
|
||||
"description": "List of roles assigned to task"
|
||||
},
|
||||
"documentTypes": {
|
||||
"name": "documentTypes",
|
||||
"type": "array",
|
||||
"scope": "global",
|
||||
"value": [
|
||||
{
|
||||
"type": "IC",
|
||||
"required": true,
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"type": "Passport",
|
||||
"required": false,
|
||||
"status": "not_required"
|
||||
}
|
||||
],
|
||||
"description": "Array of document requirement objects"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Date Variables
|
||||
```json
|
||||
{
|
||||
"dateMasukislam": {
|
||||
"name": "dateMasukislam",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "2020-05-15",
|
||||
"description": "Islam conversion date in YYYY-MM-DD format"
|
||||
},
|
||||
"verificationDate": {
|
||||
"name": "verificationDate",
|
||||
"type": "string",
|
||||
"scope": "global",
|
||||
"value": "2024-01-15T10:30:00Z",
|
||||
"description": "ISO 8601 formatted verification timestamp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Variable Scopes
|
||||
- **global**: Available throughout the entire process
|
||||
- **local**: Available only within the current node/task
|
||||
|
||||
### Variable Naming Conventions
|
||||
- Use **camelCase** for consistency (e.g., `applicantName`, `verificationLevel`)
|
||||
- Form field mappings often use **snake_case** to match form field names
|
||||
- Keep names descriptive and meaningful
|
||||
- Avoid spaces and special characters
|
||||
- Use consistent prefixes for related variables
|
||||
|
||||
### Variable Usage Patterns
|
||||
|
||||
#### In API Calls
|
||||
```json
|
||||
{
|
||||
"apiUrl": "https://api.example.com/users/{userId}/profile",
|
||||
"headers": "{ \"Authorization\": \"Bearer {apiToken}\", \"Content-Type\": \"application/json\" }",
|
||||
"requestBody": "{\n \"name\": \"{applicantName}\",\n \"score\": {profileScore},\n \"verified\": {documentsRequired}\n}"
|
||||
}
|
||||
```
|
||||
|
||||
#### In Script Nodes
|
||||
```javascript
|
||||
// Accessing variables
|
||||
const applicantName = processVariables.applicantName;
|
||||
const score = processVariables.profileScore;
|
||||
const apiData = processVariables.apiResponse;
|
||||
|
||||
// Setting variables
|
||||
processVariables.applicationId = 'APP-' + Date.now();
|
||||
processVariables.verificationLevel = score > 80 ? 'enhanced' : 'standard';
|
||||
processVariables.nextStepReady = true;
|
||||
|
||||
// Working with objects
|
||||
if (processVariables.apiResponse && processVariables.apiResponse.data) {
|
||||
processVariables.externalId = processVariables.apiResponse.data.id;
|
||||
}
|
||||
```
|
||||
|
||||
#### In Gateway Conditions
|
||||
```json
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"variable": "profileScore",
|
||||
"operator": "greater_than",
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"variable": "documentsRequired",
|
||||
"operator": "equals",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"variable": "verificationLevel",
|
||||
"operator": "equals",
|
||||
"value": "enhanced"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### In Notifications
|
||||
```json
|
||||
{
|
||||
"subject": "Application {applicationId} - Status Update",
|
||||
"message": "Dear {applicantName},\n\nYour application {applicationId} has been processed.\nScore: {profileScore}\nStatus: {verificationLevel}\n\nThank you."
|
||||
}
|
||||
```
|
||||
|
||||
#### In Form Mappings
|
||||
```json
|
||||
{
|
||||
"inputMappings": [
|
||||
{
|
||||
"formField": "reference_number",
|
||||
"processVariable": "applicationId"
|
||||
},
|
||||
{
|
||||
"formField": "applicant_name",
|
||||
"processVariable": "applicantName"
|
||||
}
|
||||
],
|
||||
"outputMappings": [
|
||||
{
|
||||
"formField": "verification_result",
|
||||
"processVariable": "verificationResult"
|
||||
},
|
||||
{
|
||||
"formField": "verification_notes",
|
||||
"processVariable": "verificationNotes"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Error Variables
|
||||
Always include error variables for nodes that can fail:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiError": {
|
||||
"name": "apiError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "API error from Submit Profile Data"
|
||||
},
|
||||
"scriptError": {
|
||||
"name": "scriptError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "Error information from script execution"
|
||||
},
|
||||
"verificationApiError": {
|
||||
"name": "verificationApiError",
|
||||
"type": "object",
|
||||
"scope": "global",
|
||||
"value": null,
|
||||
"description": "API error from verification submission"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Variable Best Practices
|
||||
1. **Initialize with null values** for variables that will be set during process execution
|
||||
2. **Provide clear descriptions** explaining the variable's purpose and source
|
||||
3. **Use consistent typing** - don't mix string and number types for the same concept
|
||||
4. **Group related variables** with similar naming patterns
|
||||
5. **Plan for error handling** by including error variables
|
||||
6. **Document data formats** especially for dates and complex objects
|
||||
|
||||
## Process Settings
|
||||
|
||||
### Basic Settings
|
||||
```json
|
||||
{
|
||||
"owner": "",
|
||||
"category": "HR",
|
||||
"priority": "normal|high|low",
|
||||
"processType": "standard|template",
|
||||
"allowParallel": false,
|
||||
"requireApproval": false,
|
||||
"enableAuditTrail": true,
|
||||
"sendNotifications": true,
|
||||
"executionPermission": "public|authenticated|role",
|
||||
"modificationPermission": "owner|managers|admin"
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Settings
|
||||
```json
|
||||
{
|
||||
"autoTimeout": 24,
|
||||
"maxExecutionTime": 60,
|
||||
"dataPersistence": "short_term|long_term|permanent",
|
||||
"dataRetentionPolicy": "",
|
||||
"enableErrorRecovery": true,
|
||||
"encryptSensitiveData": false,
|
||||
"logVariableChanges": true,
|
||||
"allowedRoles": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example Process
|
||||
|
||||
### Simple Approval Process
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start-001",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"position": {"x": 100, "y": 100},
|
||||
"data": {"label": "Start", "description": "Begin approval process"}
|
||||
},
|
||||
{
|
||||
"id": "form-application",
|
||||
"type": "form",
|
||||
"label": "Application Form",
|
||||
"position": {"x": 300, "y": 100},
|
||||
"data": {
|
||||
"formId": 1,
|
||||
"assignmentType": "public",
|
||||
"outputMappings": [
|
||||
{"formField": "applicant_name", "processVariable": "applicantName"},
|
||||
{"formField": "amount", "processVariable": "requestAmount"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gateway-amount",
|
||||
"type": "gateway",
|
||||
"label": "Check Amount",
|
||||
"position": {"x": 500, "y": 100},
|
||||
"data": {
|
||||
"gatewayType": "exclusive",
|
||||
"conditions": [
|
||||
{
|
||||
"variable": "requestAmount",
|
||||
"operator": "greater_than",
|
||||
"value": 1000,
|
||||
"targetNode": "form-manager-approval"
|
||||
}
|
||||
],
|
||||
"defaultPath": "notification-auto-approve"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "form-manager-approval",
|
||||
"type": "form",
|
||||
"label": "Manager Approval",
|
||||
"position": {"x": 500, "y": 250},
|
||||
"data": {
|
||||
"formId": 2,
|
||||
"assignmentType": "role",
|
||||
"assignedRoles": ["manager"],
|
||||
"inputMappings": [
|
||||
{"formField": "applicant", "processVariable": "applicantName"},
|
||||
{"formField": "amount", "processVariable": "requestAmount"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "notification-auto-approve",
|
||||
"type": "notification",
|
||||
"label": "Auto Approval",
|
||||
"position": {"x": 700, "y": 100},
|
||||
"data": {
|
||||
"notificationType": "email",
|
||||
"recipientType": "variable",
|
||||
"recipientValue": "applicantEmail",
|
||||
"subject": "Application Approved",
|
||||
"message": "Your request for {requestAmount} has been automatically approved."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "end-001",
|
||||
"type": "end",
|
||||
"label": "End",
|
||||
"position": {"x": 900, "y": 100},
|
||||
"data": {"label": "End", "description": "Process completed"}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"id": "e1", "source": "start-001", "target": "form-application", "type": "smoothstep"},
|
||||
{"id": "e2", "source": "form-application", "target": "gateway-amount", "type": "smoothstep"},
|
||||
{"id": "e3", "source": "gateway-amount", "target": "form-manager-approval", "type": "smoothstep", "label": "> 1000"},
|
||||
{"id": "e4", "source": "gateway-amount", "target": "notification-auto-approve", "type": "smoothstep", "label": "≤ 1000"},
|
||||
{"id": "e5", "source": "form-manager-approval", "target": "end-001", "type": "smoothstep"},
|
||||
{"id": "e6", "source": "notification-auto-approve", "target": "end-001", "type": "smoothstep"}
|
||||
],
|
||||
"viewport": {"x": 0, "y": 0, "zoom": 1}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Node Naming
|
||||
- Use descriptive labels that explain the purpose
|
||||
- Include timestamps in IDs for uniqueness
|
||||
- Keep descriptions clear and concise
|
||||
|
||||
### Variable Management
|
||||
- Use consistent naming conventions (camelCase)
|
||||
- Document all variables with descriptions
|
||||
- Group related variables logically
|
||||
|
||||
### Flow Design
|
||||
- Start simple and add complexity gradually
|
||||
- Use gateways for clear decision points
|
||||
- Include error handling paths
|
||||
- Test each node configuration
|
||||
|
||||
### Error Handling
|
||||
- Always define error variables for API and script nodes
|
||||
- Use `continueOnError` appropriately
|
||||
- Include notification nodes for error scenarios
|
||||
- Plan alternative paths for failures
|
||||
|
||||
### Performance Considerations
|
||||
- Minimize API calls in loops
|
||||
- Use appropriate timeout values
|
||||
- Consider parallel execution where possible
|
||||
- Keep script code efficient
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Data Collection → Processing → Approval
|
||||
1. Form node for data collection
|
||||
2. Script node for data validation/transformation
|
||||
3. Gateway for routing based on criteria
|
||||
4. Form node for approval tasks
|
||||
5. Notification for status updates
|
||||
|
||||
### External Integration
|
||||
1. Form node for user input
|
||||
2. Script node for data preparation
|
||||
3. API node for external system call
|
||||
4. Script node for response processing
|
||||
5. Gateway for success/failure routing
|
||||
|
||||
### Multi-level Approval
|
||||
1. Form node for request submission
|
||||
2. Gateway for routing based on amount/type
|
||||
3. Multiple form nodes for different approval levels
|
||||
4. Parallel gateways for concurrent approvals
|
||||
5. Join gateway to merge approval paths
|
@ -91,6 +91,117 @@ const showProcessSettings = ref(false);
|
||||
const showDropdown = ref(false);
|
||||
const showProcessHistoryModal = ref(false);
|
||||
|
||||
// Add mobile responsive state
|
||||
const showLeftPanel = ref(true);
|
||||
const showRightPanel = ref(true);
|
||||
const isMobile = ref(false);
|
||||
const isTablet = ref(false);
|
||||
|
||||
// Add node highlighting state
|
||||
const highlightedNodeId = ref(null);
|
||||
const highlightTimeout = ref(null);
|
||||
|
||||
// Mobile breakpoint detection
|
||||
const checkScreenSize = () => {
|
||||
const width = window.innerWidth;
|
||||
isMobile.value = width < 768; // Mobile: < 768px
|
||||
isTablet.value = width >= 768 && width < 1024; // Tablet: 768px - 1024px
|
||||
|
||||
// Auto-hide panels on mobile
|
||||
if (isMobile.value) {
|
||||
showLeftPanel.value = false;
|
||||
showRightPanel.value = false;
|
||||
} else if (isTablet.value) {
|
||||
// On tablet, show only one panel at a time if both were closed
|
||||
if (!showLeftPanel.value && !showRightPanel.value) {
|
||||
showRightPanel.value = true;
|
||||
}
|
||||
} else {
|
||||
// Desktop: show both panels by default
|
||||
if (!showLeftPanel.value && !showRightPanel.value) {
|
||||
showLeftPanel.value = true;
|
||||
showRightPanel.value = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle panel visibility
|
||||
const toggleLeftPanel = () => {
|
||||
showLeftPanel.value = !showLeftPanel.value;
|
||||
|
||||
// On mobile/tablet, close the other panel when opening one
|
||||
if ((isMobile.value || isTablet.value) && showLeftPanel.value) {
|
||||
showRightPanel.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRightPanel = () => {
|
||||
showRightPanel.value = !showRightPanel.value;
|
||||
|
||||
// On mobile/tablet, close the other panel when opening one
|
||||
if ((isMobile.value || isTablet.value) && showRightPanel.value) {
|
||||
showLeftPanel.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close panels when clicking on canvas (mobile only)
|
||||
const onPaneClickMobile = () => {
|
||||
selectedNode.value = null;
|
||||
selectedNodeData.value = null;
|
||||
selectedEdgeData.value = null;
|
||||
processStore.clearSelection();
|
||||
|
||||
// Close panels on mobile when clicking canvas
|
||||
if (isMobile.value) {
|
||||
showLeftPanel.value = false;
|
||||
showRightPanel.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle node highlighting from variable usage
|
||||
const handleNodeHighlight = (event) => {
|
||||
console.log('Received highlight event:', event.detail);
|
||||
const { nodeId, node } = event.detail;
|
||||
|
||||
// Clear any existing highlight timeout
|
||||
if (highlightTimeout.value) {
|
||||
clearTimeout(highlightTimeout.value);
|
||||
}
|
||||
|
||||
// Set the highlighted node
|
||||
highlightedNodeId.value = nodeId;
|
||||
console.log('Set highlighted node ID to:', nodeId);
|
||||
|
||||
// Center the view on the highlighted node
|
||||
if (processFlowCanvas.value && node) {
|
||||
nextTick(() => {
|
||||
try {
|
||||
console.log('Attempting to center view on node:', node);
|
||||
|
||||
// Call the exposed methods from ProcessFlowCanvas
|
||||
if (processFlowCanvas.value.centerOnNode) {
|
||||
processFlowCanvas.value.centerOnNode(nodeId, node.position);
|
||||
} else {
|
||||
console.warn('centerOnNode method not available on ProcessFlowCanvas');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error centering view on node:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
if (toast) {
|
||||
toast.success(`Navigated to: ${node.data?.label || node.type} node`);
|
||||
}
|
||||
|
||||
// Auto-remove highlight after 5 seconds
|
||||
highlightTimeout.value = setTimeout(() => {
|
||||
highlightedNodeId.value = null;
|
||||
highlightTimeout.value = null;
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Component definitions
|
||||
const components = [
|
||||
{
|
||||
@ -315,10 +426,7 @@ const updateEdgeLabel = (value) => {
|
||||
|
||||
// Update the pane click handler to clear edge selection too
|
||||
const onPaneClick = () => {
|
||||
selectedNode.value = null;
|
||||
selectedNodeData.value = null;
|
||||
selectedEdgeData.value = null;
|
||||
processStore.clearSelection();
|
||||
onPaneClickMobile();
|
||||
};
|
||||
|
||||
// Update node in store
|
||||
@ -392,27 +500,32 @@ const handleConditionUpdate = (conditions) => {
|
||||
const onNodesChange = (changes, currentNodes) => {
|
||||
if (!changes || !currentNodes) return;
|
||||
|
||||
// Handle node additions (from drag & drop)
|
||||
const addedNodes = changes.filter(change => change.type === 'add' && change.item);
|
||||
if (addedNodes.length > 0) {
|
||||
addedNodes.forEach(change => {
|
||||
// Check if node already exists in store to prevent duplicates
|
||||
const existingNode = processStore.currentProcess?.nodes?.find(n => n.id === change.item.id);
|
||||
if (!existingNode) {
|
||||
console.log('🔥 Adding node to store from canvas:', change.item);
|
||||
processStore.addNode(change.item);
|
||||
} else {
|
||||
console.log('⚠️ Node already exists in store, skipping:', change.item.id);
|
||||
}
|
||||
});
|
||||
// Skip processing during component addition to avoid conflicts
|
||||
if (isAddingComponent.value) {
|
||||
console.log('🔄 Skipping node changes during component addition');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle node removals
|
||||
// Handle position changes (only when dragging is complete)
|
||||
const positionChanges = {};
|
||||
const hasPositionChanges = changes
|
||||
.filter(change => change.type === 'position' && change.position && !change.dragging)
|
||||
.forEach(change => {
|
||||
positionChanges[change.id] = change.position;
|
||||
});
|
||||
|
||||
if (Object.keys(positionChanges).length > 0) {
|
||||
console.log('📍 Updating node positions:', Object.keys(positionChanges));
|
||||
processStore.updateNodePositions(positionChanges);
|
||||
}
|
||||
|
||||
// Handle node removals (from delete key or other removal actions)
|
||||
const removedNodes = changes
|
||||
.filter(change => change.type === 'remove')
|
||||
.map(change => change.id);
|
||||
|
||||
if (removedNodes.length > 0) {
|
||||
console.log('🗑️ Removing nodes:', removedNodes);
|
||||
removedNodes.forEach(nodeId => {
|
||||
processStore.deleteNode(nodeId);
|
||||
});
|
||||
@ -423,18 +536,6 @@ const onNodesChange = (changes, currentNodes) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle position changes (only when dragging is complete)
|
||||
const positionChanges = {};
|
||||
changes
|
||||
.filter(change => change.type === 'position' && change.position && !change.dragging)
|
||||
.forEach(change => {
|
||||
positionChanges[change.id] = change.position;
|
||||
});
|
||||
|
||||
if (Object.keys(positionChanges).length > 0) {
|
||||
processStore.updateNodePositions(positionChanges);
|
||||
}
|
||||
|
||||
// Update node selection if needed
|
||||
if (selectedNodeData.value) {
|
||||
const updatedNode = currentNodes.find(node => node.id === selectedNodeData.value.id);
|
||||
@ -622,6 +723,15 @@ onMounted(() => {
|
||||
|
||||
// Add click outside listener for dropdown
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
// Add resize listener for responsive behavior
|
||||
window.addEventListener('resize', checkScreenSize);
|
||||
|
||||
// Add node highlight listener for variable navigation
|
||||
window.addEventListener('highlightNode', handleNodeHighlight);
|
||||
|
||||
// Initial screen size check
|
||||
checkScreenSize();
|
||||
});
|
||||
|
||||
// Handle click outside dropdown
|
||||
@ -635,6 +745,13 @@ onUnmounted(() => {
|
||||
// Remove event listeners
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
window.removeEventListener('resize', checkScreenSize);
|
||||
window.removeEventListener('highlightNode', handleNodeHighlight);
|
||||
|
||||
// Clear highlight timeout if it exists
|
||||
if (highlightTimeout.value) {
|
||||
clearTimeout(highlightTimeout.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Show warning if there are unsaved changes
|
||||
@ -756,6 +873,29 @@ const saveProcess = async () => {
|
||||
|
||||
try {
|
||||
isSaving.value = true;
|
||||
|
||||
// Capture complete Vue Flow state before saving
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.toObject) {
|
||||
console.log('💾 Capturing Vue Flow state before save...');
|
||||
|
||||
try {
|
||||
const flowState = processFlowCanvas.value.toObject();
|
||||
|
||||
if (flowState && flowState.nodes) {
|
||||
// Set the captured flow state in the store for saving
|
||||
processStore.setFlowStateForSave(flowState);
|
||||
console.log('✅ Vue Flow state captured successfully');
|
||||
} else {
|
||||
console.warn('⚠️ Vue Flow state capture returned invalid data, proceeding with store data');
|
||||
}
|
||||
} catch (captureError) {
|
||||
console.error('❌ Error capturing Vue Flow state:', captureError);
|
||||
console.warn('⚠️ Proceeding with store data as fallback');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Vue Flow canvas not available for state capture');
|
||||
}
|
||||
|
||||
const success = await processStore.saveProcess();
|
||||
|
||||
if (success) {
|
||||
@ -1100,30 +1240,117 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
navigationConfirmed.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for process changes to restore Vue Flow state
|
||||
watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
||||
if (!newProcess) return;
|
||||
|
||||
// Only restore when a different process is loaded (not on updates)
|
||||
if (oldProcess && newProcess.id === oldProcess.id) return;
|
||||
|
||||
console.log('🔄 Process changed, preparing Vue Flow restoration for:', newProcess.name);
|
||||
|
||||
// Wait for the canvas to be ready
|
||||
await nextTick();
|
||||
|
||||
// Give the canvas component a moment to initialize
|
||||
setTimeout(async () => {
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.fromObject) {
|
||||
try {
|
||||
// Prepare the flow object for restoration
|
||||
const rawFlowObject = {
|
||||
nodes: newProcess.nodes || [],
|
||||
edges: newProcess.edges || [],
|
||||
viewport: newProcess.viewport || { x: 0, y: 0, zoom: 1 }
|
||||
};
|
||||
|
||||
// Clean the data to remove any Vue Flow internal properties
|
||||
const flowObject = processStore.cleanFlowData(rawFlowObject);
|
||||
|
||||
console.log('🔄 Restoring Vue Flow state:', {
|
||||
nodes: flowObject.nodes.length,
|
||||
edges: flowObject.edges.length,
|
||||
viewport: flowObject.viewport,
|
||||
nodeTypes: flowObject.nodes.map(n => ({ id: n.id, type: n.type, position: n.position })),
|
||||
edgeConnections: flowObject.edges.map(e => `${e.source} -> ${e.target}`)
|
||||
});
|
||||
|
||||
// Use Vue Flow's proper restoration method (now returns a Promise)
|
||||
await processFlowCanvas.value.fromObject(flowObject);
|
||||
|
||||
console.log('✅ Vue Flow state restoration completed');
|
||||
|
||||
// Fit view after restoration with a small delay to ensure everything is rendered
|
||||
setTimeout(() => {
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
||||
processFlowCanvas.value.fitView();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error restoring Vue Flow state:', error);
|
||||
|
||||
// Fallback to manual sync if Vue Flow restoration fails
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||
console.log('🔄 Falling back to manual canvas sync...');
|
||||
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Vue Flow canvas not available for restoration, using sync fallback');
|
||||
|
||||
// Fallback sync method
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
||||
}
|
||||
}
|
||||
}, 200); // Allow time for canvas to initialize
|
||||
}, { immediate: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="process-builder flex flex-col h-screen bg-white">
|
||||
<!-- Header Bar -->
|
||||
<header
|
||||
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
|
||||
class="bg-gray-800 px-2 md:px-4 py-2 md:py-4 flex items-center justify-between text-white shadow-md"
|
||||
>
|
||||
<!-- Left section - Logo and navigation -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2 md:gap-3">
|
||||
<Icon
|
||||
@click="confirmNavigation('/process-builder/manage')"
|
||||
name="ph:arrow-circle-left-duotone"
|
||||
class="cursor-pointer w-6 h-6"
|
||||
class="cursor-pointer w-5 h-5 md:w-6 md:h-6"
|
||||
/>
|
||||
<img
|
||||
src="@/assets/img/logo/logo-word-white.svg"
|
||||
alt="Corrad Logo"
|
||||
class="h-7"
|
||||
class="h-5 md:h-7 hidden sm:block"
|
||||
/>
|
||||
<!-- Mobile panel toggles -->
|
||||
<div v-if="hasCurrentProcess && (isMobile || isTablet)" class="flex items-center gap-1 ml-2">
|
||||
<RsButton
|
||||
@click="toggleLeftPanel"
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
:class="{ 'bg-gray-700': showLeftPanel }"
|
||||
class="p-1"
|
||||
>
|
||||
<Icon name="material-symbols:widgets" class="w-4 h-4" />
|
||||
</RsButton>
|
||||
<RsButton
|
||||
@click="toggleRightPanel"
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
:class="{ 'bg-gray-700': showRightPanel }"
|
||||
class="p-1"
|
||||
>
|
||||
<Icon name="material-symbols:tune" class="w-4 h-4" />
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle section - Process name -->
|
||||
<div class="flex-1 flex justify-center items-center mx-4">
|
||||
<div class="flex-1 flex justify-center items-center mx-2 md:mx-4">
|
||||
<FormKit
|
||||
v-if="hasCurrentProcess"
|
||||
v-model="processStore.currentProcess.name"
|
||||
@ -1132,38 +1359,38 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
validation="required"
|
||||
validation-visibility="live"
|
||||
:validation-messages="{ required: 'Please enter a process name' }"
|
||||
class="process-name-input max-w-md"
|
||||
class="process-name-input max-w-xs md:max-w-md"
|
||||
:classes="{
|
||||
outer: 'mb-0 w-full',
|
||||
}"
|
||||
/>
|
||||
<div v-else class="text-lg font-medium text-gray-400">No Process Selected</div>
|
||||
<div v-else class="text-sm md:text-lg font-medium text-gray-400 hidden sm:block">No Process Selected</div>
|
||||
</div>
|
||||
|
||||
<!-- Right section - Actions -->
|
||||
<div class="flex items-center">
|
||||
<!-- Primary actions -->
|
||||
<div class="flex items-center mr-2 border-r border-gray-600 pr-2">
|
||||
<RsButton @click="saveProcess" variant="primary" size="sm" class="mr-2" :disabled="!hasCurrentProcess || isSaving">
|
||||
<Icon v-if="isSaving" name="material-symbols:progress-activity" class="mr-1 animate-spin" />
|
||||
<Icon v-else name="material-symbols:save" class="mr-1" />
|
||||
{{ isSaving ? 'Saving...' : 'Save' }}
|
||||
<div class="flex items-center mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2">
|
||||
<RsButton @click="saveProcess" variant="primary" size="sm" class="mr-1 md:mr-2" :disabled="!hasCurrentProcess || isSaving">
|
||||
<Icon v-if="isSaving" name="material-symbols:progress-activity" class="mr-0 md:mr-1 animate-spin" />
|
||||
<Icon v-else name="material-symbols:save" class="mr-0 md:mr-1" />
|
||||
<span class="hidden md:inline">{{ isSaving ? 'Saving...' : 'Save' }}</span>
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Templates button -->
|
||||
<div class="mr-2 border-r border-gray-600 pr-2">
|
||||
<!-- Templates button - Hidden on mobile -->
|
||||
<div class="mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2 hidden md:block">
|
||||
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm" :disabled="!hasCurrentProcess">
|
||||
<Icon name="material-symbols:description-outline" class="mr-1" />
|
||||
Templates
|
||||
<Icon name="material-symbols:description-outline" class="mr-0 md:mr-1" />
|
||||
<span class="hidden lg:inline">Templates</span>
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Process History button - only show if process is saved -->
|
||||
<div v-if="processStore.currentProcess && processStore.currentProcess.id" class="mr-2 border-r border-gray-600 pr-2">
|
||||
<!-- Process History button - Hidden on mobile, only show if process is saved -->
|
||||
<div v-if="processStore.currentProcess && processStore.currentProcess.id" class="mr-1 md:mr-2 border-r border-gray-600 pr-1 md:pr-2 hidden md:block">
|
||||
<RsButton @click="showProcessHistoryModal = true" variant="secondary" size="sm">
|
||||
<Icon name="material-symbols:history" class="mr-1" />
|
||||
History
|
||||
<Icon name="material-symbols:history" class="mr-0 md:mr-1" />
|
||||
<span class="hidden lg:inline">History</span>
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
@ -1171,10 +1398,21 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
<div class="flex items-center">
|
||||
<div class="dropdown relative">
|
||||
<RsButton @click="showDropdown = !showDropdown" variant="tertiary" size="sm" class="flex items-center">
|
||||
<Icon name="material-symbols:more-vert" class="w-5 h-5" />
|
||||
<Icon name="material-symbols:more-vert" class="w-4 h-4 md:w-5 md:h-5" />
|
||||
</RsButton>
|
||||
|
||||
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded shadow-lg py-1 z-10 w-48 text-gray-800">
|
||||
<!-- Mobile-only actions -->
|
||||
<div v-if="isMobile" class="md:hidden border-b border-gray-200 pb-1 mb-1">
|
||||
<button @click="showTemplatesModal = true; showDropdown = false" :disabled="!hasCurrentProcess" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center disabled:opacity-50">
|
||||
<Icon name="material-symbols:description-outline" class="mr-2 w-4 h-4" />
|
||||
<span>Templates</span>
|
||||
</button>
|
||||
<button v-if="processStore.currentProcess && processStore.currentProcess.id" @click="showProcessHistoryModal = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:history" class="mr-2 w-4 h-4" />
|
||||
<span>History</span>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="showProcessSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
|
||||
<span>Process Settings</span>
|
||||
@ -1190,11 +1428,29 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 flex overflow-hidden" v-if="hasCurrentProcess">
|
||||
<div class="flex-1 flex overflow-hidden relative" v-if="hasCurrentProcess">
|
||||
<!-- Left Panel - Components -->
|
||||
<div class="w-64 border-r border-gray-200 flex flex-col overflow-hidden">
|
||||
<div
|
||||
v-show="showLeftPanel"
|
||||
:class="{
|
||||
'absolute inset-y-0 left-0 z-20 bg-white shadow-lg': isMobile,
|
||||
'absolute inset-y-0 left-0 z-10 bg-white shadow-md': isTablet,
|
||||
'relative w-64': !isMobile && !isTablet,
|
||||
'w-72': isMobile,
|
||||
'w-80': isTablet
|
||||
}"
|
||||
class="border-r border-gray-200 flex flex-col overflow-hidden transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<div class="bg-gray-100 p-3 flex items-center justify-between border-b border-gray-200">
|
||||
<h2 class="text-sm font-medium text-gray-700">Process Components</h2>
|
||||
<!-- Close button for mobile/tablet -->
|
||||
<button
|
||||
v-if="isMobile || isTablet"
|
||||
@click="showLeftPanel = false"
|
||||
class="p-1 hover:bg-gray-200 rounded"
|
||||
>
|
||||
<Icon name="material-symbols:close" class="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<ProcessBuilderComponents @add-component="onAddComponent" />
|
||||
@ -1207,18 +1463,57 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
ref="processFlowCanvas"
|
||||
:initial-nodes="canvasNodes"
|
||||
:initial-edges="canvasEdges"
|
||||
:highlighted-node-id="highlightedNodeId"
|
||||
@node-selected="onNodeSelected"
|
||||
@edge-selected="onEdgeSelected"
|
||||
@pane-click="onPaneClick"
|
||||
@nodes-change="onNodesChange"
|
||||
@edges-change="onEdgesChange"
|
||||
/>
|
||||
|
||||
<!-- Mobile floating action buttons -->
|
||||
<div v-if="isMobile" class="absolute bottom-4 right-4 flex flex-col gap-2">
|
||||
<RsButton
|
||||
@click="toggleLeftPanel"
|
||||
variant="primary"
|
||||
class="rounded-full w-12 h-12 p-0 shadow-lg"
|
||||
:class="{ 'bg-blue-600': showLeftPanel }"
|
||||
>
|
||||
<Icon name="material-symbols:widgets" class="w-6 h-6" />
|
||||
</RsButton>
|
||||
<RsButton
|
||||
@click="toggleRightPanel"
|
||||
variant="primary"
|
||||
class="rounded-full w-12 h-12 p-0 shadow-lg"
|
||||
:class="{ 'bg-blue-600': showRightPanel }"
|
||||
>
|
||||
<Icon name="material-symbols:tune" class="w-6 h-6" />
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Properties -->
|
||||
<div class="w-80 border-l border-gray-200 flex flex-col overflow-hidden">
|
||||
<div
|
||||
v-show="showRightPanel"
|
||||
:class="{
|
||||
'absolute inset-y-0 right-0 z-20 bg-white shadow-lg': isMobile,
|
||||
'absolute inset-y-0 right-0 z-10 bg-white shadow-md': isTablet,
|
||||
'relative w-72': !isMobile && !isTablet,
|
||||
'w-72': isMobile,
|
||||
'w-80': isTablet
|
||||
}"
|
||||
class="border-l border-gray-200 flex flex-col overflow-hidden transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<div class="bg-gray-100 p-3 flex items-center justify-between border-b border-gray-200">
|
||||
<h2 class="text-sm font-medium text-gray-700">Properties</h2>
|
||||
<!-- Close button for mobile/tablet -->
|
||||
<button
|
||||
v-if="isMobile || isTablet"
|
||||
@click="showRightPanel = false"
|
||||
class="p-1 hover:bg-gray-200 rounded"
|
||||
>
|
||||
<Icon name="material-symbols:close" class="w-4 h-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<!-- Show variable manager when no node is selected -->
|
||||
@ -1292,6 +1587,13 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop for mobile panels -->
|
||||
<div
|
||||
v-if="(isMobile || isTablet) && (showLeftPanel || showRightPanel)"
|
||||
@click="showLeftPanel = false; showRightPanel = false"
|
||||
class="absolute inset-0 bg-black bg-opacity-50 z-5"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state - No process selected -->
|
||||
@ -1574,29 +1876,76 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.process-builder-palette {
|
||||
width: 200px;
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.process-name-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.process-builder-properties {
|
||||
width: 250px;
|
||||
.process-name-input :deep(.formkit-inner) {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly adjustments */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.component-item {
|
||||
min-height: 44px; /* iOS touch target minimum */
|
||||
}
|
||||
|
||||
.dropdown-menu button {
|
||||
min-height: 44px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
/* Larger tap targets for mobile */
|
||||
.process-builder :deep(.vue-flow__node) {
|
||||
min-width: 120px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.process-builder :deep(.vue-flow__handle) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Panel transitions and animations */
|
||||
.panel-enter-active,
|
||||
.panel-leave-active {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.panel-enter-from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.panel-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.panel-right-enter-from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.panel-right-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Improved mobile canvas interactions */
|
||||
@media (max-width: 768px) {
|
||||
.process-builder-actions {
|
||||
flex-wrap: wrap;
|
||||
.process-builder :deep(.vue-flow__pane) {
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.process-builder-palette,
|
||||
.process-builder-properties {
|
||||
display: none;
|
||||
.process-builder :deep(.vue-flow__node) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.process-builder-canvas {
|
||||
width: 100%;
|
||||
.process-builder :deep(.vue-flow__edge) {
|
||||
pointer-events: stroke;
|
||||
stroke-width: 3px; /* Thicker edges for easier mobile selection */
|
||||
}
|
||||
}
|
||||
</style>
|
@ -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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user