# 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'], render() { // Check if we have a form selected const hasForm = this.data?.formId && this.data?.formName; // Create badge content based on form selection status const badgeContent = 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.label || 'Form Task', 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: hasForm ? 'node-form-value text-purple-600 font-medium' : 'node-form-value text-gray-400 italic' }, hasForm ? this.data.formName : 'None selected') ]) ]) }); } }); ``` ### Form Selection in Process Builder ```vue
``` ## 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