# 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: `
`,
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