From fa90c234c5f551b8184296d81594882d92365020 Mon Sep 17 00:00:00 2001 From: Afiq Date: Tue, 27 May 2025 17:00:24 +0800 Subject: [PATCH] Enhance Process Builder with New Features and Configuration Options - Added a new ProcessTemplatesModal component for selecting process flow templates, improving user experience in template management. - Introduced a ProcessSettingsModal component for comprehensive process configuration, including process info, execution settings, and permissions management. - Updated BusinessRuleNodeConfiguration and FormNodeConfiguration components to enhance user interaction and streamline configuration processes. - Implemented new API endpoints for managing form fields and settings, allowing for better integration and data handling. - Enhanced existing components with improved styling and functionality, including dynamic field conditions and bidirectional data mapping. - Updated nuxt.config.js to include security settings for API routes, ensuring better protection against XSS and request size limitations. - Removed the deprecated TaskNodeConfiguration component to streamline the process builder interface. - Improved documentation to reflect recent changes and enhancements in the process builder features. --- components/ProcessTemplatesModal.vue | 1073 ++++++++++++++++ .../BusinessRuleNodeConfiguration.vue | 266 +++- .../process-flow/FormNodeConfiguration.vue | 432 ++++++- .../FormNodeConfigurationModal.vue | 23 +- .../process-flow/GatewayConditionManager.vue | 223 +++- .../process-flow/ProcessBuilderComponents.vue | 35 +- components/process-flow/ProcessFlowCanvas.vue | 117 +- components/process-flow/ProcessFlowNodes.js | 559 ++++++--- .../process-flow/ProcessSettingsModal.vue | 532 ++++++++ .../process-flow/TaskNodeConfiguration.vue | 391 ------ components/process-flow/VariableManager.vue | 147 ++- doc/README.md | 18 + doc/process-builder/ROADMAP.md | 52 + doc/process-builder/TECHNICAL_GUIDE.md | 1081 ++++++++++++++++- doc/process-builder/USER_GUIDE.md | 445 ++++++- nuxt.config.js | 6 + pages/form-builder/index.vue | 3 +- pages/process-builder/index.vue | 250 +++- server/api/forms/[formId]/fields.get.js | 91 ++ server/api/forms/[id].put.js | 17 + server/api/forms/[id]/fields.get.js | 91 ++ stores/processBuilder.js | 16 + stores/variableStore.js | 13 +- 23 files changed, 5009 insertions(+), 872 deletions(-) create mode 100644 components/ProcessTemplatesModal.vue create mode 100644 components/process-flow/ProcessSettingsModal.vue delete mode 100644 components/process-flow/TaskNodeConfiguration.vue create mode 100644 server/api/forms/[formId]/fields.get.js create mode 100644 server/api/forms/[id]/fields.get.js diff --git a/components/ProcessTemplatesModal.vue b/components/ProcessTemplatesModal.vue new file mode 100644 index 0000000..fa258ce --- /dev/null +++ b/components/ProcessTemplatesModal.vue @@ -0,0 +1,1073 @@ + + + + + \ No newline at end of file diff --git a/components/process-flow/BusinessRuleNodeConfiguration.vue b/components/process-flow/BusinessRuleNodeConfiguration.vue index c94e108..c109f4e 100644 --- a/components/process-flow/BusinessRuleNodeConfiguration.vue +++ b/components/process-flow/BusinessRuleNodeConfiguration.vue @@ -172,7 +172,7 @@ +
+ + {{ availableVariables.find(v => v.name === condition.variable)?.type }} type + +
- - - +

The source variable containing the data

- - Form Field +

- The target field ID in the form + {{ formFields.length === 0 ? 'No form fields available' : 'The target field in the form' }}

@@ -217,41 +211,35 @@
- - Form Field +

- The source field ID in the form + {{ formFields.length === 0 ? 'No form fields available' : 'The source field in the form' }}

- +
+ + +
+
+
+ 3 +
+

Field Conditions

+
+ +

+ Configure conditional field behavior based on process variables (e.g., make fields readonly, hidden, or required). +

+ +
+
+
+
+ + + + Field Conditions +
+

Control field visibility, readonly state, and requirements dynamically

+
+ + Add Condition + +
+ + +
+

+ No field conditions defined. All fields will use their default behavior. +

+ + Add First Condition + +
+ +
+
+
+
+ + Field Condition #{{ index + 1 }} +
+ +
+ +
+ +
+ + +

Variable to check

+
+ + +
+ + +

Comparison type

+
+ + +
+ + +
+ No value needed for this operator +
+

Value to compare

+
+ + +
+ + +

Field to control

+
+
+ + +
+
+ + +

What to do when condition is met

+
+ +
+ + +

For documentation purposes

