From f8a67c4467db8cd44ca169a0b57c00e719b7466a Mon Sep 17 00:00:00 2001 From: Md Afiq Iskandar Date: Mon, 21 Jul 2025 11:21:58 +0800 Subject: [PATCH] Refactor Process Flow Nodes and Enhance ProcessFlowCanvas Component - Moved custom node definitions from `composables/processFlowNodes.js` to individual `.vue` files for better production compatibility and maintainability. - Updated `ProcessFlowCanvas.vue` to import node types from the new file structure, ensuring proper rendering of custom nodes. - Added new methods for setting nodes and edges directly in the `ProcessFlowCanvas` component, improving flexibility in managing flow state. - Removed the deprecated `test.vue` file to streamline the codebase and eliminate unnecessary components. - Adjusted styles and structure in the process flow components to enhance user experience and maintain consistency across the application. --- components/process-flow/ProcessFlowCanvas.vue | 27 +- composables/processFlowNodes.js | 1814 +++++++++++++++++ docs/vue-flow-custom-nodes-migration.md | 304 +++ pages/test.vue | 77 - plugins/process-flow-styles.client.js | 2 +- stores/formBuilder.js | 5 - 6 files changed, 2144 insertions(+), 85 deletions(-) create mode 100644 composables/processFlowNodes.js create mode 100644 docs/vue-flow-custom-nodes-migration.md delete mode 100644 pages/test.vue 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: ` +
+ + + + + + + + + +
+ + + +
+ + + +
+ `, + 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 + + + + + +``` + +### 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 + + + + +``` + +### โŒ 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 @@ - - - - - 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 {