Enhance Process Templates Modal with Preview Functionality and Styling Updates
- Introduced a preview modal for process templates, allowing users to view detailed information and flow before selection. - Updated the template card styles for improved visual appeal and user interaction, including hover effects and background gradients. - Enhanced the layout and structure of the template display, ensuring better organization of template details and action buttons. - Added computed properties for dynamic node styling based on template definitions, improving the visual representation of nodes in the preview. - Implemented responsive design adjustments to ensure usability across different screen sizes.
This commit is contained in:
parent
9f53c75716
commit
c5edf2abfe
@ -8,8 +8,8 @@
|
||||
v-for="cat in categories"
|
||||
:key="cat.id"
|
||||
@click="activeCategory = cat.id"
|
||||
class="px-4 py-2 text-sm font-medium"
|
||||
:class="activeCategory === cat.id ? 'border-b-2 border-blue-500 text-blue-600' : 'text-gray-500 hover:text-gray-700'"
|
||||
class="px-4 py-2 text-sm font-medium transition-colors duration-200"
|
||||
:class="activeCategory === cat.id ? 'border-b-2 border-blue-500 text-blue-600 bg-blue-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'"
|
||||
>
|
||||
{{ cat.name }}
|
||||
</button>
|
||||
@ -18,38 +18,65 @@
|
||||
|
||||
<!-- Templates Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div v-for="template in filteredTemplates" :key="template.id" class="template-card border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="template-preview bg-gray-50 p-4 border-b relative">
|
||||
<div v-for="template in filteredTemplates" :key="template.id" class="template-card border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 bg-white">
|
||||
<div class="template-preview bg-gradient-to-br from-gray-50 to-gray-100 p-4 border-b border-gray-200 relative">
|
||||
<div class="w-full h-full flex flex-col items-center justify-center">
|
||||
<div class="template-icon-wrapper">
|
||||
<Icon :name="getTemplateIcon(template)" class="w-12 h-12 text-gray-400" />
|
||||
<Icon :name="getTemplateIcon(template)" class="w-12 h-12 text-gray-500" />
|
||||
</div>
|
||||
<span class="text-sm text-gray-500 mt-2">{{ template.name }}</span>
|
||||
<span class="text-sm text-gray-600 mt-2 font-medium">{{ template.name }}</span>
|
||||
</div>
|
||||
<!-- Preview overlay -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-10 transition-all duration-200 flex items-center justify-center">
|
||||
<button
|
||||
@click="previewTemplate(template)"
|
||||
class="preview-btn opacity-0 hover:opacity-100 transition-opacity duration-200 bg-white bg-opacity-90 hover:bg-opacity-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium shadow-md border border-gray-200"
|
||||
>
|
||||
<Icon name="material-symbols:visibility" class="w-4 h-4 mr-1" />
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<h3 class="font-medium text-lg mb-1">{{ template.name }}</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">{{ template.description }}</p>
|
||||
<h3 class="font-semibold text-lg mb-2 text-gray-800">{{ template.name }}</h3>
|
||||
<p class="text-gray-600 text-sm mb-4 leading-relaxed">{{ template.description }}</p>
|
||||
|
||||
<div class="flex items-center text-xs text-gray-500 mb-4">
|
||||
<div class="flex items-center mr-4">
|
||||
<div class="flex items-center text-xs text-gray-500 mb-4 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:account-tree" class="mr-1 w-3 h-3" />
|
||||
<span>{{ template.nodeCount }} nodes</span>
|
||||
</div>
|
||||
<div class="flex items-center mr-4">
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:route" class="mr-1 w-3 h-3" />
|
||||
<span>{{ template.edgeCount }} connections</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:timer" class="mr-1 w-3 h-3" />
|
||||
<span>{{ template.complexity }}</span>
|
||||
<span class="capitalize">{{ template.complexity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RsButton @click="useTemplate(template)" variant="primary" size="sm" class="w-full">
|
||||
Use Template
|
||||
</RsButton>
|
||||
<div class="flex space-x-2">
|
||||
<RsButton
|
||||
@click="previewTemplate(template)"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
>
|
||||
<Icon name="material-symbols:visibility" class="w-4 h-4 mr-1" />
|
||||
Preview
|
||||
</RsButton>
|
||||
<RsButton
|
||||
@click="useTemplate(template)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
||||
Use Template
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -62,9 +89,148 @@
|
||||
</div>
|
||||
</div>
|
||||
</RsModal>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<RsModal v-model="showPreviewModal" title="Template Preview" size="2xl" position="center">
|
||||
<div v-if="selectedTemplate" class="template-preview-container">
|
||||
<!-- Template Info -->
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800 mb-1">{{ selectedTemplate.name }}</h2>
|
||||
<p class="text-gray-600">{{ selectedTemplate.description }}</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full capitalize">
|
||||
{{ selectedTemplate.complexity }}
|
||||
</span>
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
|
||||
{{ selectedTemplate.category }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-6 text-sm text-gray-600">
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:account-tree" class="mr-1 w-4 h-4" />
|
||||
<span>{{ selectedTemplate.nodeCount }} nodes</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:route" class="mr-1 w-4 h-4" />
|
||||
<span>{{ selectedTemplate.edgeCount }} connections</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Icon name="material-symbols:data-object" class="mr-1 w-4 h-4" />
|
||||
<span>{{ selectedTemplate.variables?.length || 0 }} variables</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Flow Preview -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-3">Process Flow</h3>
|
||||
<div class="process-flow-preview bg-white border border-gray-200 rounded-lg h-96 overflow-hidden">
|
||||
<VueFlow
|
||||
v-model="previewFlow"
|
||||
:nodes="previewNodes"
|
||||
:edges="previewEdges"
|
||||
:node-types="customNodeTypes"
|
||||
:default-viewport="{ x: 0, y: 0, zoom: 0.8 }"
|
||||
:min-zoom="0.5"
|
||||
:max-zoom="1.5"
|
||||
:pan-on-drag="true"
|
||||
:zoom-on-scroll="true"
|
||||
:pan-on-scroll="false"
|
||||
:zoom-on-double-click="false"
|
||||
:select-nodes-on-drag="false"
|
||||
:nodes-draggable="false"
|
||||
:nodes-connectable="false"
|
||||
:elements-selectable="false"
|
||||
:delete-key-code="null"
|
||||
class="preview-vue-flow"
|
||||
>
|
||||
<Background pattern-color="#e2e8f0" gap="20" />
|
||||
<Controls position="bottom-right" />
|
||||
<MiniMap position="top-right" />
|
||||
</VueFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variables -->
|
||||
<div class="mb-6" v-if="selectedTemplate.variables && selectedTemplate.variables.length > 0">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-3">Process Variables</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="variable in selectedTemplate.variables"
|
||||
:key="variable.name"
|
||||
class="variable-item p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-medium text-sm text-gray-800">{{ variable.name }}</span>
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||||
{{ variable.type }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600">{{ variable.description }}</p>
|
||||
<div v-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="mt-1">
|
||||
<span class="text-xs text-gray-500">Default: </span>
|
||||
<span class="text-xs font-mono bg-gray-200 px-1 rounded">{{ variable.defaultValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<RsButton @click="showPreviewModal = false" variant="secondary" size="sm">
|
||||
Close
|
||||
</RsButton>
|
||||
<RsButton @click="useTemplateFromPreview" variant="primary" size="sm">
|
||||
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
|
||||
Use This Template
|
||||
</RsButton>
|
||||
</div>
|
||||
</div>
|
||||
</RsModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { VueFlow } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
import { Controls } from '@vue-flow/controls';
|
||||
import { MiniMap } from '@vue-flow/minimap';
|
||||
import { markRaw } from 'vue';
|
||||
import "@vue-flow/core/dist/style.css";
|
||||
import "@vue-flow/core/dist/theme-default.css";
|
||||
import "@vue-flow/controls/dist/style.css";
|
||||
import "@vue-flow/minimap/dist/style.css";
|
||||
|
||||
// Import custom node components
|
||||
import StartNode from "~/components/process-flow/custom/StartNode.vue";
|
||||
import EndNode from "~/components/process-flow/custom/EndNode.vue";
|
||||
import FormNode from "~/components/process-flow/custom/FormNode.vue";
|
||||
import ApiNode from "~/components/process-flow/custom/ApiNode.vue";
|
||||
import GatewayNode from "~/components/process-flow/custom/GatewayNode.vue";
|
||||
import ScriptNode from "~/components/process-flow/custom/ScriptNode.vue";
|
||||
import BusinessRuleNode from "~/components/process-flow/custom/BusinessRuleNode.vue";
|
||||
import NotificationNode from "~/components/process-flow/custom/NotificationNode.vue";
|
||||
import HtmlNode from "~/components/process-flow/custom/HtmlNode.vue";
|
||||
import SubprocessNode from "~/components/process-flow/custom/SubprocessNode.vue";
|
||||
|
||||
// Create nodeTypes object with markRaw to prevent reactivity issues
|
||||
const customNodeTypes = {
|
||||
'start': markRaw(StartNode),
|
||||
'end': markRaw(EndNode),
|
||||
'form': markRaw(FormNode),
|
||||
'api': markRaw(ApiNode),
|
||||
'gateway': markRaw(GatewayNode),
|
||||
'script': markRaw(ScriptNode),
|
||||
'business-rule': markRaw(BusinessRuleNode),
|
||||
'notification': markRaw(NotificationNode),
|
||||
'html': markRaw(HtmlNode),
|
||||
'subprocess': markRaw(SubprocessNode)
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
@ -79,6 +245,10 @@ const showModal = computed({
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
// Preview modal state
|
||||
const showPreviewModal = ref(false);
|
||||
const selectedTemplate = ref(null);
|
||||
|
||||
// Categories
|
||||
const categories = [
|
||||
{ id: 'all', name: 'All Templates' },
|
||||
@ -132,7 +302,11 @@ const templates = [
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: 'Start',
|
||||
description: 'Begin approval process'
|
||||
description: 'Begin approval process',
|
||||
backgroundColor: '#dcfce7',
|
||||
borderColor: '#10b981',
|
||||
textColor: '#065f46',
|
||||
shape: 'circle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -143,7 +317,11 @@ const templates = [
|
||||
label: 'Submit Request',
|
||||
description: 'Employee submits approval request',
|
||||
formId: null,
|
||||
formName: 'Approval Request Form'
|
||||
formName: 'Approval Request Form',
|
||||
backgroundColor: '#faf5ff',
|
||||
borderColor: '#9333ea',
|
||||
textColor: '#6b21a8',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -158,7 +336,11 @@ const templates = [
|
||||
recipientRole: 'manager',
|
||||
subject: 'New approval request',
|
||||
message: 'A new approval request requires your review.',
|
||||
priority: 'medium'
|
||||
priority: 'medium',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -169,7 +351,11 @@ const templates = [
|
||||
label: 'Manager Review',
|
||||
description: 'Manager reviews and approves/rejects',
|
||||
formId: null,
|
||||
formName: 'Manager Review Form'
|
||||
formName: 'Manager Review Form',
|
||||
backgroundColor: '#faf5ff',
|
||||
borderColor: '#9333ea',
|
||||
textColor: '#6b21a8',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -187,7 +373,11 @@ const templates = [
|
||||
output: 'Approved'
|
||||
}
|
||||
],
|
||||
defaultPath: 'Rejected'
|
||||
defaultPath: 'Rejected',
|
||||
backgroundColor: '#fff7ed',
|
||||
borderColor: '#f97316',
|
||||
textColor: '#c2410c',
|
||||
shape: 'diamond'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -202,7 +392,11 @@ const templates = [
|
||||
recipientVariable: 'requester_email',
|
||||
subject: 'Request Approved',
|
||||
message: 'Your request has been approved.',
|
||||
priority: 'high'
|
||||
priority: 'high',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -217,7 +411,11 @@ const templates = [
|
||||
recipientVariable: 'requester_email',
|
||||
subject: 'Request Rejected',
|
||||
message: 'Your request has been rejected.',
|
||||
priority: 'medium'
|
||||
priority: 'medium',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -226,7 +424,11 @@ const templates = [
|
||||
position: { x: 1300, y: 100 },
|
||||
data: {
|
||||
label: 'End',
|
||||
description: 'Process completed'
|
||||
description: 'Process completed',
|
||||
backgroundColor: '#fee2e2',
|
||||
borderColor: '#dc2626',
|
||||
textColor: '#991b1b',
|
||||
shape: 'circle'
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -1022,6 +1224,21 @@ function useTemplate(template) {
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
// Preview template
|
||||
function previewTemplate(template) {
|
||||
selectedTemplate.value = template;
|
||||
showPreviewModal.value = true;
|
||||
}
|
||||
|
||||
// Use template from preview
|
||||
function useTemplateFromPreview() {
|
||||
if (selectedTemplate.value) {
|
||||
emit('select-template', selectedTemplate.value);
|
||||
showPreviewModal.value = false;
|
||||
showModal.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get icon based on template category/type
|
||||
const getTemplateIcon = (template) => {
|
||||
const icons = {
|
||||
@ -1034,6 +1251,229 @@ const getTemplateIcon = (template) => {
|
||||
|
||||
return icons[template.category] || 'material-symbols:account-tree';
|
||||
};
|
||||
|
||||
// Get icon for node types
|
||||
const getNodeIcon = (nodeType) => {
|
||||
const icons = {
|
||||
'start': 'material-symbols:play-circle-outline',
|
||||
'end': 'material-symbols:stop-circle-outline',
|
||||
'form': 'material-symbols:description-outline',
|
||||
'api': 'material-symbols:api',
|
||||
'gateway': 'material-symbols:call-split',
|
||||
'notification': 'material-symbols:notifications-outline',
|
||||
'business-rule': 'material-symbols:rule',
|
||||
'script': 'material-symbols:code',
|
||||
'html': 'material-symbols:code',
|
||||
'subprocess': 'material-symbols:hub-outline'
|
||||
};
|
||||
|
||||
return icons[nodeType] || 'material-symbols:account-tree';
|
||||
};
|
||||
|
||||
// Calculate edge path for preview
|
||||
const getEdgePath = (edge) => {
|
||||
const sourceNode = selectedTemplate.value?.nodes.find(n => n.id === edge.source);
|
||||
const targetNode = selectedTemplate.value?.nodes.find(n => n.id === edge.target);
|
||||
|
||||
if (!sourceNode || !targetNode) return '';
|
||||
|
||||
// Normalize positions to fit within the preview container
|
||||
const sourceX = Math.max(5, Math.min(95, (sourceNode.position.x / 2000) * 100));
|
||||
const sourceY = Math.max(5, Math.min(95, (sourceNode.position.y / 800) * 100));
|
||||
const targetX = Math.max(5, Math.min(95, (targetNode.position.x / 2000) * 100));
|
||||
const targetY = Math.max(5, Math.min(95, (targetNode.position.y / 800) * 100));
|
||||
|
||||
// Calculate the distance and direction
|
||||
const dx = targetX - sourceX;
|
||||
const dy = targetY - sourceY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Adjust target position to account for arrow marker
|
||||
const arrowOffset = 3; // Adjust based on marker size
|
||||
const targetAdjustedX = targetX - (dx / distance) * arrowOffset;
|
||||
const targetAdjustedY = targetY - (dy / distance) * arrowOffset;
|
||||
|
||||
// Create a smooth curved path with proper control points
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
// Horizontal flow - use gentle curve
|
||||
const controlX1 = sourceX + dx * 0.25;
|
||||
const controlY1 = sourceY;
|
||||
const controlX2 = sourceX + dx * 0.75;
|
||||
const controlY2 = targetAdjustedY;
|
||||
|
||||
return `M ${sourceX} ${sourceY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${targetAdjustedX} ${targetAdjustedY}`;
|
||||
} else {
|
||||
// Vertical flow - use straight line with slight curve
|
||||
const controlX1 = sourceX;
|
||||
const controlY1 = sourceY + dy * 0.25;
|
||||
const controlX2 = targetAdjustedX;
|
||||
const controlY2 = sourceY + dy * 0.75;
|
||||
|
||||
return `M ${sourceX} ${sourceY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${targetAdjustedX} ${targetAdjustedY}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate arrow points for fallback arrows
|
||||
const getArrowPoints = (edge) => {
|
||||
const sourceNode = selectedTemplate.value?.nodes.find(n => n.id === edge.source);
|
||||
const targetNode = selectedTemplate.value?.nodes.find(n => n.id === edge.target);
|
||||
|
||||
if (!sourceNode || !targetNode) return '';
|
||||
|
||||
// Normalize positions
|
||||
const sourceX = Math.max(5, Math.min(95, (sourceNode.position.x / 2000) * 100));
|
||||
const sourceY = Math.max(5, Math.min(95, (sourceNode.position.y / 800) * 100));
|
||||
const targetX = Math.max(5, Math.min(95, (targetNode.position.x / 2000) * 100));
|
||||
const targetY = Math.max(5, Math.min(95, (targetNode.position.y / 800) * 100));
|
||||
|
||||
// Calculate direction
|
||||
const dx = targetX - sourceX;
|
||||
const dy = targetY - sourceY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance === 0) return '';
|
||||
|
||||
// Normalize direction vector
|
||||
const dirX = dx / distance;
|
||||
const dirY = dy / distance;
|
||||
|
||||
// Arrow size
|
||||
const arrowSize = 4;
|
||||
|
||||
// Arrow tip position (slightly before the target node)
|
||||
const tipX = targetX - dirX * 6;
|
||||
const tipY = targetY - dirY * 6;
|
||||
|
||||
// Arrow base points (perpendicular to direction)
|
||||
const perpX = -dirY;
|
||||
const perpY = dirX;
|
||||
|
||||
const base1X = tipX - dirX * arrowSize + perpX * arrowSize;
|
||||
const base1Y = tipY - dirY * arrowSize + perpY * arrowSize;
|
||||
const base2X = tipX - dirX * arrowSize - perpX * arrowSize;
|
||||
const base2Y = tipY - dirY * arrowSize - perpY * arrowSize;
|
||||
|
||||
return `${tipX},${tipY} ${base1X},${base1Y} ${base2X},${base2Y}`;
|
||||
};
|
||||
|
||||
// Vue Flow preview data
|
||||
const previewFlow = ref(null);
|
||||
|
||||
// Computed properties for Vue Flow preview
|
||||
const previewNodes = computed(() => {
|
||||
if (!selectedTemplate.value?.nodes) return [];
|
||||
|
||||
return selectedTemplate.value.nodes.map(node => {
|
||||
// Get default styling from ProcessBuilderComponents
|
||||
const defaultStyling = getDefaultNodeStyling(node.type);
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
// Ensure styling properties are included
|
||||
backgroundColor: node.data.backgroundColor || defaultStyling.backgroundColor,
|
||||
borderColor: node.data.borderColor || defaultStyling.borderColor,
|
||||
textColor: node.data.textColor || defaultStyling.textColor,
|
||||
shape: node.data.shape || defaultStyling.shape
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const previewEdges = computed(() => {
|
||||
if (!selectedTemplate.value?.edges) return [];
|
||||
|
||||
return selectedTemplate.value.edges.map(edge => ({
|
||||
...edge,
|
||||
type: 'smoothstep',
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: '#475569',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: 'none'
|
||||
},
|
||||
markerEnd: {
|
||||
type: 'arrowclosed',
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: '#475569'
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// Get default styling for node types
|
||||
const getDefaultNodeStyling = (nodeType) => {
|
||||
const stylingMap = {
|
||||
'start': {
|
||||
backgroundColor: '#dcfce7',
|
||||
borderColor: '#10b981',
|
||||
textColor: '#065f46',
|
||||
shape: 'circle'
|
||||
},
|
||||
'end': {
|
||||
backgroundColor: '#fee2e2',
|
||||
borderColor: '#dc2626',
|
||||
textColor: '#991b1b',
|
||||
shape: 'circle'
|
||||
},
|
||||
'form': {
|
||||
backgroundColor: '#faf5ff',
|
||||
borderColor: '#9333ea',
|
||||
textColor: '#6b21a8',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'api': {
|
||||
backgroundColor: '#eff6ff',
|
||||
borderColor: '#3b82f6',
|
||||
textColor: '#1e40af',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'gateway': {
|
||||
backgroundColor: '#fff7ed',
|
||||
borderColor: '#f97316',
|
||||
textColor: '#c2410c',
|
||||
shape: 'diamond'
|
||||
},
|
||||
'notification': {
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'business-rule': {
|
||||
backgroundColor: '#fdf4ff',
|
||||
borderColor: '#a855f7',
|
||||
textColor: '#7c3aed',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'script': {
|
||||
backgroundColor: '#f9fafb',
|
||||
borderColor: '#6b7280',
|
||||
textColor: '#374151',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'html': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0c4a6e',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'subprocess': {
|
||||
backgroundColor: '#f0fdfa',
|
||||
borderColor: '#14b8a6',
|
||||
textColor: '#134e4a',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
};
|
||||
|
||||
return stylingMap[nodeType] || {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
textColor: '#374151',
|
||||
shape: 'rectangle'
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -1044,11 +1484,13 @@ const getTemplateIcon = (template) => {
|
||||
}
|
||||
|
||||
.template-card {
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
@ -1057,6 +1499,7 @@ const getTemplateIcon = (template) => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-icon-wrapper {
|
||||
@ -1067,7 +1510,221 @@ const getTemplateIcon = (template) => {
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 3px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.template-card:hover .template-icon-wrapper {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Preview Modal Styles */
|
||||
.template-preview-container {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.process-flow-preview {
|
||||
position: relative;
|
||||
background: linear-gradient(45deg, #f8fafc 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #f8fafc 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #f8fafc 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #f8fafc 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.process-flow-diagram {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.preview-node {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.node-content:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||
border-color: #3b82f6;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* Node type specific styling */
|
||||
.preview-node .node-content {
|
||||
border-color: #e2e8f0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="start"] {
|
||||
border-color: #10b981;
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="end"] {
|
||||
border-color: #ef4444;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="gateway"] {
|
||||
border-color: #f97316;
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #fed7aa 100%);
|
||||
color: #c2410c;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
min-width: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="gateway"] > div {
|
||||
transform: rotate(-45deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="form"] {
|
||||
border-color: #9333ea;
|
||||
background: linear-gradient(135deg, #faf5ff 0%, #e9d5ff 100%);
|
||||
color: #6b21a8;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="api"] {
|
||||
border-color: #3b82f6;
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="notification"] {
|
||||
border-color: #0ea5e9;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #bae6fd 100%);
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="business-rule"] {
|
||||
border-color: #a855f7;
|
||||
background: linear-gradient(135deg, #fdf4ff 0%, #e9d5ff 100%);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.preview-node .node-content[data-type="script"] {
|
||||
border-color: #6b7280;
|
||||
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.variable-item:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Flow edge styling */
|
||||
.flow-edge {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.flow-edge:hover {
|
||||
stroke-width: 3.5;
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* Vue Flow Preview Styling */
|
||||
.preview-vue-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-vue-flow .vue-flow__node {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.preview-vue-flow .vue-flow__node:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.preview-vue-flow .vue-flow__controls {
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-vue-flow .vue-flow__minimap {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-vue-flow .vue-flow__background {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Enhanced Preview Node Styling */
|
||||
.preview-node .node-content {
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.preview-node .node-content:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.process-flow-preview {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -74,7 +74,11 @@ const availableComponents = [
|
||||
defaultProps: {
|
||||
label: 'Start',
|
||||
data: {
|
||||
description: 'Process start point'
|
||||
description: 'Process start point',
|
||||
shape: 'circle',
|
||||
backgroundColor: '#dcfce7',
|
||||
borderColor: '#10b981',
|
||||
textColor: '#065f46'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -87,7 +91,11 @@ const availableComponents = [
|
||||
defaultProps: {
|
||||
label: 'End',
|
||||
data: {
|
||||
description: 'Process end point'
|
||||
description: 'Process end point',
|
||||
shape: 'circle',
|
||||
backgroundColor: '#fee2e2',
|
||||
borderColor: '#dc2626',
|
||||
textColor: '#991b1b'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -103,7 +111,11 @@ const availableComponents = [
|
||||
data: {
|
||||
description: 'Form submission task',
|
||||
formId: null,
|
||||
formName: null
|
||||
formName: null,
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#faf5ff',
|
||||
borderColor: '#9333ea',
|
||||
textColor: '#6b21a8'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -123,7 +135,11 @@ const availableComponents = [
|
||||
headers: '{ "Content-Type": "application/json" }',
|
||||
outputVariable: 'apiResponse',
|
||||
continueOnError: false,
|
||||
errorVariable: 'apiError'
|
||||
errorVariable: 'apiError',
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#eff6ff',
|
||||
borderColor: '#3b82f6',
|
||||
textColor: '#1e40af'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -138,7 +154,11 @@ const availableComponents = [
|
||||
data: {
|
||||
description: 'Decision point for branching the workflow',
|
||||
conditions: [],
|
||||
defaultPath: 'Default'
|
||||
defaultPath: 'Default',
|
||||
shape: 'diamond',
|
||||
backgroundColor: '#fff7ed',
|
||||
borderColor: '#f97316',
|
||||
textColor: '#c2410c'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -170,7 +190,11 @@ const availableComponents = [
|
||||
enabled: false,
|
||||
value: 24,
|
||||
unit: 'hours'
|
||||
}
|
||||
},
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -185,7 +209,11 @@ const availableComponents = [
|
||||
data: {
|
||||
description: 'Apply business rules',
|
||||
ruleGroups: [],
|
||||
priority: 'medium'
|
||||
priority: 'medium',
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#fdf4ff',
|
||||
borderColor: '#a855f7',
|
||||
textColor: '#7c3aed'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -202,7 +230,11 @@ const availableComponents = [
|
||||
scriptCode: '',
|
||||
scriptLanguage: 'javascript',
|
||||
inputVariables: [],
|
||||
outputVariables: []
|
||||
outputVariables: [],
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#f9fafb',
|
||||
borderColor: '#6b7280',
|
||||
textColor: '#374151'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -222,7 +254,11 @@ const availableComponents = [
|
||||
inputVariables: [],
|
||||
outputVariables: [],
|
||||
allowVariableAccess: true,
|
||||
autoRefresh: false
|
||||
autoRefresh: false,
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#e0f2fe',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0c4a6e'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -237,7 +273,11 @@ const availableComponents = [
|
||||
data: {
|
||||
description: 'Executes another process',
|
||||
subprocessId: null,
|
||||
subprocessName: ''
|
||||
subprocessName: '',
|
||||
shape: 'rectangle',
|
||||
backgroundColor: '#f0fdfa',
|
||||
borderColor: '#14b8a6',
|
||||
textColor: '#134e4a'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -362,7 +402,6 @@ const onDragStart = (event, component) => {
|
||||
|
||||
// Add a component directly via click
|
||||
const addComponent = (component) => {
|
||||
return;
|
||||
// Use same format as drag operation for consistency
|
||||
const componentData = {
|
||||
type: component.type,
|
||||
|
@ -39,24 +39,24 @@ import "@vue-flow/minimap/dist/style.css";
|
||||
|
||||
// Create nodeTypes object with markRaw to prevent reactivity issues
|
||||
const customNodeTypes = {
|
||||
'start': markRaw(StartNode),
|
||||
'end': markRaw(EndNode),
|
||||
'form': markRaw(FormNode),
|
||||
'api': markRaw(ApiNode),
|
||||
'gateway': markRaw(GatewayNode),
|
||||
'script': markRaw(ScriptNode),
|
||||
'business-rule': markRaw(BusinessRuleNode),
|
||||
'notification': markRaw(NotificationNode),
|
||||
'html': markRaw(HtmlNode),
|
||||
'subprocess': markRaw(SubprocessNode),
|
||||
start: markRaw(StartNode),
|
||||
end: markRaw(EndNode),
|
||||
form: markRaw(FormNode),
|
||||
api: markRaw(ApiNode),
|
||||
gateway: markRaw(GatewayNode),
|
||||
script: markRaw(ScriptNode),
|
||||
"business-rule": markRaw(BusinessRuleNode),
|
||||
notification: markRaw(NotificationNode),
|
||||
html: markRaw(HtmlNode),
|
||||
subprocess: markRaw(SubprocessNode),
|
||||
// Shape nodes
|
||||
'hexagon-shape': markRaw(HexagonShape),
|
||||
'trapezoid-shape': markRaw(TrapezoidShape),
|
||||
'rectangle-shape': markRaw(RectangleShape),
|
||||
'swimlane-horizontal': markRaw(SwimlaneHorizontal),
|
||||
'swimlane-vertical': markRaw(SwimlaneVertical),
|
||||
'text-annotation': markRaw(TextAnnotation),
|
||||
'process-group': markRaw(ProcessGroup)
|
||||
"hexagon-shape": markRaw(HexagonShape),
|
||||
"trapezoid-shape": markRaw(TrapezoidShape),
|
||||
"rectangle-shape": markRaw(RectangleShape),
|
||||
"swimlane-horizontal": markRaw(SwimlaneHorizontal),
|
||||
"swimlane-vertical": markRaw(SwimlaneVertical),
|
||||
"text-annotation": markRaw(TextAnnotation),
|
||||
"process-group": markRaw(ProcessGroup),
|
||||
};
|
||||
|
||||
// Add Material Icons import
|
||||
@ -276,10 +276,18 @@ onMounted(() => {
|
||||
// Setup window resize handler
|
||||
window.addEventListener("resize", resizeFlow);
|
||||
|
||||
// Initial fit view
|
||||
setTimeout(() => {
|
||||
fitView();
|
||||
}, 100);
|
||||
// Only fit view initially if there are no existing nodes with positions
|
||||
// This prevents repositioning existing nodes when the canvas mounts
|
||||
const hasExistingNodes = props.initialNodes.length > 0;
|
||||
const hasPositions = props.initialNodes.some(
|
||||
(node) => node.position && (node.position.x !== 0 || node.position.y !== 0)
|
||||
);
|
||||
|
||||
if (!hasExistingNodes || !hasPositions) {
|
||||
setTimeout(() => {
|
||||
fitView();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Center on a specific node
|
||||
@ -869,7 +877,7 @@ defineExpose({
|
||||
nodes.value = newNodes;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in setNodes:', error);
|
||||
console.error("Error in setNodes:", error);
|
||||
}
|
||||
},
|
||||
setEdges: (newEdges) => {
|
||||
@ -878,13 +886,13 @@ defineExpose({
|
||||
edges.value = newEdges;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in setEdges:', error);
|
||||
console.error("Error in setEdges:", error);
|
||||
}
|
||||
},
|
||||
// Provide access to the flow instance
|
||||
get flowInstance() {
|
||||
return flowInstance;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Update an existing node
|
||||
@ -978,15 +986,22 @@ function syncCanvas(newNodes, newEdges) {
|
||||
);
|
||||
|
||||
// Remove nodes that are no longer in the new list
|
||||
// Be more conservative - only remove if we're sure it's not a temporary state
|
||||
const nodesToRemove = nodes.value.filter(
|
||||
(node) => !newNodeIds.has(node.id)
|
||||
);
|
||||
if (nodesToRemove.length > 0) {
|
||||
|
||||
// Only remove nodes if the new list is not empty (prevents accidental clearing)
|
||||
if (nodesToRemove.length > 0 && newNodes.length > 0) {
|
||||
console.log(
|
||||
"🗑️ Removing nodes:",
|
||||
nodesToRemove.map((n) => n.id)
|
||||
);
|
||||
removeNodes(nodesToRemove);
|
||||
} else if (nodesToRemove.length > 0 && newNodes.length === 0) {
|
||||
console.log(
|
||||
"⚠️ Not removing nodes - new list is empty, might be temporary state"
|
||||
);
|
||||
}
|
||||
|
||||
// Add new nodes that aren't already present
|
||||
@ -1254,22 +1269,51 @@ function fromObject(flowObject) {
|
||||
// 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]);
|
||||
// Check if we're restoring the same data that's already there
|
||||
const currentNodes = nodes.value.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: n.position,
|
||||
}));
|
||||
const newNodes =
|
||||
flowObject.nodes?.map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: n.position,
|
||||
})) || [];
|
||||
|
||||
const nodesAreSame =
|
||||
currentNodes.length === newNodes.length &&
|
||||
currentNodes.every(
|
||||
(node, index) =>
|
||||
node.id === newNodes[index]?.id &&
|
||||
node.type === newNodes[index]?.type
|
||||
);
|
||||
|
||||
// Only clear if we're actually restoring different data
|
||||
if (!nodesAreSame) {
|
||||
// 1. First, clear existing state
|
||||
if (nodes.value.length > 0) {
|
||||
console.log("🔄 Clearing existing nodes for restoration");
|
||||
removeNodes([...nodes.value]);
|
||||
}
|
||||
if (edges.value.length > 0) {
|
||||
console.log("🔄 Clearing existing edges for restoration");
|
||||
removeEdges([...edges.value]);
|
||||
}
|
||||
} else {
|
||||
console.log("✅ Nodes are the same, skipping clear operation");
|
||||
}
|
||||
|
||||
// Wait for clearing to complete
|
||||
await nextTick();
|
||||
|
||||
// 2. Restore nodes first
|
||||
// 2. Restore nodes first (only if we cleared or if no nodes exist)
|
||||
if (
|
||||
flowObject.nodes &&
|
||||
Array.isArray(flowObject.nodes) &&
|
||||
flowObject.nodes.length > 0
|
||||
flowObject.nodes.length > 0 &&
|
||||
(!nodesAreSame || nodes.value.length === 0)
|
||||
) {
|
||||
const nodesToRestore = flowObject.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
@ -1280,19 +1324,23 @@ function fromObject(flowObject) {
|
||||
// Only include essential properties needed for Vue Flow
|
||||
}));
|
||||
|
||||
console.log("🔄 Restoring", nodesToRestore.length, "nodes");
|
||||
addNodes(nodesToRestore);
|
||||
await nextTick();
|
||||
|
||||
// Wait a bit more for nodes to be fully initialized
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
} else if (nodesAreSame) {
|
||||
console.log("✅ Nodes already exist, skipping node restoration");
|
||||
}
|
||||
|
||||
// 3. Restore edges after nodes are ready
|
||||
// 3. Restore edges after nodes are ready (only if we cleared or if no edges exist)
|
||||
if (
|
||||
flowObject.edges &&
|
||||
Array.isArray(flowObject.edges) &&
|
||||
flowObject.edges.length > 0 &&
|
||||
nodes.value.length > 0
|
||||
nodes.value.length > 0 &&
|
||||
(!nodesAreSame || edges.value.length === 0)
|
||||
) {
|
||||
// Verify all edges have valid source and target nodes
|
||||
const validEdges = flowObject.edges.filter((edge) => {
|
||||
@ -1328,9 +1376,12 @@ function fromObject(flowObject) {
|
||||
// Only include essential properties
|
||||
}));
|
||||
|
||||
console.log("🔄 Restoring", cleanEdges.length, "edges");
|
||||
addEdges(cleanEdges);
|
||||
await nextTick();
|
||||
}
|
||||
} else if (nodesAreSame) {
|
||||
console.log("✅ Edges already exist, skipping edge restoration");
|
||||
}
|
||||
|
||||
// 4. Finally, restore viewport position and zoom
|
||||
|
@ -1331,25 +1331,37 @@ const saveProcess = async () => {
|
||||
|
||||
// Add a component handler to add components from the component panel
|
||||
const onAddComponent = async (component) => {
|
||||
if (isAddingComponent.value) return; // Prevent concurrent additions
|
||||
if (isAddingComponent.value || isProcessLoading.value) return; // Prevent concurrent additions or interference with process loading
|
||||
|
||||
try {
|
||||
isAddingComponent.value = true;
|
||||
console.log('🎯 Adding component:', component.type);
|
||||
|
||||
// Create a new node from the component definition
|
||||
// The component structure from ProcessBuilderComponents is:
|
||||
// { type, label, data: { ...defaultProps.data } }
|
||||
|
||||
// Calculate a better position for the new node to avoid overlap
|
||||
let nodePosition = { x: 100, y: 100 };
|
||||
if (processStore.currentProcess && processStore.currentProcess.nodes.length > 0) {
|
||||
// Find the rightmost and bottommost positions
|
||||
const maxX = Math.max(...processStore.currentProcess.nodes.map(n => n.position?.x || 0));
|
||||
const maxY = Math.max(...processStore.currentProcess.nodes.map(n => n.position?.y || 0));
|
||||
|
||||
// Place new node to the right with some spacing
|
||||
nodePosition = {
|
||||
x: maxX + 200, // 200px spacing from rightmost node
|
||||
y: Math.max(100, maxY) // At least 100px from top, or at the level of the bottommost node
|
||||
};
|
||||
}
|
||||
|
||||
const newNode = {
|
||||
id: `${component.type}_${Date.now()}`,
|
||||
type: component.type,
|
||||
position: { x: 100, y: 100 }, // Default position
|
||||
position: nodePosition,
|
||||
label: component.label,
|
||||
data: {
|
||||
...component.data,
|
||||
// Ensure shape is set for new nodes
|
||||
shape: component.data.shape || (component.type === 'gateway' ? 'diamond' : 'rectangle'),
|
||||
// Ensure default colors are set for new nodes
|
||||
backgroundColor: component.data.backgroundColor,
|
||||
borderColor: component.data.borderColor,
|
||||
textColor: component.data.textColor
|
||||
...component.data
|
||||
}
|
||||
};
|
||||
|
||||
@ -1390,78 +1402,51 @@ const onAddComponent = async (component) => {
|
||||
}
|
||||
|
||||
// Add the node to the process store
|
||||
await processStore.addNode(newNode);
|
||||
console.log('📝 Adding node to store:', newNode.id);
|
||||
const addedNode = await processStore.addNode(newNode);
|
||||
|
||||
if (!addedNode) {
|
||||
console.error('❌ Failed to add node to store');
|
||||
toast.error('Failed to add component to store. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for store update and next render cycle
|
||||
await nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// CRITICAL FIX: Instead of calling syncCanvas (which can cause edge removal/re-addition),
|
||||
// we'll add the node directly to the canvas and preserve existing edges
|
||||
// Add the node directly to the canvas using Vue Flow's addNode method
|
||||
if (processFlowCanvas.value) {
|
||||
try {
|
||||
// Get the fresh node from store (with any store-side modifications)
|
||||
const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id);
|
||||
console.log('🎨 Adding node to canvas:', addedNode.id);
|
||||
|
||||
if (freshNode && processFlowCanvas.value.addNode) {
|
||||
// Add only the new node to the canvas directly
|
||||
processFlowCanvas.value.addNode(freshNode);
|
||||
|
||||
// Wait for the node to be added to the canvas
|
||||
await nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// Select the newly added node after it's stable
|
||||
onNodeSelected(freshNode);
|
||||
|
||||
console.log('✅ Successfully added new node without affecting existing edges');
|
||||
} else {
|
||||
console.warn('⚠️ Fresh node not found in store, falling back to full sync');
|
||||
// Fallback to full sync if something went wrong
|
||||
const currentNodes = processStore.currentProcess?.nodes || [];
|
||||
const currentEdges = processStore.currentProcess?.edges || [];
|
||||
|
||||
if (processFlowCanvas.value.syncCanvas) {
|
||||
processFlowCanvas.value.syncCanvas(currentNodes, currentEdges);
|
||||
|
||||
await nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const addedNode = currentNodes.find(n => n.id === newNode.id);
|
||||
if (addedNode) {
|
||||
onNodeSelected(addedNode);
|
||||
}
|
||||
}
|
||||
// Use Vue Flow's addNode method directly to avoid any sync issues
|
||||
if (processFlowCanvas.value.addNode) {
|
||||
processFlowCanvas.value.addNode(addedNode);
|
||||
}
|
||||
|
||||
// Wait for the node to be added to the canvas
|
||||
await nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Select the newly added node after it's stable
|
||||
onNodeSelected(addedNode);
|
||||
|
||||
console.log('✅ Successfully added new node:', addedNode.id, 'at position:', addedNode.position);
|
||||
} catch (error) {
|
||||
console.error('❌ Error adding node to canvas:', error);
|
||||
|
||||
// Fallback to full sync if direct addition fails
|
||||
const currentNodes = processStore.currentProcess?.nodes || [];
|
||||
const currentEdges = processStore.currentProcess?.edges || [];
|
||||
|
||||
if (processFlowCanvas.value.syncCanvas) {
|
||||
console.log('🔄 Falling back to full canvas sync due to error');
|
||||
processFlowCanvas.value.syncCanvas(currentNodes, currentEdges);
|
||||
|
||||
await nextTick();
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
const addedNode = currentNodes.find(n => n.id === newNode.id);
|
||||
if (addedNode) {
|
||||
onNodeSelected(addedNode);
|
||||
}
|
||||
}
|
||||
toast.error('Failed to add component to canvas. Please try again.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding component:', error);
|
||||
console.error('❌ Error adding component:', error);
|
||||
toast.error('Failed to add component. Please try again.');
|
||||
} finally {
|
||||
// Reset the flag after a longer delay to ensure canvas is stable
|
||||
setTimeout(() => {
|
||||
console.log('🏁 Component addition completed, resetting flag');
|
||||
isAddingComponent.value = false;
|
||||
}, 200);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1504,6 +1489,9 @@ const applyProcessTemplate = async (template) => {
|
||||
|
||||
// Process nodes first and wait for them to be fully added
|
||||
for (const node of templateNodes) {
|
||||
// Get default styling for the node type
|
||||
const defaultStyling = getDefaultNodeStyling(node.type);
|
||||
|
||||
const newNode = {
|
||||
...node,
|
||||
id: node.id, // Keep original ID for edge references
|
||||
@ -1511,7 +1499,12 @@ const applyProcessTemplate = async (template) => {
|
||||
position: node.position || { x: 100, y: 100 },
|
||||
data: {
|
||||
...node.data,
|
||||
label: node.data?.label || node.label || `${node.type} node`
|
||||
label: node.data?.label || node.label || `${node.type} node`,
|
||||
// Preserve styling properties from template or use defaults
|
||||
backgroundColor: node.data?.backgroundColor || defaultStyling.backgroundColor,
|
||||
borderColor: node.data?.borderColor || defaultStyling.borderColor,
|
||||
textColor: node.data?.textColor || defaultStyling.textColor,
|
||||
shape: node.data?.shape || defaultStyling.shape
|
||||
}
|
||||
};
|
||||
|
||||
@ -1584,6 +1577,78 @@ const applyProcessTemplate = async (template) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Get default styling for node types
|
||||
const getDefaultNodeStyling = (nodeType) => {
|
||||
const stylingMap = {
|
||||
'start': {
|
||||
backgroundColor: '#dcfce7',
|
||||
borderColor: '#10b981',
|
||||
textColor: '#065f46',
|
||||
shape: 'circle'
|
||||
},
|
||||
'end': {
|
||||
backgroundColor: '#fee2e2',
|
||||
borderColor: '#dc2626',
|
||||
textColor: '#991b1b',
|
||||
shape: 'circle'
|
||||
},
|
||||
'form': {
|
||||
backgroundColor: '#faf5ff',
|
||||
borderColor: '#9333ea',
|
||||
textColor: '#6b21a8',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'api': {
|
||||
backgroundColor: '#eff6ff',
|
||||
borderColor: '#3b82f6',
|
||||
textColor: '#1e40af',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'gateway': {
|
||||
backgroundColor: '#fff7ed',
|
||||
borderColor: '#f97316',
|
||||
textColor: '#c2410c',
|
||||
shape: 'diamond'
|
||||
},
|
||||
'notification': {
|
||||
backgroundColor: '#f0f9ff',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0284c7',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'business-rule': {
|
||||
backgroundColor: '#fdf4ff',
|
||||
borderColor: '#a855f7',
|
||||
textColor: '#7c3aed',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'script': {
|
||||
backgroundColor: '#f9fafb',
|
||||
borderColor: '#6b7280',
|
||||
textColor: '#374151',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'html': {
|
||||
backgroundColor: '#e0f2fe',
|
||||
borderColor: '#0ea5e9',
|
||||
textColor: '#0c4a6e',
|
||||
shape: 'rectangle'
|
||||
},
|
||||
'subprocess': {
|
||||
backgroundColor: '#f0fdfa',
|
||||
borderColor: '#14b8a6',
|
||||
textColor: '#134e4a',
|
||||
shape: 'rectangle'
|
||||
}
|
||||
};
|
||||
|
||||
return stylingMap[nodeType] || {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
textColor: '#374151',
|
||||
shape: 'rectangle'
|
||||
};
|
||||
};
|
||||
// Fix references to functions
|
||||
const onFormSelected = (formData) => {
|
||||
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
||||
@ -1818,62 +1883,85 @@ watch(() => canvasNodes.value, (newNodes) => {
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 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;
|
||||
|
||||
// Track if we're in the middle of adding a component to prevent interference
|
||||
const isProcessLoading = ref(false);
|
||||
|
||||
// Watch for process changes to restore Vue Flow state
|
||||
// Only trigger when the entire process object changes (different process loaded)
|
||||
watch(() => processStore.currentProcess?.id, async (newProcessId, oldProcessId) => {
|
||||
if (!newProcessId) return;
|
||||
|
||||
// Only restore when a different process is loaded (not on updates to the same process)
|
||||
if (oldProcessId && newProcessId === oldProcessId) return;
|
||||
|
||||
// Don't interfere if we're in the middle of adding a component or modifying nodes
|
||||
if (isAddingComponent.value || processStore.isModifyingNodes) {
|
||||
console.log('🚫 Skipping process restoration during component/node modification');
|
||||
return;
|
||||
}
|
||||
|
||||
const newProcess = processStore.currentProcess;
|
||||
console.log('🔄 Loading process:', newProcessId, 'Nodes:', newProcess.nodes?.length || 0);
|
||||
isProcessLoading.value = true;
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
|
||||
// Use Vue Flow's proper restoration method (now returns a Promise)
|
||||
await processFlowCanvas.value.fromObject(flowObject);
|
||||
|
||||
|
||||
|
||||
// Fit view after restoration with a small delay to ensure everything is rendered
|
||||
setTimeout(() => {
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
||||
processFlowCanvas.value.fitView();
|
||||
try {
|
||||
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 with', flowObject.nodes.length, 'nodes');
|
||||
|
||||
// Use Vue Flow's proper restoration method (now returns a Promise)
|
||||
await processFlowCanvas.value.fromObject(flowObject);
|
||||
|
||||
console.log('✅ Vue Flow state restored successfully');
|
||||
|
||||
// Only fit view if nodes don't have meaningful positions (all at 0,0)
|
||||
// This prevents repositioning nodes that were carefully positioned
|
||||
const hasPositionedNodes = flowObject.nodes.some(node =>
|
||||
node.position && (node.position.x !== 0 || node.position.y !== 0)
|
||||
);
|
||||
|
||||
if (!hasPositionedNodes && flowObject.nodes.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
||||
processFlowCanvas.value.fitView();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 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 sync');
|
||||
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Vue Flow canvas not available for restoration, using sync fallback');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error restoring Vue Flow state:', error);
|
||||
|
||||
// Fallback to manual sync if Vue Flow restoration fails
|
||||
// Fallback sync method
|
||||
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||
|
||||
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);
|
||||
}
|
||||
} finally {
|
||||
isProcessLoading.value = false;
|
||||
}
|
||||
}, 200); // Allow time for canvas to initialize
|
||||
}, { immediate: false });
|
||||
|
@ -11,7 +11,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
history: [],
|
||||
historyIndex: -1,
|
||||
unsavedChanges: false,
|
||||
lastChangeDescription: ''
|
||||
lastChangeDescription: '',
|
||||
isModifyingNodes: false // Flag to prevent interference during node operations
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -653,37 +654,48 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
return existingNode;
|
||||
}
|
||||
|
||||
// Create a new node with proper data structure
|
||||
const newNode = {
|
||||
id: node.id || uuidv4(),
|
||||
type: node.type,
|
||||
label: node.label || node.data?.label || 'New Node',
|
||||
position: node.position || { x: 100, y: 100 },
|
||||
data: {
|
||||
...node.data,
|
||||
label: node.data?.label || node.label || 'New Node',
|
||||
// Ensure shape is set for new nodes
|
||||
shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'),
|
||||
// Ensure default colors are set for new nodes
|
||||
backgroundColor: node.data?.backgroundColor || '#ffffff',
|
||||
borderColor: node.data?.borderColor || '#000000'
|
||||
}
|
||||
};
|
||||
// Set flag to prevent interference during node addition
|
||||
this.isModifyingNodes = true;
|
||||
|
||||
// Create a deep copy to avoid reference issues
|
||||
const nodeCopy = JSON.parse(JSON.stringify(newNode));
|
||||
try {
|
||||
// Create a new node with proper data structure
|
||||
const newNode = {
|
||||
id: node.id || uuidv4(),
|
||||
type: node.type,
|
||||
label: node.label || node.data?.label || 'New Node',
|
||||
position: node.position || { x: 100, y: 100 },
|
||||
data: {
|
||||
...node.data,
|
||||
label: node.data?.label || node.label || 'New Node',
|
||||
// Ensure shape is set for new nodes
|
||||
shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'),
|
||||
// Ensure default colors are set for new nodes
|
||||
backgroundColor: node.data?.backgroundColor || '#ffffff',
|
||||
borderColor: node.data?.borderColor || '#000000'
|
||||
}
|
||||
};
|
||||
|
||||
// Add to current process nodes array
|
||||
this.currentProcess.nodes = [...this.currentProcess.nodes, nodeCopy];
|
||||
|
||||
// Update selection
|
||||
this.selectedNodeId = nodeCopy.id;
|
||||
|
||||
// Save to history
|
||||
this.saveToHistory('Add node');
|
||||
this.unsavedChanges = true;
|
||||
// Create a deep copy to avoid reference issues
|
||||
const nodeCopy = JSON.parse(JSON.stringify(newNode));
|
||||
|
||||
return nodeCopy;
|
||||
// Add to current process nodes array using push to avoid creating new array reference
|
||||
// This prevents triggering the watch that monitors currentProcess changes
|
||||
this.currentProcess.nodes.push(nodeCopy);
|
||||
|
||||
// Update selection
|
||||
this.selectedNodeId = nodeCopy.id;
|
||||
|
||||
// Save to history
|
||||
this.saveToHistory('Add node');
|
||||
this.unsavedChanges = true;
|
||||
|
||||
console.log('📝 Store: Added node', nodeCopy.id, 'Total nodes:', this.currentProcess.nodes.length);
|
||||
|
||||
return nodeCopy;
|
||||
} finally {
|
||||
// Reset flag after operation completes
|
||||
this.isModifyingNodes = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -706,36 +718,46 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
deleteNode(nodeId) {
|
||||
if (!this.currentProcess) return;
|
||||
|
||||
// Find the node index
|
||||
const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId);
|
||||
if (index !== -1) {
|
||||
// Remove the node
|
||||
this.currentProcess.nodes.splice(index, 1);
|
||||
|
||||
// Remove any edges connected to this node
|
||||
const edgesToRemove = this.currentProcess.edges.filter(
|
||||
edge => edge.source === nodeId || edge.target === nodeId
|
||||
);
|
||||
|
||||
edgesToRemove.forEach(edge => {
|
||||
const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id);
|
||||
if (edgeIndex !== -1) {
|
||||
this.currentProcess.edges.splice(edgeIndex, 1);
|
||||
// Set flag to prevent interference during node deletion
|
||||
this.isModifyingNodes = true;
|
||||
|
||||
try {
|
||||
// Find the node index
|
||||
const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId);
|
||||
if (index !== -1) {
|
||||
// Remove the node using splice to avoid creating new array reference
|
||||
this.currentProcess.nodes.splice(index, 1);
|
||||
|
||||
// Remove any edges connected to this node
|
||||
const edgesToRemove = this.currentProcess.edges.filter(
|
||||
edge => edge.source === nodeId || edge.target === nodeId
|
||||
);
|
||||
|
||||
edgesToRemove.forEach(edge => {
|
||||
const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id);
|
||||
if (edgeIndex !== -1) {
|
||||
this.currentProcess.edges.splice(edgeIndex, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selection if the deleted node was selected
|
||||
if (this.selectedNodeId === nodeId) {
|
||||
this.selectedNodeId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selection if the deleted node was selected
|
||||
if (this.selectedNodeId === nodeId) {
|
||||
this.selectedNodeId = null;
|
||||
this.saveToHistory('Delete node');
|
||||
this.unsavedChanges = true;
|
||||
|
||||
console.log('🗑️ Store: Deleted node', nodeId, 'Total nodes:', this.currentProcess.nodes.length);
|
||||
|
||||
return true; // Return success
|
||||
}
|
||||
|
||||
this.saveToHistory('Delete node');
|
||||
this.unsavedChanges = true;
|
||||
|
||||
return true; // Return success
|
||||
return false; // Return failure
|
||||
} finally {
|
||||
// Reset flag after operation completes
|
||||
this.isModifyingNodes = false;
|
||||
}
|
||||
|
||||
return false; // Return failure
|
||||
},
|
||||
|
||||
/**
|
||||
@ -744,22 +766,33 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
addEdge(edge) {
|
||||
if (!this.currentProcess) return;
|
||||
|
||||
const newEdge = {
|
||||
id: edge.id || `${edge.source}-${edge.target}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label || '',
|
||||
type: edge.type || 'default',
|
||||
animated: edge.animated !== undefined ? edge.animated : true,
|
||||
data: edge.data || {}
|
||||
};
|
||||
// Set flag to prevent interference during edge addition
|
||||
this.isModifyingNodes = true;
|
||||
|
||||
this.currentProcess.edges.push(newEdge);
|
||||
this.selectedEdgeId = newEdge.id;
|
||||
this.saveToHistory('Add edge');
|
||||
this.unsavedChanges = true;
|
||||
try {
|
||||
const newEdge = {
|
||||
id: edge.id || `${edge.source}-${edge.target}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.label || '',
|
||||
type: edge.type || 'default',
|
||||
animated: edge.animated !== undefined ? edge.animated : true,
|
||||
data: edge.data || {}
|
||||
};
|
||||
|
||||
return newEdge;
|
||||
// Use push to avoid creating new array reference
|
||||
this.currentProcess.edges.push(newEdge);
|
||||
this.selectedEdgeId = newEdge.id;
|
||||
this.saveToHistory('Add edge');
|
||||
this.unsavedChanges = true;
|
||||
|
||||
console.log('🔗 Store: Added edge', newEdge.id, 'Total edges:', this.currentProcess.edges.length);
|
||||
|
||||
return newEdge;
|
||||
} finally {
|
||||
// Reset flag after operation completes
|
||||
this.isModifyingNodes = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user