+
+
+
+
+
+
@@ -295,6 +455,10 @@ const emit = defineEmits(['update']); // Get the variable store for creating variables const variableStore = useVariableStore(); +// State for form fields +const formFields = ref([]); +const isLoadingFields = ref(false); + // Local state for node data - create a deep copy to avoid mutation issues const localNodeData = ref({ label: 'Form Task', @@ -303,11 +467,12 @@ const localNodeData = ref({ formName: '', formUuid: null, inputMappings: [], - outputMappings: [] + outputMappings: [], + fieldConditions: [] }); // Watch for changes from parent props -watch(() => props.nodeData, (newNodeData) => { +watch(() => props.nodeData, async (newNodeData) => { if (newNodeData) { // Create a deep copy to break reactivity chains with parent localNodeData.value = { @@ -317,17 +482,25 @@ watch(() => props.nodeData, (newNodeData) => { formName: newNodeData.formName || '', formUuid: newNodeData.formUuid || null, inputMappings: Array.isArray(newNodeData.inputMappings) - ? [...newNodeData.inputMappings] + ? newNodeData.inputMappings.map(mapping => ({ ...mapping })) : [], outputMappings: Array.isArray(newNodeData.outputMappings) - ? [...newNodeData.outputMappings] + ? newNodeData.outputMappings.map(mapping => ({ ...mapping })) + : [], + fieldConditions: Array.isArray(newNodeData.fieldConditions) + ? newNodeData.fieldConditions.map(condition => ({ ...condition })) : [] }; + + // Load form fields if form is already selected + if (newNodeData.formId || newNodeData.formUuid) { + await loadFormFields(newNodeData.formId || newNodeData.formUuid); + } } }, { immediate: true, deep: true }); // Function to handle form selection -function handleFormSelection(form) { +async function handleFormSelection(form) { if (!form) return; localNodeData.value = { @@ -336,9 +509,13 @@ function handleFormSelection(form) { formName: form.formName, formUuid: form.formUUID, label: form.formName || 'Form Task', - description: `Form: ${form.formName}` + description: `Form: ${form.formName}`, + fieldConditions: [] }; + // Load form fields for this form + await loadFormFields(form.formID || form.formUUID); + saveChanges(); } @@ -352,9 +529,13 @@ function clearFormSelection() { label: 'Form Task', description: 'Form submission task', inputMappings: [], - outputMappings: [] + outputMappings: [], + fieldConditions: [] }; + // Clear form fields + formFields.value = []; + saveChanges(); } @@ -400,34 +581,60 @@ function removeOutputMapping(index) { // Generate a variable name from a form field function getVariableNameFromFormField(formField) { + // Handle cases where formField is not a string if (!formField) return 'form_data'; + // Convert to string if it's not already (in case it's an object) + let fieldName = ''; + if (typeof formField === 'string') { + fieldName = formField; + } else if (typeof formField === 'object' && formField.name) { + fieldName = formField.name; + } else if (typeof formField === 'object' && formField.value) { + fieldName = formField.value; + } else { + return 'form_data'; + } + + // Ensure we have a valid string + if (!fieldName || typeof fieldName !== 'string') { + return 'form_data'; + } + // Convert the form field to a valid variable name // Remove spaces, special characters, and convert to camelCase - return formField.replace(/[^a-zA-Z0-9_]/g, '_') + const result = fieldName.replace(/[^a-zA-Z0-9_]/g, '_') .replace(/^([A-Z])/, (match) => match.toLowerCase()) .replace(/[\s_]+(\w)/g, (_, c) => c.toUpperCase()); + + return result; } // Create a variable from a mapping function createVariableFromMapping(mapping) { - if (!mapping.processVariable || !mapping.processVariable.startsWith('create_new_')) return; + const processVariableValue = getStringValue(mapping.processVariable); - const variableName = mapping.processVariable.replace('create_new_', ''); - if (!variableName) return; + if (!processVariableValue || !processVariableValue.startsWith('create_new_')) { + return; + } + + const variableName = processVariableValue.replace('create_new_', ''); + if (!variableName) { + return; + } // Create the variable const newVariable = { name: variableName, type: 'string', scope: 'global', - description: `Form data from ${mapping.formField} in ${localNodeData.value.formName || 'form'}` + description: `Form data from ${getStringValue(mapping.formField)} in ${localNodeData.value.formName || 'form'}` }; // Add the variable variableStore.addVariable(newVariable); - // Update the mapping to use the new variable + // Update the mapping to use the new variable name (string value, not object) mapping.processVariable = variableName; // Save changes @@ -436,14 +643,115 @@ function createVariableFromMapping(mapping) { }); } +// Load form fields for the selected form +async function loadFormFields(formId) { + if (!formId) { + formFields.value = []; + return; + } + + isLoadingFields.value = true; + + try { + const response = await fetch(`/api/forms/${formId}/fields`); + const result = await response.json(); + + if (result.success && result.fields) { + formFields.value = result.fields; + } else { + console.error('Failed to load form fields:', result.error); + formFields.value = []; + } + } catch (error) { + console.error('Error loading form fields:', error); + formFields.value = []; + } finally { + isLoadingFields.value = false; + } +} + +// Computed property for form field options (for FormKit select) +const formFieldOptions = computed(() => { + return formFields.value.map(field => ({ + label: `${field.label} (${field.name})`, + value: field.name, + description: field.description || `${field.type} field` + })); +}); + +// Computed property for process variable options (for FormKit select) +const processVariableOptions = computed(() => { + return props.availableVariables.map(variable => ({ + label: variable.label || `${variable.name} (${variable.scope || 'unknown'})`, + value: variable.name, + description: variable.description || `Type: ${variable.type || 'unknown'}` + })); +}); + // Save changes by emitting them to parent function saveChanges() { // Create a clean copy of the data to avoid reactivity issues - const nodeDataCopy = JSON.parse(JSON.stringify(localNodeData.value)); + const nodeDataCopy = { + ...localNodeData.value, + inputMappings: localNodeData.value.inputMappings ? + localNodeData.value.inputMappings.map(mapping => ({ ...mapping })) : [], + outputMappings: localNodeData.value.outputMappings ? + localNodeData.value.outputMappings.map(mapping => ({ ...mapping })) : [], + fieldConditions: localNodeData.value.fieldConditions ? + localNodeData.value.fieldConditions.map(condition => ({ ...condition })) : [] + }; // Emit the updated data to parent emit('update', nodeDataCopy); } + +// Explicit save function for when the Save button is clicked +function saveAllChanges() { + saveChanges(); +} + +// Expose saveAllChanges so parent components can call it +defineExpose({ + saveAllChanges +}); + +// Add a field condition +function addFieldCondition() { + if (!localNodeData.value.fieldConditions) { + localNodeData.value.fieldConditions = []; + } + + localNodeData.value.fieldConditions.push({ + processVariable: '', + operator: '', + value: '', + targetField: '', + action: '', + description: '' + }); + + saveChanges(); +} + +// Remove a field condition +function removeFieldCondition(index) { + localNodeData.value.fieldConditions.splice(index, 1); + saveChanges(); +} + +// Helper function to safely extract string values from FormKit select options +function getStringValue(value) { + if (typeof value === 'string') { + return value; + } else if (typeof value === 'object' && value.label) { + return value.label; + } else if (typeof value === 'object' && value.value) { + return value.value; + } else { + console.warn('Unexpected value type:', value); + return ''; + } +} \ No newline at end of file diff --git a/components/process-flow/TaskNodeConfiguration.vue b/components/process-flow/TaskNodeConfiguration.vue deleted file mode 100644 index 41c7f13..0000000 --- a/components/process-flow/TaskNodeConfiguration.vue +++ /dev/null @@ -1,391 +0,0 @@ - - - - - \ No newline at end of file diff --git a/components/process-flow/VariableManager.vue b/components/process-flow/VariableManager.vue index 18b8675..b2b77a4 100644 --- a/components/process-flow/VariableManager.vue +++ b/components/process-flow/VariableManager.vue @@ -8,9 +8,9 @@
-

