diff --git a/components/process-flow/ProcessFlowCanvas.vue b/components/process-flow/ProcessFlowCanvas.vue
index 8508944..8060c57 100644
--- a/components/process-flow/ProcessFlowCanvas.vue
+++ b/components/process-flow/ProcessFlowCanvas.vue
@@ -13,7 +13,7 @@ import { VueFlow, useVueFlow, Panel } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import { MiniMap } from "@vue-flow/minimap";
-import { nodeTypes as customNodeTypes, nodeStyles } from "./ProcessFlowNodes";
+import { nodeTypes as customNodeTypes, nodeStyles } from "~/composables/processFlowNodes";
import InteractiveArrowEdge from "./InteractiveArrowEdge.vue";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
@@ -95,7 +95,7 @@ const {
// Define custom edge types - use markRaw to prevent reactivity issues
const edgeTypes = {
- custom: markRaw(InteractiveArrowEdge),
+ custom: shallowRef(InteractiveArrowEdge),
};
// Default nodes if empty
@@ -823,6 +823,29 @@ defineExpose({
// Add Vue Flow save/restore methods
toObject,
fromObject,
+ // Add direct access to Vue Flow methods for production fallbacks
+ setNodes: (newNodes) => {
+ try {
+ if (Array.isArray(newNodes)) {
+ nodes.value = newNodes;
+ }
+ } catch (error) {
+ console.error('Error in setNodes:', error);
+ }
+ },
+ setEdges: (newEdges) => {
+ try {
+ if (Array.isArray(newEdges)) {
+ edges.value = newEdges;
+ }
+ } catch (error) {
+ console.error('Error in setEdges:', error);
+ }
+ },
+ // Provide access to the flow instance
+ get flowInstance() {
+ return flowInstance;
+ }
});
// Update an existing node
diff --git a/composables/processFlowNodes.js b/composables/processFlowNodes.js
new file mode 100644
index 0000000..393b255
--- /dev/null
+++ b/composables/processFlowNodes.js
@@ -0,0 +1,1814 @@
+import { h, markRaw } from 'vue';
+import { Handle, Position } from '@vue-flow/core';
+
+// Custom node renderer
+const CustomNode = markRaw({
+ setup() {
+ return {
+ Position
+ };
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ nodeLabel }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ nodeLabel }}
+
+
+
+
+
+
+
+ `,
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // First try the label prop, then try data.label, then provide a default
+ return this.label || (this.data && this.data.label) || this.type || 'Node';
+ },
+ showBadge() {
+ return this.$slots.badge;
+ },
+ shapeClass() {
+ // Don't apply shape classes to start and end nodes - they have fixed shapes
+ if (this.type === 'start' || this.type === 'end') {
+ return '';
+ }
+
+ // Get shape from node data, default to rectangle
+ const shape = this.data?.shape || 'rectangle';
+ return `shape-${shape}`;
+ },
+ nodeStyle() {
+ // Apply custom colors from node data with proper defaults based on node type
+ let defaultBg = '#ffffff';
+ let defaultBorder = '#dddddd';
+ let defaultText = '#333333';
+
+ // Set type-specific defaults if no colors are set
+ if (this.type && !this.data?.backgroundColor) {
+ switch (this.type) {
+ case 'form':
+ defaultBg = '#faf5ff';
+ defaultBorder = '#9333ea';
+ defaultText = '#6b21a8';
+ break;
+ case 'api':
+ defaultBg = '#eff6ff';
+ defaultBorder = '#3b82f6';
+ defaultText = '#1e40af';
+ break;
+ case 'gateway':
+ defaultBg = '#fff7ed';
+ defaultBorder = '#f97316';
+ defaultText = '#c2410c';
+ break;
+ case 'script':
+ defaultBg = '#f9fafb';
+ defaultBorder = '#6b7280';
+ defaultText = '#374151';
+ break;
+ case 'business-rule':
+ defaultBg = '#fdf4ff';
+ defaultBorder = '#a855f7';
+ defaultText = '#7c3aed';
+ break;
+ case 'notification':
+ defaultBg = '#f0f9ff';
+ defaultBorder = '#0ea5e9';
+ defaultText = '#0284c7';
+ break;
+ case 'subprocess':
+ defaultBg = '#f0fdfa';
+ defaultBorder = '#14b8a6';
+ defaultText = '#134e4a';
+ break;
+ }
+ }
+
+ const backgroundColor = this.data?.backgroundColor || defaultBg;
+ const borderColor = this.data?.borderColor || defaultBorder;
+ const textColor = this.data?.textColor || defaultText;
+
+ // Get the current shape to determine if we should apply direct styles
+ const shape = this.data?.shape || 'rectangle';
+ const isShapedNode = shape !== 'rectangle' && shape !== 'rounded-rectangle' && shape !== 'circle';
+
+ const style = {
+ '--node-bg-color': backgroundColor,
+ '--node-border-color': borderColor,
+ '--node-text-color': textColor,
+ color: textColor
+ };
+
+ // Only apply direct background/border styles for non-shaped nodes
+ // Shaped nodes (hexagon, trapezoid) use CSS ::before pseudo-elements with clip-path
+ if (!isShapedNode) {
+ style.backgroundColor = backgroundColor;
+ style.borderColor = borderColor;
+ }
+
+ return style;
+ }
+ },
+ methods: {
+ onClick() {
+ this.$emit('node-click', this.id);
+ }
+ },
+ components: {
+ Handle
+ }
+});
+
+// Start node
+export const StartNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ setup() {
+ return {
+ Position
+ };
+ },
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'Start';
+ }
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+ play_arrow
+
+
{{ nodeLabel }}
+
+
+ `,
+ components: {
+ Handle
+ }
+});
+
+// End node
+export const EndNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ setup() {
+ return {
+ Position
+ };
+ },
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'End';
+ }
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+ stop
+
+
{{ nodeLabel }}
+
+
+ `,
+ components: {
+ Handle
+ }
+});
+
+// Decision/Gateway node
+export const GatewayNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ return this.label || (this.data && this.data.label) || 'Decision Point';
+ },
+
+ totalPaths() {
+ return Array.isArray(this.data?.conditions) ? this.data.conditions.length : 0;
+ },
+
+ totalConditions() {
+ if (!Array.isArray(this.data?.conditions)) return 0;
+
+ return this.data.conditions.reduce((total, group) => {
+ return total + (Array.isArray(group.conditions) ? group.conditions.length : 0);
+ }, 0);
+ },
+
+ conditionSummary() {
+ if (this.totalPaths === 0) return 'No paths';
+
+ const paths = this.data.conditions
+ .map(group => group.output || 'Unlabeled')
+ .filter(Boolean)
+ .join(', ');
+
+ return paths || 'Unconfigured paths';
+ },
+
+ defaultPath() {
+ return this.data?.defaultPath || 'Default';
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'gateway',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-orange-500' }, 'call_split'),
+ default: () => h('div', { class: 'gateway-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Decision based on conditions'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Paths:'),
+ h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' },
+ this.totalPaths === 0 ? 'None' : this.totalPaths
+ )
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Default:'),
+ h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' },
+ this.defaultPath
+ )
+ ])
+ ])
+ });
+ }
+});
+
+// Form node
+export const FormNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'Form Task';
+ },
+ formName() {
+ return this.data?.formName || 'None selected';
+ },
+ hasForm() {
+ return !!(this.data?.formId && this.data?.formName);
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'form',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-purple-500' }, 'description'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Form submission task'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Form:'),
+ h('span', {
+ class: this.hasForm ? 'node-rule-detail-value ml-1 font-medium text-purple-600' : 'node-rule-detail-value ml-1 italic text-gray-400'
+ }, this.formName)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Status:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-purple-600'
+ }, this.hasForm ? 'Configured' : 'Not configured')
+ ])
+ ])
+ });
+ }
+});
+
+// Script node
+export const ScriptNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'Script';
+ },
+ scriptLanguage() {
+ return this.data?.language || 'Not specified';
+ },
+ hasScript() {
+ return !!this.data?.script;
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'script',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-gray-500' }, 'code'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Script execution'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Language:'),
+ h('span', { class: 'node-rule-detail-value ml-1 font-medium text-gray-600' }, this.scriptLanguage)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Script:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-gray-600'
+ }, this.hasScript ? 'Defined' : 'Not defined')
+ ])
+ ])
+ });
+ }
+ });
+
+// HTML node
+export const HtmlNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'HTML Content';
+ },
+ hasHtmlContent() {
+ return !!this.data?.htmlCode;
+ },
+ hasCssContent() {
+ return !!this.data?.cssCode;
+ },
+ hasJsContent() {
+ return !!this.data?.jsCode;
+ },
+ contentSummary() {
+ const parts = [];
+ if (this.hasHtmlContent) parts.push('HTML');
+ if (this.hasCssContent) parts.push('CSS');
+ if (this.hasJsContent) parts.push('JS');
+
+ return parts.length > 0 ? parts.join(' + ') : 'Empty';
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'html',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-blue-500' }, 'code'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Custom HTML content'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Content:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
+ }, 'HTML')
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Status:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-gray-600'
+ }, this.hasHtmlContent ? 'Configured' : 'Not configured')
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Variables:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
+ }, this.data?.allowVariableAccess ? 'Enabled' : 'Disabled')
+ ])
+ ])
+ });
+ }
+});
+
+// API Call node
+export const ApiCallNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ return this.label || (this.data && this.data.label) || 'API Call';
+ },
+ apiUrl() {
+ return this.data?.apiUrl || 'No URL specified';
+ },
+ apiMethod() {
+ return this.data?.apiMethod || 'GET';
+ },
+ isConfigured() {
+ return !!this.data?.apiUrl;
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'api',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-indigo-500' }, 'api'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'External API call'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Method:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-indigo-600'
+ }, this.apiMethod)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'URL:'),
+ h('span', {
+ class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-indigo-600' : 'node-rule-detail-value ml-1 italic text-gray-400',
+ style: 'max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;'
+ }, this.apiUrl)
+ ])
+ ])
+ });
+ }
+});
+
+// Business Rule node
+export const BusinessRuleNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'Business Rule';
+ },
+
+ ruleConditionSummary() {
+ // First try to use the new ruleGroups structure
+ if (this.data && this.data.ruleGroups && Array.isArray(this.data.ruleGroups)) {
+ // Count total conditions across all rule groups
+ const totalConditions = this.data.ruleGroups.reduce((count, group) => {
+ return count + (Array.isArray(group.conditions) ? group.conditions.length : 0);
+ }, 0);
+
+ return totalConditions === 0 ? 'No conditions' :
+ totalConditions === 1 ? '1 condition' :
+ `${totalConditions} conditions`;
+ }
+
+ // Fallback to old structure for backward compatibility
+ if (this.data && this.data.conditions && Array.isArray(this.data.conditions)) {
+ const count = this.data.conditions.length;
+ return count === 1 ? '1 condition' : `${count} conditions`;
+ }
+
+ return 'No conditions defined';
+ },
+
+ ruleActionSummary() {
+ // First try to use the new ruleGroups structure
+ if (this.data && this.data.ruleGroups && Array.isArray(this.data.ruleGroups)) {
+ // Count total actions across all rule groups
+ const totalActions = this.data.ruleGroups.reduce((count, group) => {
+ return count + (Array.isArray(group.actions) ? group.actions.length : 0);
+ }, 0);
+
+ return totalActions === 0 ? 'No actions' :
+ totalActions === 1 ? '1 action' :
+ `${totalActions} actions`;
+ }
+
+ // Fallback to old structure for backward compatibility
+ if (this.data && this.data.actions && Array.isArray(this.data.actions)) {
+ const count = this.data.actions.length;
+ return count === 1 ? '1 action' : `${count} actions`;
+ }
+
+ return 'No actions defined';
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'business-rule',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-purple-600' }, 'rule'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Applies business rules to process data'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Conditions:'),
+ h('span', { class: 'node-rule-detail-value ml-1 font-medium text-purple-600' }, this.ruleConditionSummary)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Actions:'),
+ h('span', { class: 'node-rule-detail-value ml-1 font-medium text-purple-600' }, this.ruleActionSummary)
+ ])
+ ])
+ });
+ }
+});
+
+// Notification node
+export const NotificationNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ // Get label from either prop or data, with fallback
+ return this.label || (this.data && this.data.label) || 'Notification';
+ },
+ notificationType() {
+ return this.data?.notificationType || 'info';
+ },
+ notificationTypeIcon() {
+ const types = {
+ info: 'material-symbols:info-outline',
+ success: 'material-symbols:check-circle-outline',
+ warning: 'material-symbols:warning-outline',
+ error: 'material-symbols:error-outline'
+ };
+ return types[this.notificationType] || types.info;
+ },
+ notificationTypeColor() {
+ const colors = {
+ info: 'text-blue-500',
+ success: 'text-green-500',
+ warning: 'text-yellow-500',
+ error: 'text-red-500'
+ };
+ return colors[this.notificationType] || colors.info;
+ },
+ recipientType() {
+ return this.data?.recipientType || 'user';
+ },
+ recipientLabel() {
+ const types = {
+ user: 'User',
+ role: 'Role',
+ variable: 'Variable',
+ email: 'Email'
+ };
+ return types[this.recipientType] || 'User';
+ },
+ isConfigured() {
+ // Check if notification has required fields
+ return !!(this.data?.subject && this.data?.message);
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'notification',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: `material-icons ${this.notificationTypeColor}` }, 'notifications'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Send notification'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Type:'),
+ h('span', {
+ class: `node-rule-detail-value ml-1 font-medium ${this.notificationTypeColor}`
+ }, this.notificationType.charAt(0).toUpperCase() + this.notificationType.slice(1))
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Recipient:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
+ }, this.recipientLabel)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Status:'),
+ h('span', {
+ class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-green-600' : 'node-rule-detail-value ml-1 font-medium text-red-600'
+ }, this.isConfigured ? 'Configured' : 'Not configured')
+ ])
+ ])
+ });
+ }
+});
+
+// Subprocess node
+export const SubprocessNode = markRaw({
+ props: ['id', 'type', 'label', 'selected', 'data'],
+ computed: {
+ nodeLabel() {
+ return this.label || (this.data && this.data.label) || 'Sub Process';
+ },
+ subprocessName() {
+ return this.data?.subprocessName || 'None selected';
+ },
+ isConfigured() {
+ return !!this.data?.subprocessId;
+ }
+ },
+ render() {
+ return h(CustomNode, {
+ id: this.id,
+ type: 'subprocess',
+ label: this.nodeLabel,
+ selected: this.selected,
+ data: this.data,
+ onClick: () => this.$emit('node-click', this.id)
+ }, {
+ icon: () => h('i', { class: 'material-icons text-teal-500' }, 'hub'),
+ default: () => h('div', { class: 'node-details' }, [
+ h('p', { class: 'node-description' }, this.data?.description || 'Executes another process'),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Process:'),
+ h('span', {
+ class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-teal-600' : 'node-rule-detail-value ml-1 italic text-gray-400'
+ }, this.subprocessName)
+ ]),
+ h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
+ h('span', { class: 'node-rule-detail-label' }, 'Status:'),
+ h('span', {
+ class: 'node-rule-detail-value ml-1 font-medium',
+ 'class': this.isConfigured ? 'text-green-600' : 'text-red-600'
+ }, this.isConfigured ? 'Configured' : 'Not configured')
+ ])
+ ])
+ });
+ }
+});
+
+// Shape Components (Design Elements)
+const HorizontalSwimlaneShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 600}px`,
+ height: `${this.data?.height || 150}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#f8fafc',
+ '--node-border-color': this.data?.borderColor || '#e2e8f0',
+ '--node-text-color': this.data?.textColor || '#475569',
+ backgroundColor: this.data?.backgroundColor || '#f8fafc',
+ border: `2px solid ${this.data?.borderColor || '#e2e8f0'}`,
+ borderRadius: '8px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: this.data?.textColor || '#475569',
+ fontSize: '14px',
+ fontWeight: '500',
+ cursor: 'move',
+ zIndex: -10 // Behind process nodes
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+const VerticalSwimlaneShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 200}px`,
+ height: `${this.data?.height || 400}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#f8fafc',
+ '--node-border-color': this.data?.borderColor || '#e2e8f0',
+ '--node-text-color': this.data?.textColor || '#475569',
+ backgroundColor: this.data?.backgroundColor || '#f8fafc',
+ border: `2px solid ${this.data?.borderColor || '#e2e8f0'}`,
+ borderRadius: '8px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: this.data?.textColor || '#475569',
+ fontSize: '14px',
+ fontWeight: '500',
+ cursor: 'move',
+ zIndex: -10, // Behind process nodes
+ writingMode: 'vertical-rl',
+ textOrientation: 'mixed'
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+const RectangleShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 300}px`,
+ height: `${this.data?.height || 200}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#fefefe',
+ '--node-border-color': this.data?.borderColor || '#d1d5db',
+ '--node-text-color': this.data?.textColor || '#374151',
+ backgroundColor: this.data?.backgroundColor || '#fefefe',
+ border: `2px solid ${this.data?.borderColor || '#d1d5db'}`,
+ borderRadius: '4px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: this.data?.textColor || '#374151',
+ fontSize: '14px',
+ fontWeight: '500',
+ cursor: 'move',
+ zIndex: -10 // Behind process nodes
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+const TextAnnotationShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 200}px`,
+ height: `${this.data?.height || 80}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#fffbeb',
+ '--node-border-color': this.data?.borderColor || '#fbbf24',
+ '--node-text-color': this.data?.textColor || '#92400e',
+ backgroundColor: this.data?.backgroundColor || '#fffbeb',
+ border: `2px dashed ${this.data?.borderColor || '#fbbf24'}`,
+ borderRadius: '4px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: this.data?.textColor || '#92400e',
+ fontSize: '12px',
+ fontWeight: '400',
+ cursor: 'move',
+ zIndex: -10, // Behind process nodes
+ fontStyle: 'italic'
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+const ProcessGroupShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 400}px`,
+ height: `${this.data?.height || 300}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#f0f9ff',
+ '--node-border-color': this.data?.borderColor || '#0284c7',
+ '--node-text-color': this.data?.textColor || '#0369a1',
+ backgroundColor: this.data?.backgroundColor || '#f0f9ff',
+ border: `3px solid ${this.data?.borderColor || '#0284c7'}`,
+ borderRadius: '12px',
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ color: this.data?.textColor || '#0369a1',
+ fontSize: '16px',
+ fontWeight: '600',
+ cursor: 'move',
+ zIndex: -10, // Behind process nodes
+ padding: '16px'
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+ });
+
+// Hexagon Shape Component
+const HexagonShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 200}px`,
+ height: `${this.data?.height || 150}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#f8fafc',
+ '--node-border-color': this.data?.borderColor || '#e2e8f0',
+ '--node-text-color': this.data?.textColor || '#475569',
+ position: 'relative',
+ cursor: 'move',
+ zIndex: -10 // Behind process nodes
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+// Trapezoid Shape Component
+const TrapezoidShape = markRaw({
+ props: ['id', 'data', 'selected', 'label'],
+ computed: {
+ shapeStyle() {
+ return {
+ width: `${this.data?.width || 220}px`,
+ height: `${this.data?.height || 120}px`,
+ '--node-bg-color': this.data?.backgroundColor || '#f8fafc',
+ '--node-border-color': this.data?.borderColor || '#e2e8f0',
+ '--node-text-color': this.data?.textColor || '#475569',
+ position: 'relative',
+ cursor: 'move',
+ zIndex: -10 // Behind process nodes
+ };
+ },
+ displayLabel() {
+ return this.label || this.data?.label || '';
+ },
+ displayDescription() {
+ return this.data?.description || '';
+ }
+ },
+ template: `
+
+
+
{{ displayLabel }}
+
{{ displayDescription }}
+
+
+ `
+});
+
+// Export the node types object to use with Vue Flow
+export const nodeTypes = {
+ start: StartNode,
+ end: EndNode,
+ gateway: GatewayNode,
+ form: FormNode,
+ script: ScriptNode,
+ 'business-rule': BusinessRuleNode,
+ api: ApiCallNode,
+ notification: NotificationNode,
+ html: HtmlNode, // Add the new HtmlNode to the nodeTypes object
+ subprocess: SubprocessNode,
+ // Shape nodes
+ 'swimlane-horizontal': HorizontalSwimlaneShape,
+ 'swimlane-vertical': VerticalSwimlaneShape,
+ 'rectangle-shape': RectangleShape,
+ 'text-annotation': TextAnnotationShape,
+ 'process-group': ProcessGroupShape,
+ 'hexagon-shape': HexagonShape,
+ 'trapezoid-shape': TrapezoidShape
+};
+
+// Default CSS for the nodes to be imported where needed
+export const nodeStyles = `
+.custom-node {
+ position: relative;
+ color: #333;
+ font-size: 12px;
+ transition: all 0.2s;
+ border: 1px solid transparent;
+}
+
+.custom-node.selected {
+ border-color: #ff6b6b;
+ box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
+}
+
+/* Handle positioning and styling */
+.handle-top {
+ top: -6px !important;
+ left: 50% !important;
+ transform: translateX(-50%) !important;
+ width: 12px !important;
+ height: 12px !important;
+ border-radius: 50% !important;
+ background: #fff !important;
+ border: 2px solid #666 !important;
+ opacity: 0;
+ transition: all 0.2s ease;
+ cursor: crosshair;
+ z-index: 100 !important;
+ position: absolute !important;
+}
+
+.handle-bottom {
+ bottom: -6px !important;
+ left: 50% !important;
+ transform: translateX(-50%) !important;
+ width: 12px !important;
+ height: 12px !important;
+ border-radius: 50% !important;
+ background: #fff !important;
+ border: 2px solid #666 !important;
+ opacity: 0;
+ transition: all 0.2s ease;
+ cursor: crosshair;
+ z-index: 100 !important;
+ position: absolute !important;
+}
+
+.handle-left {
+ left: -6px !important;
+ top: 50% !important;
+ transform: translateY(-50%) !important;
+ width: 12px !important;
+ height: 12px !important;
+ border-radius: 50% !important;
+ background: #fff !important;
+ border: 2px solid #666 !important;
+ opacity: 0;
+ transition: all 0.2s ease;
+ cursor: crosshair;
+ z-index: 100 !important;
+ position: absolute !important;
+}
+
+.handle-right {
+ right: -6px !important;
+ top: 50% !important;
+ transform: translateY(-50%) !important;
+ width: 12px !important;
+ height: 12px !important;
+ border-radius: 50% !important;
+ background: #fff !important;
+ border: 2px solid #666 !important;
+ opacity: 0;
+ transition: all 0.2s ease;
+ cursor: crosshair;
+ z-index: 100 !important;
+ position: absolute !important;
+}
+
+/* Show handles on hover and during connection */
+.custom-node:hover .handle-top,
+.custom-node:hover .handle-bottom,
+.custom-node:hover .handle-left,
+.custom-node:hover .handle-right,
+.vue-flow__node.connecting .handle-top,
+.vue-flow__node.connecting .handle-bottom,
+.vue-flow__node.connecting .handle-left,
+.vue-flow__node.connecting .handle-right {
+ opacity: 1;
+}
+
+/* Show all handles when any node is being connected */
+.vue-flow.connecting .handle-top,
+.vue-flow.connecting .handle-bottom,
+.vue-flow.connecting .handle-left,
+.vue-flow.connecting .handle-right {
+ opacity: 1 !important;
+}
+
+/* Active handle styles for connection mode */
+.vue-flow__handle.connecting,
+.vue-flow__handle.valid {
+ opacity: 1 !important;
+ transform: scale(1.3) !important;
+ border-width: 3px !important;
+ box-shadow: 0 0 10px rgba(37, 99, 235, 0.5);
+}
+
+/* Ensure handles are clickable and properly sized */
+.vue-flow__handle {
+ pointer-events: all !important;
+ min-width: 12px !important;
+ min-height: 12px !important;
+ position: absolute !important;
+}
+
+/* Force handle visibility during connection */
+.vue-flow.connecting .vue-flow__handle {
+ opacity: 1 !important;
+ pointer-events: all !important;
+ z-index: 1000 !important;
+}
+
+/* Connection line styles */
+.vue-flow__connection-line {
+ stroke: #2563eb;
+ stroke-width: 3;
+ stroke-dasharray: 5,5;
+ z-index: 1000;
+}
+
+/* Target handle highlighting during connection */
+.vue-flow__handle.target:hover,
+.vue-flow__handle.valid {
+ background: #2563eb !important;
+ border-color: #1d4ed8 !important;
+ transform: scale(1.3) !important;
+}
+
+/* Handle hover effects */
+.handle-top:hover,
+.handle-bottom:hover,
+.handle-left:hover,
+.handle-right:hover {
+ transform: scale(1.1);
+ border-width: 3px;
+}
+
+.handle-top:hover {
+ transform: translateX(-50%) scale(1.1);
+}
+
+.handle-bottom:hover {
+ transform: translateX(-50%) scale(1.1);
+}
+
+.handle-left:hover {
+ transform: translateY(-50%) scale(1.1);
+}
+
+.handle-right:hover {
+ transform: translateY(-50%) scale(1.1);
+}
+
+/* Source handles (output) */
+.handle-start-output,
+.handle-gateway-output,
+.handle-form-output,
+.handle-script-output,
+.handle-api-output,
+.handle-business-rule-output,
+.handle-notification-output {
+ border-color: #4CAF50;
+ background: #e8f5e9;
+}
+
+.handle-start-output:hover,
+.handle-gateway-output:hover,
+.handle-form-output:hover,
+.handle-script-output:hover,
+.handle-api-output:hover,
+.handle-business-rule-output:hover,
+.handle-notification-output:hover {
+ background: #4CAF50;
+ border-color: #2E7D32;
+}
+
+/* Target handles (input) */
+.handle-end-input,
+.handle-gateway-input,
+.handle-form-input,
+.handle-script-input,
+.handle-api-input,
+.handle-business-rule-input,
+.handle-notification-input {
+ border-color: #2196F3;
+ background: #e3f2fd;
+}
+
+.handle-end-input:hover,
+.handle-gateway-input:hover,
+.handle-form-input:hover,
+.handle-script-input:hover,
+.handle-api-input:hover,
+.handle-business-rule-input:hover,
+.handle-notification-input:hover {
+ background: #2196F3;
+ border-color: #1565C0;
+}
+
+/* Base styles for different node types */
+.node-form, .node-script, .node-api, .node-business-rule, .node-notification {
+ width: 180px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ border: 1px solid #e0e0e0;
+}
+
+.node-gateway {
+ width: 120px !important;
+ height: 120px !important;
+ background: white;
+ transform: rotate(45deg);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ border: 2px solid #f97316;
+ position: relative;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.node-start, .node-end {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.node-start {
+ background: #e8f5e9;
+ border: 1px solid #4CAF50;
+}
+
+.node-end {
+ background: #ffebee;
+ border: 1px solid #f44336;
+}
+
+/* Content positioning */
+.custom-node-content {
+ padding: 8px;
+ position: relative;
+ z-index: 2;
+}
+
+.node-gateway .custom-node-content {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ transform: rotate(-45deg);
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ text-align: center;
+ z-index: 10;
+}
+
+.node-start .custom-node-content,
+.node-end .custom-node-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding: 0;
+}
+
+.custom-node-icon {
+ margin-right: 6px;
+ display: inline-flex;
+ align-items: center;
+}
+
+.node-start .custom-node-icon,
+.node-end .custom-node-icon {
+ margin: 0;
+}
+
+.custom-node-icon .material-icons {
+ font-size: 16px;
+}
+
+.node-start .material-icons,
+.node-end .material-icons {
+ font-size: 14px;
+}
+
+.node-form .custom-node-title,
+.node-script .custom-node-title,
+.node-api .custom-node-title,
+.node-business-rule .custom-node-title,
+.node-notification .custom-node-title {
+ font-weight: 500;
+ font-size: 11px;
+ display: flex;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.node-end .custom-node-title {
+ position: absolute;
+ width: 60px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ bottom: -29px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 10px;
+ font-weight: 500;
+ text-align: center;
+}
+
+.node-start .custom-node-title {
+ position: absolute;
+ width: 60px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ bottom: 51px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 10px;
+ font-weight: 500;
+ text-align: center;
+}
+
+.node-gateway .custom-node-title {
+ font-size: 11px;
+ font-weight: 700;
+ color: #c2410c;
+ margin: 0 0 4px 0;
+ text-align: center;
+ width: 90%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.2;
+}
+
+.node-details {
+ margin-top: 4px;
+}
+
+.node-description {
+ margin-bottom: 2px;
+ color: #666;
+ white-space: normal;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ font-size: 10px;
+}
+
+.node-assignee,
+.node-form-info,
+.node-script-info,
+.node-api-info,
+.node-api-method-info,
+.node-conditions {
+ display: flex;
+ font-size: 10px;
+ color: #666;
+ align-items: center;
+}
+
+.node-assignee-label,
+.node-form-label,
+.node-script-label,
+.node-api-label,
+.node-api-method-label {
+ font-weight: 500;
+ margin-right: 4px;
+}
+
+.node-form-value,
+.node-script-value,
+.node-api-value,
+.node-api-method-value,
+.node-assignee-value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.node-conditions-value {
+ font-size: 11px;
+ color: #666;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
+ line-height: 1.2;
+}
+
+.node-form-id {
+ font-size: 9px;
+ color: #999;
+}
+
+.gateway-details {
+ width: 80%;
+ text-align: center;
+ margin-top: 2px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Position handles correctly for gateway node */
+.node-gateway .handle-top {
+ top: -6px !important;
+ left: 50% !important;
+ transform: translateX(-50%) rotate(-45deg) !important;
+ border-color: #f97316 !important;
+ background: #fff !important;
+}
+
+.node-gateway .handle-bottom {
+ bottom: -6px !important;
+ left: 50% !important;
+ transform: translateX(-50%) rotate(-45deg) !important;
+ border-color: #f97316 !important;
+ background: #fff !important;
+}
+
+.node-gateway .handle-left {
+ left: -6px !important;
+ top: 50% !important;
+ transform: translateY(-50%) rotate(-45deg) !important;
+ border-color: #f97316 !important;
+ background: #fff !important;
+}
+
+.node-gateway .handle-right {
+ right: -6px !important;
+ top: 50% !important;
+ transform: translateY(-50%) rotate(-45deg) !important;
+ border-color: #f97316 !important;
+ background: #fff !important;
+}
+
+.node-gateway:hover .handle-top,
+.node-gateway:hover .handle-bottom,
+.node-gateway:hover .handle-left,
+.node-gateway:hover .handle-right {
+ opacity: 1 !important;
+}
+
+.node-gateway .handle-top:hover {
+ transform: translateX(-50%) rotate(-45deg) scale(1.1) !important;
+ background: #f97316 !important;
+}
+
+.node-gateway .handle-bottom:hover {
+ transform: translateX(-50%) rotate(-45deg) scale(1.1) !important;
+ background: #f97316 !important;
+}
+
+.node-gateway .handle-left:hover {
+ transform: translateY(-50%) rotate(-45deg) scale(1.1) !important;
+ background: #f97316 !important;
+}
+
+.node-gateway .handle-right:hover {
+ transform: translateY(-50%) rotate(-45deg) scale(1.1) !important;
+ background: #f97316 !important;
+}
+
+/* Badge style */
+.node-badge {
+ font-size: 9px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ margin-left: 4px;
+}
+
+/* Gateway specific styles */
+.node-gateway:hover {
+ border-color: #ea580c;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.node-gateway .node-description {
+ text-align: center;
+ margin-bottom: 2px;
+ font-size: 8px;
+ line-height: 1.1;
+ overflow: hidden;
+ max-width: 80px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ max-width: 90px;
+}
+
+.node-gateway .node-rule-detail {
+ display: flex;
+ font-size: 8px;
+ color: #666;
+ align-items: center;
+ justify-content: center;
+ width: 80%;
+ margin-bottom: 2px;
+}
+
+.node-gateway .node-rule-detail-label {
+ font-weight: 500;
+ margin-right: 4px;
+}
+
+.node-gateway .node-rule-detail-value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 8px;
+ max-width: 50px;
+ text-align: center;
+ font-weight: 600;
+ color: #c2410c;
+}
+
+.node-gateway .material-icons {
+ font-size: 18px;
+ color: #f97316;
+ margin-bottom: 4px;
+}
+
+/* Update node-specific styles to be more consistent */
+.node-form {
+ width: 180px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ border: 1px solid #ddd;
+ border-left: 4px solid #9333ea; /* Purple border to match icon color */
+}
+
+.node-api {
+ width: 180px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ border: 1px solid #ddd;
+ border-left: 4px solid #6366f1; /* Indigo border to match icon color */
+}
+
+/* Node details styles for consistency */
+.node-rule-detail {
+ display: flex;
+ font-size: 10px;
+ color: #666;
+ align-items: center;
+ margin-bottom: 2px;
+}
+
+.node-rule-detail-label {
+ font-weight: 500;
+ margin-right: 4px;
+}
+
+.node-rule-detail-value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Add task node specific styling to be consistent with business rule */
+.node-task {
+ width: 180px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ border: 1px solid #ddd;
+ border-left: 4px solid #3b82f6; /* Blue border to match icon color */
+}
+
+/* Script node styling */
+.node-script {
+ width: 180px;
+ background: white;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ border: 1px solid #ddd;
+ border-left: 4px solid #6b7280; /* Gray border to match icon color */
+}
+
+/* HTML node styling */
+.node-html {
+ border-left: 4px solid #0ea5e9; /* Blue border to match icon color */
+ background-color: #f0f9ff;
+ border-radius: 4px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+/* Shape node styles */
+.shape-node {
+ position: relative;
+ border: 2px solid #e5e7eb;
+ border-radius: 8px;
+ background: white;
+ cursor: move;
+ transition: all 0.2s ease;
+ /* z-index is now controlled dynamically through node data */
+}
+
+.shape-node.selected {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ /* z-index is now controlled dynamically through node data */
+}
+
+.shape-node:hover {
+ border-color: #94a3b8;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.shape-content {
+ position: relative;
+ z-index: 1;
+ text-align: center;
+ overflow: hidden;
+}
+
+.shape-label {
+ font-weight: 500;
+ margin-bottom: 4px;
+ user-select: none;
+ word-wrap: break-word;
+}
+
+.shape-description {
+ font-size: 12px;
+ opacity: 0.7;
+ user-select: none;
+ word-wrap: break-word;
+}
+
+/* Specific shape styles */
+.swimlane-horizontal {
+ border-style: solid;
+ border-width: 2px 0;
+ border-radius: 0;
+}
+
+.swimlane-vertical {
+ border-style: solid;
+ border-width: 0 2px;
+ border-radius: 0;
+}
+
+.text-annotation {
+ border-style: dashed;
+ background: rgba(255, 251, 235, 0.8);
+ font-style: italic;
+}
+
+.process-group {
+ border-width: 3px;
+ border-style: solid;
+ border-radius: 12px;
+}
+
+.rectangle-shape {
+ border-radius: 4px;
+}
+
+/* Ensure shapes don't interfere with node connections */
+.shape-node * {
+ pointer-events: none;
+}
+
+.shape-node {
+ pointer-events: all;
+}
+
+/* Resize handles for shapes when selected */
+.shape-node.selected::after {
+ content: '';
+ position: absolute;
+ bottom: -4px;
+ right: -4px;
+ width: 8px;
+ height: 8px;
+ background: #3b82f6;
+ border: 2px solid white;
+ border-radius: 50%;
+ cursor: nw-resize;
+ pointer-events: all;
+}
+`;
\ No newline at end of file
diff --git a/docs/vue-flow-custom-nodes-migration.md b/docs/vue-flow-custom-nodes-migration.md
new file mode 100644
index 0000000..46c0cf9
--- /dev/null
+++ b/docs/vue-flow-custom-nodes-migration.md
@@ -0,0 +1,304 @@
+# Vue Flow Custom Nodes Migration Guide
+
+## ๐ฏ Problem Solved
+Custom nodes defined inline (using template strings or object definitions) **do not load in production** with Vue Flow. The solution is to use **separate .vue component files** that are properly imported.
+
+## โ
Production-Safe Approach: File-Based Custom Nodes
+
+### 1. Create Separate .vue Files for Each Node Type
+
+Instead of defining nodes inline in `composables/processFlowNodes.js`, create individual `.vue` files:
+
+```
+components/process-flow/custom/
+โโโ StartNode.vue
+โโโ FormNode.vue
+โโโ EndNode.vue
+โโโ ConditionalNode.vue
+โโโ ScriptNode.vue
+โโโ ApiNode.vue
+โโโ NotificationNode.vue
+โโโ ... (other node types)
+```
+
+### 2. Standard Vue Component Structure
+
+Each node component should follow this pattern:
+
+```vue
+
+
+
+
+
+
+
+
+
+
๐
+
{{ data?.label || label || 'Default Label' }}
+
+
+
+
+
+
+
+
+```
+
+### 3. Import Components in Your Main File
+
+In `pages/process-builder/index.vue`:
+
+```javascript
+// Import all custom node components
+import StartNode from "~/components/process-flow/custom/StartNode.vue"
+import FormNode from "~/components/process-flow/custom/FormNode.vue"
+import EndNode from "~/components/process-flow/custom/EndNode.vue"
+import ConditionalNode from "~/components/process-flow/custom/ConditionalNode.vue"
+import ScriptNode from "~/components/process-flow/custom/ScriptNode.vue"
+import ApiNode from "~/components/process-flow/custom/ApiNode.vue"
+import NotificationNode from "~/components/process-flow/custom/NotificationNode.vue"
+// ... import other nodes
+```
+
+### 4. Create nodeTypes Object
+
+```javascript
+import { markRaw } from 'vue'
+
+// Map node types to components (use markRaw to prevent reactivity issues)
+const nodeTypes = {
+ 'start': markRaw(StartNode),
+ 'form': markRaw(FormNode),
+ 'end': markRaw(EndNode),
+ 'conditional': markRaw(ConditionalNode),
+ 'script': markRaw(ScriptNode),
+ 'api': markRaw(ApiNode),
+ 'notification': markRaw(NotificationNode),
+ // ... other node types
+}
+```
+
+### 5. Use nodeTypes in VueFlow Component
+
+```vue
+
+
+
+
+
+
+```
+
+## ๐ซ What NOT to Use (Breaks in Production)
+
+### โ Template Slots Approach
+```vue
+
+
+
+ {{ label }}
+
+
+```
+
+### โ Inline Object Definitions
+```javascript
+// This breaks in production
+const nodeTypes = {
+ 'start': {
+ props: ['id', 'data'],
+ template: `{{ data.label }}
`
+ }
+}
+```
+
+## ๐ Migration Checklist
+
+### Current State Analysis
+- [ ] Review `composables/processFlowNodes.js` for all node type definitions
+- [ ] Identify unique node types used in your process builder
+- [ ] Note any complex computed properties or methods in existing nodes
+
+### Migration Steps
+1. [ ] Create individual `.vue` files for each node type in `components/process-flow/custom/`
+2. [ ] Convert existing node logic from `processFlowNodes.js` to Vue component format
+3. [ ] Import all node components in main process builder file
+4. [ ] Create `nodeTypes` object mapping types to components with `markRaw`
+5. [ ] Update VueFlow component to use `:node-types` prop instead of template slots
+6. [ ] Remove old `processFlowNodes.js` file
+7. [ ] Test all node types render correctly
+8. [ ] Test in production environment
+
+### Key Node Types to Migrate
+Based on your `processFlowNodes.js`, you have these node types:
+- `start` - Start nodes
+- `form` - Form nodes
+- `end` - End nodes
+- `conditional` - Conditional/gateway nodes
+- `script` - Script execution nodes
+- `api` - API call nodes
+- `notification` - Notification nodes
+- `subprocess` - Subprocess nodes
+- Various shape nodes (rectangle, circle, diamond, etc.)
+
+## ๐ง Common Patterns for Migration
+
+### Computed Properties
+Convert computed properties from options API to composition API:
+
+```javascript
+// OLD (in processFlowNodes.js)
+computed: {
+ nodeLabel() {
+ return this.data?.label || 'Default'
+ }
+}
+
+// NEW (in .vue component)
+const nodeLabel = computed(() => {
+ return props.data?.label || 'Default'
+})
+```
+
+### Event Handlers
+```javascript
+// OLD
+methods: {
+ onClick() {
+ this.$emit('node-click', this.id)
+ }
+}
+
+// NEW
+const emit = defineEmits(['node-click'])
+const onClick = () => {
+ emit('node-click', props.id)
+}
+```
+
+### Complex Node Logic
+For nodes with complex logic (like ConditionalNode), maintain the same patterns but convert to composition API:
+
+```vue
+
+```
+
+## ๐ Benefits of File-Based Approach
+
+1. **Production Compatibility** - Works reliably in all environments
+2. **Better Performance** - Proper component compilation and tree-shaking
+3. **Developer Experience** - Full IDE support, syntax highlighting, linting
+4. **Maintainability** - Separate files are easier to manage and debug
+5. **Reusability** - Components can be imported and used elsewhere
+6. **Scoped Styles** - No CSS conflicts between node types
+
+## ๐ Debugging Tips
+
+### Error Handling
+Add error handling to catch Vue Flow issues:
+
+```javascript
+const handleVueFlowError = (error) => {
+ console.error('Vue Flow Error:', error)
+ if (isErrorOfType(error, ErrorCode.MISSING_VIEWPORT_DIMENSIONS)) {
+ console.error('Container needs explicit width/height')
+ }
+}
+```
+
+### Node ID Conflicts
+Ensure all node IDs are unique across your application to prevent rendering conflicts.
+
+## ๐ File Structure After Migration
+
+```
+components/process-flow/custom/
+โโโ StartNode.vue
+โโโ FormNode.vue
+โโโ EndNode.vue
+โโโ ConditionalNode.vue
+โโโ ScriptNode.vue
+โโโ ApiNode.vue
+โโโ NotificationNode.vue
+โโโ SubprocessNode.vue
+โโโ RectangleShapeNode.vue
+โโโ CircleShapeNode.vue
+โโโ DiamondShapeNode.vue
+โโโ TriangleShapeNode.vue
+โโโ PentagonShapeNode.vue
+โโโ HexagonShapeNode.vue
+โโโ OctagonShapeNode.vue
+
+pages/process-builder/
+โโโ index.vue (updated to import and use file-based nodes)
+
+# Remove after migration:
+composables/processFlowNodes.js โ
+```
+
+---
+
+## ๐ฏ Ready to Migrate?
+
+When you're ready to start the migration, provide:
+1. Your current `pages/process-builder/index.vue` file
+2. Your current `composables/processFlowNodes.js` file
+3. Any specific node types you want to prioritize
+
+The assistant will help you convert each node type to the production-safe file-based approach! ๐
\ No newline at end of file
diff --git a/pages/test.vue b/pages/test.vue
deleted file mode 100644
index 3819ed2..0000000
--- a/pages/test.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
Vue Flow Test Component
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/plugins/process-flow-styles.client.js b/plugins/process-flow-styles.client.js
index 5929ec3..62d20f8 100644
--- a/plugins/process-flow-styles.client.js
+++ b/plugins/process-flow-styles.client.js
@@ -1,4 +1,4 @@
-import { nodeStyles } from '~/components/process-flow/ProcessFlowNodes';
+import { nodeStyles } from '~/composables/processFlowNodes';
export default defineNuxtPlugin(() => {
// Create a style element
diff --git a/stores/formBuilder.js b/stores/formBuilder.js
index 7df5cfb..0a1b4a2 100644
--- a/stores/formBuilder.js
+++ b/stores/formBuilder.js
@@ -613,11 +613,6 @@ export const useFormBuilderStore = defineStore('formBuilder', {
this.hasUnsavedChanges = false;
},
- // Update preview form data
- updatePreviewFormData(data) {
- this.previewFormData = { ...data };
- },
-
// Get forms from the backend
async getForms() {
try {