# Process Builder Technical Appendix This document provides technical implementation details for developers working with the Process Builder system. > For user documentation and usage guidelines, please refer to [Process Builder Documentation](PROCESS_BUILDER_DOCUMENTATION.md) ## Architecture Overview ### Technology Stack - **Frontend Framework**: Nuxt 3 / Vue 3 - **State Management**: Pinia - **Flow Visualization**: Vue Flow - **UI Framework**: Tailwind CSS - **Icons**: Material Design Icons - **Validation**: Zod ### Key Dependencies ```json { "@vue-flow/core": "^1.42.5", "@vue-flow/background": "^1.3.2", "@vue-flow/controls": "^1.1.2", "@vue-flow/minimap": "^1.5.3", "@pinia/nuxt": "^0.4.11", "uuid": "^10.0.0", "zod": "^3.22.2" } ``` ## Project Structure ``` pages/ ├── process-builder/ │ ├── index.vue # Main builder interface │ └── manage.vue # Process management components/ ├── process-flow/ │ ├── ProcessFlowCanvas.vue # Flow canvas │ ├── ProcessFlowNodes.js # Custom node types │ ├── FormSelector.vue # Form selection component │ └── GatewayConditionManager.vue # Gateway conditions UI stores/ └── processBuilder.js # State management composables/ └── useProcessValidation.js # Process validation types/ └── process-builder.d.ts # TypeScript definitions ``` ## Component Architecture ### Core Components 1. **ProcessFlowCanvas.vue** ```vue ``` 2. **ProcessFlowNodes.js** ```javascript import { h, markRaw } from 'vue'; import { Handle, Position } from '@vue-flow/core'; // Custom node renderer with handles const CustomNode = markRaw({ template: `
{{ label }}
`, props: ['id', 'type', 'label', 'data'], components: { Handle } }); // Node type definitions export const nodeTypes = markRaw({ task: TaskNode, start: StartNode, end: EndNode, gateway: GatewayNode, form: FormNode, script: ScriptNode }); ``` 3. **FormSelector.vue** ```vue ``` ## State Management ### Process Builder Store ```typescript export const useProcessBuilderStore = defineStore('processBuilder', { state: () => ({ processes: [], currentProcess: null, selectedNodeId: null, selectedEdgeId: null, history: [], historyIndex: -1, unsavedChanges: false }), getters: { selectedNode: (state) => { if (!state.currentProcess || !state.selectedNodeId) return null; return state.currentProcess.nodes.find(node => node.id === state.selectedNodeId); }, selectedEdge: (state) => { if (!state.currentProcess || !state.selectedEdgeId) return null; return state.currentProcess.edges.find(edge => edge.id === state.selectedEdgeId); }, hasUnsavedChanges: (state) => { return state.unsavedChanges; } }, actions: { createProcess(name, description) { const process = { id: uuidv4(), name, description, nodes: [], edges: [], createdAt: new Date().toISOString() }; this.processes.push(process); this.currentProcess = JSON.parse(JSON.stringify(process)); // Deep clone this.clearHistory(); this.unsavedChanges = false; }, addNode(node) { if (!this.currentProcess) return; const newNode = { id: node.id || uuidv4(), type: node.type, label: node.label || 'New Node', position: node.position || { x: 0, y: 0 }, data: node.data || {} }; this.currentProcess.nodes.push(newNode); this.selectedNodeId = newNode.id; this.saveToHistory('Add node'); this.unsavedChanges = true; return newNode; }, updateNode(nodeId, updates) { if (!this.currentProcess) return; const node = this.currentProcess.nodes.find(n => n.id === nodeId); if (node) { Object.assign(node, updates); this.saveToHistory('Update node'); this.unsavedChanges = true; } }, deleteNode(nodeId) { if (!this.currentProcess) return; const index = this.currentProcess.nodes.findIndex(n => n.id === nodeId); if (index !== -1) { // Remove the node this.currentProcess.nodes.splice(index, 1); // Remove any edges connected to this node const edgesToRemove = this.currentProcess.edges.filter( edge => edge.source === nodeId || edge.target === nodeId ); edgesToRemove.forEach(edge => { const edgeIndex = this.currentProcess.edges.findIndex(e => e.id === edge.id); if (edgeIndex !== -1) { this.currentProcess.edges.splice(edgeIndex, 1); } }); // Clear selection if the deleted node was selected if (this.selectedNodeId === nodeId) { this.selectedNodeId = null; } this.saveToHistory('Delete node'); this.unsavedChanges = true; return true; } return false; } } }); ``` ## Node Types and Styles ### Node Configuration ```typescript interface NodeConfig { type: 'start' | 'end' | 'task' | 'form' | 'script' | 'gateway'; label: string; icon: string; iconColor: string; data: { description?: string; assignee?: string; formId?: string; formName?: string; language?: string; conditions?: Condition[]; defaultPath?: string; }; } const nodeConfigs: Record = { start: { type: 'start', label: 'Start', icon: 'play_circle_filled', iconColor: 'text-green-500', data: { description: 'Process starts here' } }, task: { type: 'task', label: 'Task', icon: 'assignment', iconColor: 'text-blue-500', data: { description: 'Task node', assignee: '' } }, form: { type: 'form', label: 'Form Task', icon: 'description', iconColor: 'text-purple-500', data: { description: 'Form submission task', formId: null, formName: null, formUuid: null } }, // Additional node configurations... }; ``` ## Connection Handling ### Connection Logic ```typescript // Connection validation function validateConnection(connection: Connection): boolean { if (!connection.source || !connection.target) return false; if (connection.source === connection.target) return false; const sourceNode = nodes.value.find(n => n.id === connection.source); const targetNode = nodes.value.find(n => n.id === connection.target); if (!sourceNode || !targetNode) return false; // Prevent connecting to start node's input or from end node's output if (targetNode.type === 'start') return false; if (sourceNode.type === 'end') return false; return true; } // Create new connection function createConnection(connection: Connection): Edge { return { id: `${connection.source}-${connection.target}`, source: connection.source, target: connection.target, type: 'smoothstep', animated: true, style: { stroke: '#555' } }; } ``` ## Form Integration ### Form Task Implementation ```typescript // Form task node implementation const FormNode = markRaw({ props: ['id', 'type', 'label', 'selected', 'data'], computed: { nodeLabel() { 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() { const badgeContent = this.hasForm ? h('span', { class: 'node-badge bg-purple-100 text-purple-600 px-1 text-xs rounded' }, 'Form') : null; 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'), badge: () => badgeContent, default: () => h('div', { class: 'node-details' }, [ h('p', { class: 'node-description' }, this.data?.description || 'Form submission task'), h('div', { class: 'node-form-info' }, [ h('span', { class: 'node-form-label' }, 'Form: '), h('span', { class: this.hasForm ? 'node-form-value text-purple-600 font-medium' : 'node-form-value text-gray-400 italic' }, this.formName) ]) ]) }); } }); ``` ### Form Selection in Process Builder ```vue
``` ## Decision Point/Gateway Node ### Gateway Node Implementation ```typescript // 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'; } }, render() { // Create the badge content const badgeContent = h('span', { class: 'node-badge bg-orange-100 text-orange-600 px-1 text-xs rounded absolute -top-5 left-1/2 transform -translate-x-1/2 whitespace-nowrap' }, `${this.totalPaths} path${this.totalPaths !== 1 ? 's' : ''}`); 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'), badge: () => badgeContent, default: () => h('div', { class: 'gateway-details' }, [ h('div', { class: 'node-conditions-value' }, this.conditionSummary) ]) }); } }); ``` ### Gateway Node Styling ```css /* Gateway specific styles */ .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-gateway:hover { border-color: #ea580c; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .node-gateway .custom-node-content { position: absolute; transform: rotate(-45deg); width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 8px; } .node-gateway .custom-node-title { font-size: 12px; font-weight: 500; color: #333; margin: 0; text-align: center; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; } .node-gateway .gateway-details { width: 100%; text-align: center; margin-top: 4px; } .node-gateway .node-conditions-value { font-size: 11px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; line-height: 1.2; } .node-gateway .material-icons { font-size: 24px; color: #f97316; margin-bottom: 4px; } .node-gateway .node-badge { position: absolute; top: 0; left: 50%; transform: translateX(-50%) rotate(-45deg); background-color: #fff7ed; border: 1px solid #fdba74; z-index: 10; font-size: 11px; padding: 2px 8px; white-space: nowrap; margin-top: 8px; } /* Position handles correctly for gateway node */ .handle-gateway-input { transform: translateY(-42px) !important; background-color: #f97316 !important; border: 2px solid white !important; width: 12px !important; height: 12px !important; } .handle-gateway-output { transform: translateY(42px) !important; background-color: #f97316 !important; border: 2px solid white !important; width: 12px !important; height: 12px !important; } ``` ### Gateway Condition Management ```typescript // Handle condition update const handleConditionUpdate = (conditions) => { if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') { // Update conditions in the node data selectedNodeData.value.data = { ...selectedNodeData.value.data, conditions: conditions }; // Update edges with new condition outputs if (processStore.currentProcess?.edges) { const updatedEdges = processStore.currentProcess.edges.map(edge => { if (edge.source === selectedNodeData.value.id) { // Find matching condition group const matchingGroup = conditions.find(group => group.output === edge.label); if (!matchingGroup) { // If no matching group found, update edge label to default return { ...edge, label: selectedNodeData.value.data.defaultPath || 'Default' }; } } return edge; }); // Update edges in store processStore.currentProcess.edges = updatedEdges; } // Update the node in store updateNodeInStore(); } }; ``` ## Event Handling ### Node Events ```typescript // Node selection function onNodeClick({ node }): void { try { // Create a plain object copy of the node to avoid reactivity issues const nodeData = { id: node.id, type: node.type, data: node.data ? JSON.parse(JSON.stringify(node.data)) : {}, position: node.dimensions ? { x: node.dimensions.x || 0, y: node.dimensions.y || 0 } : { x: 0, y: 0 } }; selectedNode.value = nodeData; emit('nodeSelected', nodeData); } catch (error) { console.error('Error processing node data:', error); } } // Node deletion function onNodeDelete(event): void { // Check if we have a node in the event if (event && event.node) { removeNodes([event.node]); emit('nodesChange', nodes.value); } } // Handle delete key press function onDeleteKeyPress(): void { const { getSelectedNodes, getSelectedEdges } = flowInstance.value; const selectedNodes = getSelectedNodes(); const selectedEdges = getSelectedEdges(); if (selectedNodes.length > 0) { removeNodes(selectedNodes); emit('nodesChange', nodes.value); } if (selectedEdges.length > 0) { removeEdges(selectedEdges); emit('edgesChange', edges.value); } } ``` ### Edge Events ```typescript // Edge selection function onEdgeClick(event, edge): void { // Create a simplified copy of the edge data const edgeData = { id: edge.id, source: edge.source, target: edge.target, label: edge.label || '', sourceNode: nodes.value.find(node => node.id === edge.source), targetNode: nodes.value.find(node => node.id === edge.target) }; emit('edgeSelected', edgeData); } // Edge deletion function onEdgeDelete(event): void { if (event && event.edge) { removeEdges([event.edge]); emit('edgesChange', edges.value); } } ``` ## Development Guidelines ### Best Practices 1. Use Vue Flow's built-in features instead of custom implementations 2. Handle all node/edge updates through the store 3. Maintain proper typings for all components 4. Follow Vue 3 Composition API patterns 5. Implement proper validation for all process changes ### Performance Considerations 1. Use `markRaw` for node components 2. Minimize reactive wrapping of node data 3. Use proper key bindings for lists 4. Implement efficient node filtering 5. Optimize canvas rendering ### Error Handling 1. Validate all connections before creation 2. Handle edge cases in node operations 3. Provide meaningful error messages 4. Implement proper error boundaries 5. Log errors appropriately --- For user documentation and usage guidelines, please refer to [Process Builder Documentation](PROCESS_BUILDER_DOCUMENTATION.md). Last updated: June 10, 2024