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"
|
v-for="cat in categories"
|
||||||
:key="cat.id"
|
:key="cat.id"
|
||||||
@click="activeCategory = cat.id"
|
@click="activeCategory = cat.id"
|
||||||
class="px-4 py-2 text-sm font-medium"
|
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' : 'text-gray-500 hover:text-gray-700'"
|
: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 }}
|
{{ cat.name }}
|
||||||
</button>
|
</button>
|
||||||
@ -18,38 +18,65 @@
|
|||||||
|
|
||||||
<!-- Templates Grid -->
|
<!-- Templates Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<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 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-gray-50 p-4 border-b relative">
|
<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="w-full h-full flex flex-col items-center justify-center">
|
||||||
<div class="template-icon-wrapper">
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h3 class="font-medium text-lg mb-1">{{ template.name }}</h3>
|
<h3 class="font-semibold text-lg mb-2 text-gray-800">{{ template.name }}</h3>
|
||||||
<p class="text-gray-600 text-sm mb-4">{{ template.description }}</p>
|
<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 text-xs text-gray-500 mb-4 space-x-4">
|
||||||
<div class="flex items-center mr-4">
|
<div class="flex items-center">
|
||||||
<Icon name="material-symbols:account-tree" class="mr-1 w-3 h-3" />
|
<Icon name="material-symbols:account-tree" class="mr-1 w-3 h-3" />
|
||||||
<span>{{ template.nodeCount }} nodes</span>
|
<span>{{ template.nodeCount }} nodes</span>
|
||||||
</div>
|
</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" />
|
<Icon name="material-symbols:route" class="mr-1 w-3 h-3" />
|
||||||
<span>{{ template.edgeCount }} connections</span>
|
<span>{{ template.edgeCount }} connections</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Icon name="material-symbols:timer" class="mr-1 w-3 h-3" />
|
<Icon name="material-symbols:timer" class="mr-1 w-3 h-3" />
|
||||||
<span>{{ template.complexity }}</span>
|
<span class="capitalize">{{ template.complexity }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RsButton @click="useTemplate(template)" variant="primary" size="sm" class="w-full">
|
<div class="flex space-x-2">
|
||||||
Use Template
|
<RsButton
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -62,9 +89,148 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RsModal>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -79,6 +245,10 @@ const showModal = computed({
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Preview modal state
|
||||||
|
const showPreviewModal = ref(false);
|
||||||
|
const selectedTemplate = ref(null);
|
||||||
|
|
||||||
// Categories
|
// Categories
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 'all', name: 'All Templates' },
|
{ id: 'all', name: 'All Templates' },
|
||||||
@ -132,7 +302,11 @@ const templates = [
|
|||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
data: {
|
data: {
|
||||||
label: 'Start',
|
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',
|
label: 'Submit Request',
|
||||||
description: 'Employee submits approval request',
|
description: 'Employee submits approval request',
|
||||||
formId: null,
|
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',
|
recipientRole: 'manager',
|
||||||
subject: 'New approval request',
|
subject: 'New approval request',
|
||||||
message: 'A new approval request requires your review.',
|
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',
|
label: 'Manager Review',
|
||||||
description: 'Manager reviews and approves/rejects',
|
description: 'Manager reviews and approves/rejects',
|
||||||
formId: null,
|
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'
|
output: 'Approved'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultPath: 'Rejected'
|
defaultPath: 'Rejected',
|
||||||
|
backgroundColor: '#fff7ed',
|
||||||
|
borderColor: '#f97316',
|
||||||
|
textColor: '#c2410c',
|
||||||
|
shape: 'diamond'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -202,7 +392,11 @@ const templates = [
|
|||||||
recipientVariable: 'requester_email',
|
recipientVariable: 'requester_email',
|
||||||
subject: 'Request Approved',
|
subject: 'Request Approved',
|
||||||
message: 'Your request has been 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',
|
recipientVariable: 'requester_email',
|
||||||
subject: 'Request Rejected',
|
subject: 'Request Rejected',
|
||||||
message: 'Your request has been 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 },
|
position: { x: 1300, y: 100 },
|
||||||
data: {
|
data: {
|
||||||
label: 'End',
|
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;
|
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
|
// Get icon based on template category/type
|
||||||
const getTemplateIcon = (template) => {
|
const getTemplateIcon = (template) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
@ -1034,6 +1251,229 @@ const getTemplateIcon = (template) => {
|
|||||||
|
|
||||||
return icons[template.category] || 'material-symbols:account-tree';
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -1044,11 +1484,13 @@ const getTemplateIcon = (template) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.template-card {
|
.template-card {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card:hover {
|
.template-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-preview {
|
.template-preview {
|
||||||
@ -1057,6 +1499,7 @@ const getTemplateIcon = (template) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-icon-wrapper {
|
.template-icon-wrapper {
|
||||||
@ -1067,7 +1510,221 @@ const getTemplateIcon = (template) => {
|
|||||||
height: 80px;
|
height: 80px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: white;
|
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;
|
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>
|
</style>
|
@ -74,7 +74,11 @@ const availableComponents = [
|
|||||||
defaultProps: {
|
defaultProps: {
|
||||||
label: 'Start',
|
label: 'Start',
|
||||||
data: {
|
data: {
|
||||||
description: 'Process start point'
|
description: 'Process start point',
|
||||||
|
shape: 'circle',
|
||||||
|
backgroundColor: '#dcfce7',
|
||||||
|
borderColor: '#10b981',
|
||||||
|
textColor: '#065f46'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -87,7 +91,11 @@ const availableComponents = [
|
|||||||
defaultProps: {
|
defaultProps: {
|
||||||
label: 'End',
|
label: 'End',
|
||||||
data: {
|
data: {
|
||||||
description: 'Process end point'
|
description: 'Process end point',
|
||||||
|
shape: 'circle',
|
||||||
|
backgroundColor: '#fee2e2',
|
||||||
|
borderColor: '#dc2626',
|
||||||
|
textColor: '#991b1b'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -103,7 +111,11 @@ const availableComponents = [
|
|||||||
data: {
|
data: {
|
||||||
description: 'Form submission task',
|
description: 'Form submission task',
|
||||||
formId: null,
|
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" }',
|
headers: '{ "Content-Type": "application/json" }',
|
||||||
outputVariable: 'apiResponse',
|
outputVariable: 'apiResponse',
|
||||||
continueOnError: false,
|
continueOnError: false,
|
||||||
errorVariable: 'apiError'
|
errorVariable: 'apiError',
|
||||||
|
shape: 'rectangle',
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
textColor: '#1e40af'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -138,7 +154,11 @@ const availableComponents = [
|
|||||||
data: {
|
data: {
|
||||||
description: 'Decision point for branching the workflow',
|
description: 'Decision point for branching the workflow',
|
||||||
conditions: [],
|
conditions: [],
|
||||||
defaultPath: 'Default'
|
defaultPath: 'Default',
|
||||||
|
shape: 'diamond',
|
||||||
|
backgroundColor: '#fff7ed',
|
||||||
|
borderColor: '#f97316',
|
||||||
|
textColor: '#c2410c'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -170,7 +190,11 @@ const availableComponents = [
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
value: 24,
|
value: 24,
|
||||||
unit: 'hours'
|
unit: 'hours'
|
||||||
}
|
},
|
||||||
|
shape: 'rectangle',
|
||||||
|
backgroundColor: '#f0f9ff',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
textColor: '#0284c7'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -185,7 +209,11 @@ const availableComponents = [
|
|||||||
data: {
|
data: {
|
||||||
description: 'Apply business rules',
|
description: 'Apply business rules',
|
||||||
ruleGroups: [],
|
ruleGroups: [],
|
||||||
priority: 'medium'
|
priority: 'medium',
|
||||||
|
shape: 'rectangle',
|
||||||
|
backgroundColor: '#fdf4ff',
|
||||||
|
borderColor: '#a855f7',
|
||||||
|
textColor: '#7c3aed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -202,7 +230,11 @@ const availableComponents = [
|
|||||||
scriptCode: '',
|
scriptCode: '',
|
||||||
scriptLanguage: 'javascript',
|
scriptLanguage: 'javascript',
|
||||||
inputVariables: [],
|
inputVariables: [],
|
||||||
outputVariables: []
|
outputVariables: [],
|
||||||
|
shape: 'rectangle',
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
borderColor: '#6b7280',
|
||||||
|
textColor: '#374151'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -222,7 +254,11 @@ const availableComponents = [
|
|||||||
inputVariables: [],
|
inputVariables: [],
|
||||||
outputVariables: [],
|
outputVariables: [],
|
||||||
allowVariableAccess: true,
|
allowVariableAccess: true,
|
||||||
autoRefresh: false
|
autoRefresh: false,
|
||||||
|
shape: 'rectangle',
|
||||||
|
backgroundColor: '#e0f2fe',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
textColor: '#0c4a6e'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -237,7 +273,11 @@ const availableComponents = [
|
|||||||
data: {
|
data: {
|
||||||
description: 'Executes another process',
|
description: 'Executes another process',
|
||||||
subprocessId: null,
|
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
|
// Add a component directly via click
|
||||||
const addComponent = (component) => {
|
const addComponent = (component) => {
|
||||||
return;
|
|
||||||
// Use same format as drag operation for consistency
|
// Use same format as drag operation for consistency
|
||||||
const componentData = {
|
const componentData = {
|
||||||
type: component.type,
|
type: component.type,
|
||||||
|
@ -39,24 +39,24 @@ import "@vue-flow/minimap/dist/style.css";
|
|||||||
|
|
||||||
// Create nodeTypes object with markRaw to prevent reactivity issues
|
// Create nodeTypes object with markRaw to prevent reactivity issues
|
||||||
const customNodeTypes = {
|
const customNodeTypes = {
|
||||||
'start': markRaw(StartNode),
|
start: markRaw(StartNode),
|
||||||
'end': markRaw(EndNode),
|
end: markRaw(EndNode),
|
||||||
'form': markRaw(FormNode),
|
form: markRaw(FormNode),
|
||||||
'api': markRaw(ApiNode),
|
api: markRaw(ApiNode),
|
||||||
'gateway': markRaw(GatewayNode),
|
gateway: markRaw(GatewayNode),
|
||||||
'script': markRaw(ScriptNode),
|
script: markRaw(ScriptNode),
|
||||||
'business-rule': markRaw(BusinessRuleNode),
|
"business-rule": markRaw(BusinessRuleNode),
|
||||||
'notification': markRaw(NotificationNode),
|
notification: markRaw(NotificationNode),
|
||||||
'html': markRaw(HtmlNode),
|
html: markRaw(HtmlNode),
|
||||||
'subprocess': markRaw(SubprocessNode),
|
subprocess: markRaw(SubprocessNode),
|
||||||
// Shape nodes
|
// Shape nodes
|
||||||
'hexagon-shape': markRaw(HexagonShape),
|
"hexagon-shape": markRaw(HexagonShape),
|
||||||
'trapezoid-shape': markRaw(TrapezoidShape),
|
"trapezoid-shape": markRaw(TrapezoidShape),
|
||||||
'rectangle-shape': markRaw(RectangleShape),
|
"rectangle-shape": markRaw(RectangleShape),
|
||||||
'swimlane-horizontal': markRaw(SwimlaneHorizontal),
|
"swimlane-horizontal": markRaw(SwimlaneHorizontal),
|
||||||
'swimlane-vertical': markRaw(SwimlaneVertical),
|
"swimlane-vertical": markRaw(SwimlaneVertical),
|
||||||
'text-annotation': markRaw(TextAnnotation),
|
"text-annotation": markRaw(TextAnnotation),
|
||||||
'process-group': markRaw(ProcessGroup)
|
"process-group": markRaw(ProcessGroup),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add Material Icons import
|
// Add Material Icons import
|
||||||
@ -276,10 +276,18 @@ onMounted(() => {
|
|||||||
// Setup window resize handler
|
// Setup window resize handler
|
||||||
window.addEventListener("resize", resizeFlow);
|
window.addEventListener("resize", resizeFlow);
|
||||||
|
|
||||||
// Initial fit view
|
// Only fit view initially if there are no existing nodes with positions
|
||||||
setTimeout(() => {
|
// This prevents repositioning existing nodes when the canvas mounts
|
||||||
fitView();
|
const hasExistingNodes = props.initialNodes.length > 0;
|
||||||
}, 100);
|
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
|
// Center on a specific node
|
||||||
@ -869,7 +877,7 @@ defineExpose({
|
|||||||
nodes.value = newNodes;
|
nodes.value = newNodes;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in setNodes:', error);
|
console.error("Error in setNodes:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setEdges: (newEdges) => {
|
setEdges: (newEdges) => {
|
||||||
@ -878,13 +886,13 @@ defineExpose({
|
|||||||
edges.value = newEdges;
|
edges.value = newEdges;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in setEdges:', error);
|
console.error("Error in setEdges:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Provide access to the flow instance
|
// Provide access to the flow instance
|
||||||
get flowInstance() {
|
get flowInstance() {
|
||||||
return flowInstance;
|
return flowInstance;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update an existing node
|
// Update an existing node
|
||||||
@ -978,15 +986,22 @@ function syncCanvas(newNodes, newEdges) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Remove nodes that are no longer in the new list
|
// 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(
|
const nodesToRemove = nodes.value.filter(
|
||||||
(node) => !newNodeIds.has(node.id)
|
(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(
|
console.log(
|
||||||
"🗑️ Removing nodes:",
|
"🗑️ Removing nodes:",
|
||||||
nodesToRemove.map((n) => n.id)
|
nodesToRemove.map((n) => n.id)
|
||||||
);
|
);
|
||||||
removeNodes(nodesToRemove);
|
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
|
// Add new nodes that aren't already present
|
||||||
@ -1254,22 +1269,51 @@ function fromObject(flowObject) {
|
|||||||
// Wait for any pending operations to complete
|
// Wait for any pending operations to complete
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
// 1. First, clear existing state
|
// Check if we're restoring the same data that's already there
|
||||||
if (nodes.value.length > 0) {
|
const currentNodes = nodes.value.map((n) => ({
|
||||||
removeNodes([...nodes.value]);
|
id: n.id,
|
||||||
}
|
type: n.type,
|
||||||
if (edges.value.length > 0) {
|
position: n.position,
|
||||||
removeEdges([...edges.value]);
|
}));
|
||||||
|
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
|
// Wait for clearing to complete
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// 2. Restore nodes first
|
// 2. Restore nodes first (only if we cleared or if no nodes exist)
|
||||||
if (
|
if (
|
||||||
flowObject.nodes &&
|
flowObject.nodes &&
|
||||||
Array.isArray(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) => ({
|
const nodesToRestore = flowObject.nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
@ -1280,19 +1324,23 @@ function fromObject(flowObject) {
|
|||||||
// Only include essential properties needed for Vue Flow
|
// Only include essential properties needed for Vue Flow
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log("🔄 Restoring", nodesToRestore.length, "nodes");
|
||||||
addNodes(nodesToRestore);
|
addNodes(nodesToRestore);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Wait a bit more for nodes to be fully initialized
|
// Wait a bit more for nodes to be fully initialized
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
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 (
|
if (
|
||||||
flowObject.edges &&
|
flowObject.edges &&
|
||||||
Array.isArray(flowObject.edges) &&
|
Array.isArray(flowObject.edges) &&
|
||||||
flowObject.edges.length > 0 &&
|
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
|
// Verify all edges have valid source and target nodes
|
||||||
const validEdges = flowObject.edges.filter((edge) => {
|
const validEdges = flowObject.edges.filter((edge) => {
|
||||||
@ -1328,9 +1376,12 @@ function fromObject(flowObject) {
|
|||||||
// Only include essential properties
|
// Only include essential properties
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log("🔄 Restoring", cleanEdges.length, "edges");
|
||||||
addEdges(cleanEdges);
|
addEdges(cleanEdges);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
}
|
}
|
||||||
|
} else if (nodesAreSame) {
|
||||||
|
console.log("✅ Edges already exist, skipping edge restoration");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Finally, restore viewport position and zoom
|
// 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
|
// Add a component handler to add components from the component panel
|
||||||
const onAddComponent = async (component) => {
|
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 {
|
try {
|
||||||
isAddingComponent.value = true;
|
isAddingComponent.value = true;
|
||||||
|
console.log('🎯 Adding component:', component.type);
|
||||||
|
|
||||||
// Create a new node from the component definition
|
// 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 = {
|
const newNode = {
|
||||||
id: `${component.type}_${Date.now()}`,
|
id: `${component.type}_${Date.now()}`,
|
||||||
type: component.type,
|
type: component.type,
|
||||||
position: { x: 100, y: 100 }, // Default position
|
position: nodePosition,
|
||||||
label: component.label,
|
label: component.label,
|
||||||
data: {
|
data: {
|
||||||
...component.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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1390,78 +1402,51 @@ const onAddComponent = async (component) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the node to the process store
|
// 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
|
// Wait for store update and next render cycle
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
// CRITICAL FIX: Instead of calling syncCanvas (which can cause edge removal/re-addition),
|
// Add the node directly to the canvas using Vue Flow's addNode method
|
||||||
// we'll add the node directly to the canvas and preserve existing edges
|
|
||||||
if (processFlowCanvas.value) {
|
if (processFlowCanvas.value) {
|
||||||
try {
|
try {
|
||||||
// Get the fresh node from store (with any store-side modifications)
|
console.log('🎨 Adding node to canvas:', addedNode.id);
|
||||||
const freshNode = processStore.currentProcess?.nodes.find(n => n.id === newNode.id);
|
|
||||||
|
|
||||||
if (freshNode && processFlowCanvas.value.addNode) {
|
// Use Vue Flow's addNode method directly to avoid any sync issues
|
||||||
// Add only the new node to the canvas directly
|
if (processFlowCanvas.value.addNode) {
|
||||||
processFlowCanvas.value.addNode(freshNode);
|
processFlowCanvas.value.addNode(addedNode);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
console.error('❌ Error adding node to canvas:', error);
|
console.error('❌ Error adding node to canvas:', error);
|
||||||
|
toast.error('Failed to add component to canvas. Please try again.');
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding component:', error);
|
console.error('❌ Error adding component:', error);
|
||||||
toast.error('Failed to add component. Please try again.');
|
toast.error('Failed to add component. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
// Reset the flag after a longer delay to ensure canvas is stable
|
// Reset the flag after a longer delay to ensure canvas is stable
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('🏁 Component addition completed, resetting flag');
|
||||||
isAddingComponent.value = false;
|
isAddingComponent.value = false;
|
||||||
}, 200);
|
}, 300);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1504,6 +1489,9 @@ const applyProcessTemplate = async (template) => {
|
|||||||
|
|
||||||
// Process nodes first and wait for them to be fully added
|
// Process nodes first and wait for them to be fully added
|
||||||
for (const node of templateNodes) {
|
for (const node of templateNodes) {
|
||||||
|
// Get default styling for the node type
|
||||||
|
const defaultStyling = getDefaultNodeStyling(node.type);
|
||||||
|
|
||||||
const newNode = {
|
const newNode = {
|
||||||
...node,
|
...node,
|
||||||
id: node.id, // Keep original ID for edge references
|
id: node.id, // Keep original ID for edge references
|
||||||
@ -1511,7 +1499,12 @@ const applyProcessTemplate = async (template) => {
|
|||||||
position: node.position || { x: 100, y: 100 },
|
position: node.position || { x: 100, y: 100 },
|
||||||
data: {
|
data: {
|
||||||
...node.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
|
// Fix references to functions
|
||||||
const onFormSelected = (formData) => {
|
const onFormSelected = (formData) => {
|
||||||
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
if (selectedNodeData.value && selectedNodeData.value.type === 'form') {
|
||||||
@ -1818,62 +1883,85 @@ watch(() => canvasNodes.value, (newNodes) => {
|
|||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
// Watch for process changes to restore Vue Flow state
|
// Track if we're in the middle of adding a component to prevent interference
|
||||||
watch(() => processStore.currentProcess, async (newProcess, oldProcess) => {
|
const isProcessLoading = ref(false);
|
||||||
if (!newProcess) return;
|
|
||||||
|
|
||||||
// Only restore when a different process is loaded (not on updates)
|
|
||||||
if (oldProcess && newProcess.id === oldProcess.id) return;
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
// Wait for the canvas to be ready
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
// Give the canvas component a moment to initialize
|
// Give the canvas component a moment to initialize
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.fromObject) {
|
try {
|
||||||
try {
|
if (processFlowCanvas.value && processFlowCanvas.value.fromObject) {
|
||||||
// Prepare the flow object for restoration
|
try {
|
||||||
const rawFlowObject = {
|
// Prepare the flow object for restoration
|
||||||
nodes: newProcess.nodes || [],
|
const rawFlowObject = {
|
||||||
edges: newProcess.edges || [],
|
nodes: newProcess.nodes || [],
|
||||||
viewport: newProcess.viewport || { x: 0, y: 0, zoom: 1 }
|
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);
|
// 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);
|
// Use Vue Flow's proper restoration method (now returns a Promise)
|
||||||
|
await processFlowCanvas.value.fromObject(flowObject);
|
||||||
|
|
||||||
|
console.log('✅ Vue Flow state restored successfully');
|
||||||
// Fit view after restoration with a small delay to ensure everything is rendered
|
|
||||||
setTimeout(() => {
|
// Only fit view if nodes don't have meaningful positions (all at 0,0)
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
// This prevents repositioning nodes that were carefully positioned
|
||||||
processFlowCanvas.value.fitView();
|
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) {
|
// Fallback sync method
|
||||||
console.error('❌ Error restoring Vue Flow state:', error);
|
|
||||||
|
|
||||||
// Fallback to manual sync if Vue Flow restoration fails
|
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||||
|
|
||||||
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} finally {
|
||||||
console.warn('⚠️ Vue Flow canvas not available for restoration, using sync fallback');
|
isProcessLoading.value = false;
|
||||||
|
|
||||||
// Fallback sync method
|
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
|
||||||
processFlowCanvas.value.syncCanvas(newProcess.nodes, newProcess.edges);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 200); // Allow time for canvas to initialize
|
}, 200); // Allow time for canvas to initialize
|
||||||
}, { immediate: false });
|
}, { immediate: false });
|
||||||
|
@ -11,7 +11,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
history: [],
|
history: [],
|
||||||
historyIndex: -1,
|
historyIndex: -1,
|
||||||
unsavedChanges: false,
|
unsavedChanges: false,
|
||||||
lastChangeDescription: ''
|
lastChangeDescription: '',
|
||||||
|
isModifyingNodes: false // Flag to prevent interference during node operations
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
@ -653,37 +654,48 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
return existingNode;
|
return existingNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new node with proper data structure
|
// Set flag to prevent interference during node addition
|
||||||
const newNode = {
|
this.isModifyingNodes = true;
|
||||||
id: node.id || uuidv4(),
|
|
||||||
type: node.type,
|
|
||||||
label: node.label || node.data?.label || 'New Node',
|
|
||||||
position: node.position || { x: 100, y: 100 },
|
|
||||||
data: {
|
|
||||||
...node.data,
|
|
||||||
label: node.data?.label || node.label || 'New Node',
|
|
||||||
// Ensure shape is set for new nodes
|
|
||||||
shape: node.data?.shape || (node.type === 'gateway' ? 'diamond' : 'rectangle'),
|
|
||||||
// Ensure default colors are set for new nodes
|
|
||||||
backgroundColor: node.data?.backgroundColor || '#ffffff',
|
|
||||||
borderColor: node.data?.borderColor || '#000000'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a deep copy to avoid reference issues
|
try {
|
||||||
const nodeCopy = JSON.parse(JSON.stringify(newNode));
|
// 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
|
// Create a deep copy to avoid reference issues
|
||||||
this.currentProcess.nodes = [...this.currentProcess.nodes, nodeCopy];
|
const nodeCopy = JSON.parse(JSON.stringify(newNode));
|
||||||
|
|
||||||
// Update selection
|
|
||||||
this.selectedNodeId = nodeCopy.id;
|
|
||||||
|
|
||||||
// Save to history
|
|
||||||
this.saveToHistory('Add node');
|
|
||||||
this.unsavedChanges = true;
|
|
||||||
|
|
||||||
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) {
|
deleteNode(nodeId) {
|
||||||
if (!this.currentProcess) return;
|
if (!this.currentProcess) return;
|
||||||
|
|
||||||
// Find the node index
|
// Set flag to prevent interference during node deletion
|
||||||
const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId);
|
this.isModifyingNodes = true;
|
||||||
if (index !== -1) {
|
|
||||||
// Remove the node
|
try {
|
||||||
this.currentProcess.nodes.splice(index, 1);
|
// Find the node index
|
||||||
|
const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId);
|
||||||
// Remove any edges connected to this node
|
if (index !== -1) {
|
||||||
const edgesToRemove = this.currentProcess.edges.filter(
|
// Remove the node using splice to avoid creating new array reference
|
||||||
edge => edge.source === nodeId || edge.target === nodeId
|
this.currentProcess.nodes.splice(index, 1);
|
||||||
);
|
|
||||||
|
// Remove any edges connected to this node
|
||||||
edgesToRemove.forEach(edge => {
|
const edgesToRemove = this.currentProcess.edges.filter(
|
||||||
const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id);
|
edge => edge.source === nodeId || edge.target === nodeId
|
||||||
if (edgeIndex !== -1) {
|
);
|
||||||
this.currentProcess.edges.splice(edgeIndex, 1);
|
|
||||||
|
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
|
this.saveToHistory('Delete node');
|
||||||
if (this.selectedNodeId === nodeId) {
|
this.unsavedChanges = true;
|
||||||
this.selectedNodeId = null;
|
|
||||||
|
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) {
|
addEdge(edge) {
|
||||||
if (!this.currentProcess) return;
|
if (!this.currentProcess) return;
|
||||||
|
|
||||||
const newEdge = {
|
// Set flag to prevent interference during edge addition
|
||||||
id: edge.id || `${edge.source}-${edge.target}`,
|
this.isModifyingNodes = true;
|
||||||
source: edge.source,
|
|
||||||
target: edge.target,
|
|
||||||
label: edge.label || '',
|
|
||||||
type: edge.type || 'default',
|
|
||||||
animated: edge.animated !== undefined ? edge.animated : true,
|
|
||||||
data: edge.data || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.currentProcess.edges.push(newEdge);
|
try {
|
||||||
this.selectedEdgeId = newEdge.id;
|
const newEdge = {
|
||||||
this.saveToHistory('Add edge');
|
id: edge.id || `${edge.source}-${edge.target}`,
|
||||||
this.unsavedChanges = true;
|
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