Enhance Form and Process Builder Documentation

- Updated the Form Builder documentation to include integration details with the Process Builder, outlining how to connect forms to processes and manage form data.
- Added a new section on Process Builder integration, detailing API endpoints for form selection and loading forms by URL parameters.
- Improved the Process Builder documentation with instructions for adding form tasks, selecting forms, and handling form data within processes.
- Updated last modified dates for documentation files to reflect recent changes.
This commit is contained in:
Md Afiq Iskandar 2025-05-15 10:42:58 +08:00
parent bb5e4c0637
commit 668e08884e
4 changed files with 570 additions and 42 deletions

View File

@ -2,7 +2,7 @@
## Overview
The Form Builder is a powerful drag-and-drop interface for creating dynamic forms. It provides an intuitive, visual way to build forms by selecting components, configuring their properties, and arranging them in your desired layout. Perfect for creating everything from simple contact forms to complex multi-step surveys.
The Form Builder is a powerful drag-and-drop interface for creating dynamic forms. It provides an intuitive, visual way to build forms by selecting components, configuring their properties, and arranging them in your desired layout. Perfect for creating everything from simple contact forms to complex multi-step surveys. Forms can be used independently or integrated with the Process Builder for workflow automation.
> For technical implementation details, please refer to [Form Builder Technical Appendix](FORM_BUILDER_TECHNICAL_APPENDIX.md)
@ -136,6 +136,31 @@ Form structure and organization:
- Custom CSS classes
- Event handlers
## Process Builder Integration
The Form Builder integrates with the Process Builder to create workflow-driven forms:
### Connecting Forms to Processes
1. **Create and Save Your Form**
- Design your form in the Form Builder
- Save the form with a clear name and description
- Forms must be saved before they can be used in processes
2. **Using Forms in Process Builder**
- In Process Builder, add a Form Task to your process
- Select your form from the Form Selector dropdown
- The form will be presented to users when they reach this task in the workflow
3. **Form Data in Processes**
- Data submitted through forms becomes available as process variables
- Use form data to drive process decisions in gateways
- Reference form fields in scripts and conditions
### Form URL Parameters
- Forms can accept URL parameters to pre-populate fields
- Process variables can be passed to forms as parameters
- Use the `formId` parameter to load a specific form
## Best Practices
### Form Design
@ -196,6 +221,11 @@ Form structure and organization:
- Check field names are unique
- Ensure validation is enabled
4. **Form Not Appearing in Process Builder**
- Verify the form was saved successfully
- Check user permissions
- Refresh the Process Builder page
### Getting Help
- Check the technical documentation
- Contact support team
@ -206,4 +236,4 @@ Form structure and organization:
For technical details about implementation, component structure, and development guidelines, please refer to the [Technical Appendix](FORM_BUILDER_TECHNICAL_APPENDIX.md).
Last updated: April 9, 2025
Last updated: June 10, 2024

View File