Process Variables

+

Variables

- Define and manage global variables to store and pass data within your process + Define and manage variables to store and pass data within your process

@@ -75,44 +75,89 @@ -
+
-
-
- {{ variable.name }} - - {{ variable.type }} - + +
+
+ +
+ +
+ + +
+

+ {{ variable.name }} +

+
+ + {{ variable.type }} + +
+
-

- {{ variable.description }} -

-
- {{ formatValue(variable.value, variable.type) }} + + +
+ +
-
- - + + +
+

+ {{ variable.description }} +

+
+ + +
+
+
+ + Current Value +
+
+ {{ formatValue(variable.value, variable.type) }} +
+
+
+ + +
+
+
+ + Default Value +
+
+ {{ formatValue(variable.defaultValue, variable.type) }} +
+
@@ -239,8 +284,9 @@ const variableTypes = [ // Computed const variables = computed(() => { - // Only return global variables - return variableStore.getAllVariables.global; + // Return all variables (treating everything as global) + const allVars = variableStore.getAllVariables; + return [...allVars.global, ...allVars.process]; }); // Filtered variables based on search query @@ -298,7 +344,7 @@ const saveVariable = async (formData) => { variableStore.updateVariable( editingVariable.value.name, newVariable, - "global" + 'global' ); } else { // Add new variable @@ -316,14 +362,14 @@ const saveVariable = async (formData) => { // Get badge color based on variable type const getTypeColor = (type) => { switch (type) { - case 'string': return 'blue'; + case 'string': return 'info'; case 'int': - case 'decimal': return 'purple'; - case 'object': return 'emerald'; + case 'decimal': return 'primary'; + case 'object': return 'success'; case 'datetime': - case 'date': return 'amber'; - case 'boolean': return 'indigo'; - default: return 'gray'; + case 'date': return 'warning'; + case 'boolean': return 'secondary'; + default: return 'secondary'; } }; @@ -344,6 +390,27 @@ const formatValue = (value, type) => { return String(value); } }; + +// Get icon based on variable type +const getVariableIcon = (type) => { + switch (type) { + case 'string': + return 'material-symbols:text-fields'; + case 'int': + case 'decimal': + return 'material-symbols:pin'; + case 'boolean': + return 'material-symbols:toggle-on'; + case 'date': + return 'material-symbols:calendar-today'; + case 'datetime': + return 'material-symbols:schedule'; + case 'object': + return 'material-symbols:data-object'; + default: + return 'material-symbols:data-object'; + } +};