@ -43,6 +43,11 @@ composables/
└── useToast.js # Toast notifications
types/
└── form-builder.d.ts # TypeScript definitions
server/
└── api/
└── forms/
├── index.js # Form API endpoints
└── [id].js # Form by ID endpoints
```
## Component Architecture
@ -129,6 +134,8 @@ interface FormState {
selectedComponentId: string | null;
formName: string;
formDescription: string;
formId: string | null;
formUUID: string | null;
isDraggingOver: boolean;
savedForms: SavedForm[];
}
@ -139,20 +146,89 @@ export const useFormBuilderStore = defineStore('formBuilder', {
selectedComponentId: null,
formName: 'New Form',
formDescription: '',
formId: null,
formUUID: null,
isDraggingOver: false,
savedForms: []
}),
getters: {
selectedComponent: (state) => // Implementation
formConfig: (state) => // Implementation
selectedComponent: (state) => {
if (!state.selectedComponentId) return null;
return state.formComponents.find(c => c.id === state.selectedComponentId);
},
formConfig: (state) => {
return {
id: state.formId,
uuid: state.formUUID,
name: state.formName,
description: state.formDescription,
components: state.formComponents
};
}
},
actions: {
addComponent(component: FormComponent) // Implementation
updateComponent(id: string, updates: Partial<FormComponent>) // Implementation
deleteComponent(id: string) // Implementation
moveComponent(oldIndex: number, newIndex: number) // Implementation
addComponent(component: FormComponent) {
this.formComponents.push({
...component,
id: component.id || uuidv4()
});
},
updateComponent(id: string, updates: Partial<FormComponent>) {
const index = this.formComponents.findIndex(c => c.id === id);
if (index !== -1) {
this.formComponents[index] = {
...this.formComponents[index],
...updates
};
}
},
deleteComponent(id: string) {
const index = this.formComponents.findIndex(c => c.id === id);
if (index !== -1) {
this.formComponents.splice(index, 1);
if (this.selectedComponentId === id) {
this.selectedComponentId = null;
}
}
},
moveComponent(oldIndex: number, newIndex: number) {
if (oldIndex === newIndex) return;
const component = this.formComponents.splice(oldIndex, 1)[0];
this.formComponents.splice(newIndex, 0, component);
},
saveForm() {
return fetch('/api/forms', {
method: this.formId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.formConfig)
})
.then(response => response.json())
.then(data => {
this.formId = data.formID;
this.formUUID = data.formUUID;
return data;
});
},
loadForm(id: string) {
return fetch(`/api/forms/${id}`)
.then(response => response.json())
.then(data => {
this.formId = data.formID;
this.formUUID = data.formUUID;
this.formName = data.formName;
this.formDescription = data.formDescription;
this.formComponents = data.components;
return data;
});
}
}
});
```
@ -226,6 +302,104 @@ const gridSystem = {
};
```
## Process Builder Integration
### API Endpoints for Form Selection
```javascript
// server/api/forms/index.js
export default defineEventHandler(async (event) => {
try {
// Get all forms for selection in Process Builder
const forms = await prisma.form.findMany({
select: {
formID: true,
formUUID: true,
formName: true,
formDescription: true,
createdAt: true,
updatedAt: true
},
orderBy: {
updatedAt: 'desc'
}
});
return { forms };
} catch (error) {
console.error('Error fetching forms:', error);
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch forms'
});
}
});
```
### Form Loading by URL Parameter
```javascript
// pages/form-builder/index.vue
const route = useRoute();
const formStore = useFormBuilderStore();
// Check for formId parameter to load a specific form
onMounted(async () => {
const formId = route.query.formId;
if (formId) {
try {
await formStore.loadForm(formId);
} catch (error) {
console.error('Error loading form:', error);
useToast().error('Failed to load form');
}
}
});
```
### Form Selection Component
```vue
<!-- components/process-flow/FormSelector.vue -->
<template>
<div class="form-selector">
<h4 class="text-sm font-medium mb-2">Select Form</h4>
<div v-if="loading" class="text-center py-4">
<span class="loading"></span>
</div>
<div v-else-if="error" class="text-center py-4 text-red-500">
{{ error }}
</div>
<div v-else-if="forms.length === 0" class="text-center py-4 text-gray-500">
No forms available
</div>
<div v-else class="space-y-2">
<div
v-for="form in forms"
:key="form.formID"
class="form-item p-2 border rounded hover:bg-gray-50 cursor-pointer"
:class="{ 'border-blue-400 bg-blue-50': modelValue === form.formID }"
@click="selectForm(form)"
>
<div class="font-medium">{{ form.formName }}</div>
<div class="text-xs text-gray-500">{{ form.formDescription }}</div>
</div>
</div>
<div class="mt-4">
<button
v-if="modelValue"
@click="clearSelection"
class="text-sm text-red-500 hover:underline"
>
Clear Selection
</button>
</div>
</div>
</template>
```
## Event Handling
### Component Events
@ -355,4 +529,4 @@ npm run preview
For user documentation and usage guidelines, please refer to [Form Builder Documentation](FORM_BUILDER_DOCUMENTATION.md).
Last updated: April 9, 2025
Last updated: June 10, 2024

View File

@ -62,12 +62,13 @@ Activities represent work performed in a process:
- **Form Task** (Purple Icon)
- A task that requires form data
- Has both input and output handles
- Properties: Form name, description
- Properties: Form selection, description
- Connects to forms created in the Form Builder
- **Script Task** (Grey Icon)
- Automated task that executes code
- Has both input and output handles
- Properties: Language, description
- Properties: Language, description, script content
### Gateways
Gateways control flow divergence and convergence:
@ -75,7 +76,7 @@ Gateways control flow divergence and convergence:
- **Gateway** (Orange Icon)
- Creates alternative paths based on conditions
- Has both input and output handles
- Properties: Conditions, description
- Properties: Conditions, default path, description
## Working with the Process Canvas
@ -88,7 +89,7 @@ Gateways control flow divergence and convergence:
- **Select**: Click on an element
- **Multi-select**: Hold Shift while clicking elements
- **Move**: Drag selected elements
- **Delete**: Press Delete key or double-click element
- **Delete**: Press Delete key, double-click element, or use the Delete button in the properties panel
- **Connect**: Drag from one node's handle to another's
### Keyboard Shortcuts
@ -123,6 +124,24 @@ Gateways control flow divergence and convergence:
- Role-based assignments
- Group assignments
## Form Integration
The Process Builder integrates with the Form Builder to allow forms to be attached to process tasks:
1. **Adding a Form Task**
- Drag a Form Task component onto the canvas
- Select the task to open its properties
2. **Selecting a Form**
- In the properties panel, use the Form Selector to choose a form
- Forms are listed from those created in the Form Builder
- Selected forms will be displayed to users when they reach this task
3. **Form Data in Process**
- Form submissions become available as process variables
- Data can be referenced in gateway conditions
- Form fields can be pre-populated with process data
## Best Practices
### Process Design
@ -155,13 +174,19 @@ Gateways control flow divergence and convergence:
2. **Node Won't Delete**
- Make sure the node is selected
- Try using the Delete key
- Alternative: double-click the node
- Use the Delete button in the properties panel
- Double-click the node to remove it
3. **Connection Looks Wrong**
- Try repositioning nodes for better flow
- Check that connections are made to correct handles
- Consider using different connection types
4. **Form Not Showing in Selector**
- Verify the form was saved in the Form Builder
- Check that you have permission to access the form
- Refresh the page to update the form list
### Getting Help
- Use the control panel hints in top-right
- Review this documentation
@ -171,4 +196,4 @@ Gateways control flow divergence and convergence:
For technical details about implementation and integration, please refer to the [Process Builder Technical Documentation](PROCESS_BUILDER_TECHNICAL_APPENDIX.md).
Last updated: May 15, 2024
Last updated: June 10, 2024

View File

@ -37,7 +37,9 @@ pages/
components/
├── process-flow/
│ ├── ProcessFlowCanvas.vue # Flow canvas
│ └── ProcessFlowNodes.js # Custom node types
│ ├── ProcessFlowNodes.js # Custom node types
│ ├── FormSelector.vue # Form selection component
│ └── GatewayConditionManager.vue # Gateway conditions UI
stores/
└── processBuilder.js # State management
composables/
@ -121,6 +123,39 @@ const handleConnect = (connection) => {
addEdges([newEdge]);
};
// Handle node deletion
const onNodeDelete = (event) => {
if (event && event.node) {
removeNodes([event.node]);
emit('nodesChange', nodes.value);
}
};
// Handle edge deletion
const onEdgeDelete = (event) => {
if (event && event.edge) {
removeEdges([event.edge]);
emit('edgesChange', edges.value);
}
};
// Handle delete key press
const onDeleteKeyPress = () => {
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);
}
};
</script>
```
@ -172,6 +207,62 @@ export const nodeTypes = markRaw({
});
```
3. **FormSelector.vue**
```vue
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
modelValue: {
type: String,
default: null
}
});
const emit = defineEmits(['update:modelValue', 'select']);
const forms = ref([]);
const loading = ref(false);
const error = ref(null);
// Load available forms from the API
const loadForms = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/forms');
if (!response.ok) throw new Error('Failed to load forms');
const data = await response.json();
forms.value = data.forms || [];
} catch (err) {
error.value = err.message;
console.error('Error loading forms:', err);
} finally {
loading.value = false;
}
};
// Select a form
const selectForm = (form) => {
emit('update:modelValue', form.formID);
emit('select', form);
};
// Clear form selection
const clearSelection = () => {
emit('update:modelValue', null);
emit('select', null);
};
// Load forms on component mount
onMounted(() => {
loadForms();
});
</script>
```
## State Management
### Process Builder Store
@ -182,9 +273,27 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
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 = {
@ -196,26 +305,74 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
createdAt: new Date().toISOString()
};
this.processes.push(process);
this.currentProcess = process;
this.currentProcess = JSON.parse(JSON.stringify(process)); // Deep clone
this.clearHistory();
this.unsavedChanges = false;
},
updateNode(nodeData) {
if (!this.currentProcess || !nodeData.id) return;
const nodeIndex = this.currentProcess.nodes.findIndex(
node => node.id === nodeData.id
);
if (nodeIndex > -1) {
this.currentProcess.nodes[nodeIndex] = {
...this.currentProcess.nodes[nodeIndex],
...nodeData
};
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;
}
},
// Additional actions...
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;
}
}
});
```
@ -232,9 +389,11 @@ interface NodeConfig {
data: {
description?: string;
assignee?: string;
formId?: string;
formName?: string;
language?: string;
conditions?: string[];
conditions?: Condition[];
defaultPath?: string;
};
}
@ -253,6 +412,18 @@ const nodeConfigs: Record<string, NodeConfig> = {
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...
};
```
@ -291,26 +462,154 @@ function createConnection(connection: Connection): Edge {
}
```
## 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
<!-- Form selection in process properties panel -->
<div v-if="selectedNodeData.type === 'form'" class="space-y-3">
<FormSelector
v-model="selectedNodeData.data.formId"
@select="handleFormSelection"
/>
</div>
<script setup>
// Form selection handler
const handleFormSelection = (form) => {
if (selectedNodeData.value && form) {
selectedNodeData.value.data.formId = form.formID;
selectedNodeData.value.data.formName = form.formName;
selectedNodeData.value.data.formUuid = form.formUUID;
updateNodeInStore();
}
};
// Clear form selection
const clearFormSelection = () => {
if (selectedNodeData.value) {
selectedNodeData.value.data.formId = null;
selectedNodeData.value.data.formName = '';
selectedNodeData.value.data.formUuid = null;
updateNodeInStore();
}
};
</script>
```
## Event Handling
### Node Events
```typescript
// Node selection
function onNodeClick(node: Node): void {
selectedNode.value = node;
emit('nodeSelected', node);
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(nodes: Node[]): void {
removeNodes(nodes);
emit('nodesChange', nodes.value);
function onNodeDelete(event): void {
// Check if we have a node in the event
if (event && event.node) {
removeNodes([event.node]);
emit('nodesChange', nodes.value);
}
}
// Node dragging
function onNodeDragStop(node: Node): void {
updateNodePosition(node);
emit('nodePositionChange', node);
// 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);
}
}
```
@ -341,4 +640,4 @@ function onNodeDragStop(node: Node): void {
For user documentation and usage guidelines, please refer to [Process Builder Documentation](PROCESS_BUILDER_DOCUMENTATION.md).
Last updated: May 15, 2024
Last updated: June 10, 2024