From 82bbc9ff5792280983233496bea36eea97a14772 Mon Sep 17 00:00:00 2001 From: Afiq Date: Tue, 20 May 2025 14:07:51 +0800 Subject: [PATCH] Enhance Process Flow Node Configuration and UI - Updated ApiNodeConfiguration, BusinessRuleNodeConfiguration, and FormNodeConfiguration components to improve user experience with clearer layouts and enhanced styling. - Introduced Quick Reference Guides in modals for better user guidance during configuration. - Refined VariableManager with search functionality and improved variable display, allowing for easier management of process variables. - Enhanced GatewayConditionManager and related modals to support clearer decision path configurations and default path management. - Improved overall UI consistency across components, ensuring a more cohesive look and feel in the process builder. --- .../process-flow/ApiNodeConfiguration.vue | 511 ++++++++------- .../ApiNodeConfigurationModal.vue | 47 +- .../BusinessRuleNodeConfiguration.vue | 603 ++++++++++-------- .../BusinessRuleNodeConfigurationModal.vue | 47 +- .../process-flow/FormNodeConfiguration.vue | 359 +++++++---- .../FormNodeConfigurationModal.vue | 47 +- .../process-flow/GatewayConditionManager.vue | 434 +++++++------ .../GatewayConditionManagerModal.vue | 138 +++- components/process-flow/ProcessFlowNodes.js | 222 +++++-- .../process-flow/TaskNodeConfiguration.vue | 4 +- components/process-flow/VariableManager.vue | 206 ++++-- pages/process-builder/index.vue | 15 +- 12 files changed, 1691 insertions(+), 942 deletions(-) diff --git a/components/process-flow/ApiNodeConfiguration.vue b/components/process-flow/ApiNodeConfiguration.vue index c805a99..023f308 100644 --- a/components/process-flow/ApiNodeConfiguration.vue +++ b/components/process-flow/ApiNodeConfiguration.vue @@ -1,228 +1,276 @@ @@ -21,6 +54,7 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/components/process-flow/BusinessRuleNodeConfiguration.vue b/components/process-flow/BusinessRuleNodeConfiguration.vue index 2cd4e64..c94e108 100644 --- a/components/process-flow/BusinessRuleNodeConfiguration.vue +++ b/components/process-flow/BusinessRuleNodeConfiguration.vue @@ -1,59 +1,104 @@ @@ -320,6 +406,7 @@ import { ref, computed, onMounted, watch } from 'vue'; import { useVariableStore } from '~/stores/variableStore'; import { DateTime } from 'luxon'; +import { Icon } from '#components'; const props = defineProps({ nodeId: { @@ -668,40 +755,32 @@ const updateConditionOperator = (groupIndex, condIndex) => { \ No newline at end of file diff --git a/components/process-flow/BusinessRuleNodeConfigurationModal.vue b/components/process-flow/BusinessRuleNodeConfigurationModal.vue index 8005f56..66bf7a5 100644 --- a/components/process-flow/BusinessRuleNodeConfigurationModal.vue +++ b/components/process-flow/BusinessRuleNodeConfigurationModal.vue @@ -2,20 +2,53 @@ @@ -23,6 +56,7 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/components/process-flow/FormNodeConfiguration.vue b/components/process-flow/FormNodeConfiguration.vue index a30d4e2..b9c5e59 100644 --- a/components/process-flow/FormNodeConfiguration.vue +++ b/components/process-flow/FormNodeConfiguration.vue @@ -26,55 +26,222 @@ > --> - -
- + +
+
+
+ 1 +
+

Form Selection

+
+ +
+
+

+ Select an existing form or create a new one to use in this task. +

+ +
+ +
+
+
- -
-
-

Form Data Mapping

+ +
+
+
+ 2 +
+

Form Data Mapping

+
+ +

+ Configure how data flows between your process and the form. +

- -
-
- - + +
+
+
+
+ + + + Input Variables (Process → Form) +
+

Map process variables to pre-fill form fields

- -
+ + Add Mapping + +
+ + +
+

No input mappings defined. Form will not be prefilled with process data. -

- -
-
-
-
Mapping #{{ index + 1 }}
- +

+ + Add First Mapping + +
+ +
+
+
+
+ + Process Variable to Form Field #{{ index + 1 }} +
+ +
+ +
+
+ + +

+ The source variable containing the data +

-
-
- +
+ + +

+ The target field ID in the form +

+
+
+
+
+
+ + +
+
+
+
+ + + + Output Variables (Form → Process) +
+

Store form submission data in process variables

+
+ + Add Mapping + +
+ + +
+

+ No output mappings defined. Form data will not be stored in process variables. +

+ + Add First Mapping + +
+ +
+
+
+
+ + Form Field to Process Variable #{{ index + 1 }} +
+ +
+ +
+
+ + +

+ The source field ID in the form +

+
+ +
+ +
+ + +
- -
- - -
-
-
-
-
- - -
-
- - -
- -
- No output mappings defined. Form data will not be stored in process variables. -
- -
-
-
-
Mapping #{{ index + 1 }}
- -
- -
-
- - -
- -
- -
- - -
-
+

+ The target variable to store form data +

@@ -181,6 +277,7 @@ import { ref, computed, watch, nextTick } from 'vue'; import { useVariableStore } from '@/stores/variableStore'; import FormSelector from './FormSelector.vue'; +import { Icon } from '#components'; const props = defineProps({ nodeData: { @@ -350,10 +447,10 @@ function saveChanges() { \ No newline at end of file diff --git a/components/process-flow/FormNodeConfigurationModal.vue b/components/process-flow/FormNodeConfigurationModal.vue index a6b993c..3a59ca0 100644 --- a/components/process-flow/FormNodeConfigurationModal.vue +++ b/components/process-flow/FormNodeConfigurationModal.vue @@ -2,18 +2,51 @@ @@ -21,6 +54,7 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/components/process-flow/GatewayConditionManager.vue b/components/process-flow/GatewayConditionManager.vue index 9893c4e..d6dd2c5 100644 --- a/components/process-flow/GatewayConditionManager.vue +++ b/components/process-flow/GatewayConditionManager.vue @@ -306,204 +306,267 @@ const groupedConditionText = (group) => { return `${c.operator.toUpperCase()} ${c.text}`; }).join(' '); }; + +// Add the following helper method to the existing script +const getValuePlaceholder = (condition) => { + const varType = condition.valueType?.toLowerCase(); + if (varType === 'number') return 'Enter a number'; + if (varType === 'date') return 'Select a date'; + if (varType === 'string') return 'Enter text value'; + return 'Enter value'; +}; + +// Add a method to get friendly path summary +const getPathSummary = (group) => { + if (!group.conditions || group.conditions.length === 0) { + return 'No conditions defined'; + } + + if (group.conditions.length === 1) { + return conditionText(group.conditions[0]); + } + + // Build readable text with individual operators + return group.conditions.map((condition, index) => { + const text = conditionText(condition); + if (index === 0) return text; + return `${condition.logicalOperator.toUpperCase()} ${text}`; + }).join(' '); +}; @@ -25,6 +98,7 @@ \ No newline at end of file + +function addNewPath() { + // Create a default new path + const defaultVar = props.availableVariables.length > 0 ? props.availableVariables[0] : null; + + if (!defaultVar) { + alert('No variables available. Please add process variables before creating conditions.'); + return; + } + + const newConditionGroup = { + id: `condition-group-${Date.now()}`, + output: `Path ${localConditions.value.length + 1}`, // Auto-numbered path + conditions: [ + { + id: `condition-${Date.now()}`, + variable: defaultVar.name, + operator: 'eq', // Default to equals + value: '', + valueType: defaultVar.type || 'string', + logicalOperator: 'and' + } + ] + }; + + localConditions.value.push(newConditionGroup); + emit('update:conditions', [...localConditions.value]); +} + + + \ No newline at end of file diff --git a/components/process-flow/ProcessFlowNodes.js b/components/process-flow/ProcessFlowNodes.js index 0f0a7ed..ee3ee1c 100644 --- a/components/process-flow/ProcessFlowNodes.js +++ b/components/process-flow/ProcessFlowNodes.js @@ -108,15 +108,28 @@ export const TaskNode = markRaw({ }; return priorityColors[this.data.priority] || ''; + }, + + // Helper to get priority label + priorityLabel() { + if (!this.data || !this.data.priority) return 'None'; + return this.data.priority.charAt(0).toUpperCase() + this.data.priority.slice(1); + }, + + // Helper for due date + dueLabel() { + if (!this.data || !this.data.dueDateType || this.data.dueDateType === 'none') { + return 'Not set'; + } + + if (this.data.dueDateType === 'fixed') { + return `${this.data.dueDateDays || 0} days`; + } + + return `Variable: ${this.data.dueDateVariable || 'none'}`; } }, render() { - const badgeContent = this.data?.priority ? - h('span', { - class: `node-badge px-1 text-xs rounded ${this.priorityClass} bg-gray-100` - }, this.data.priority.charAt(0).toUpperCase() + this.data.priority.slice(1)) : - null; - return h(CustomNode, { id: this.id, type: 'task', @@ -126,21 +139,22 @@ export const TaskNode = markRaw({ onClick: () => this.$emit('node-click', this.id) }, { icon: () => h('i', { class: 'material-icons text-blue-500' }, 'assignment'), - badge: () => badgeContent, default: () => h('div', { class: 'node-details' }, [ h('p', { class: 'node-description' }, this.data?.description || 'A general task'), - h('div', { class: 'node-assignee flex items-center justify-between text-xs' }, [ - h('span', { class: 'node-assignee-label' }, 'Assigned:'), - h('span', { class: 'node-assignee-value ml-1 font-medium text-blue-600' }, this.assignmentText) + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Assigned:'), + h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.assignmentText) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Priority:'), + h('span', { + class: `node-rule-detail-value ml-1 font-medium ${this.priorityClass}` + }, this.priorityLabel) ]), this.data?.dueDateType !== 'none' && this.data?.dueDateType ? - h('div', { class: 'node-due-date text-xs mt-1' }, [ - h('span', { class: 'node-due-date-label' }, 'Due:'), - h('span', { class: 'node-due-date-value ml-1' }, - this.data.dueDateType === 'fixed' ? - `${this.data.dueDateDays || 0} days` : - `Variable: ${this.data.dueDateVariable || 'none'}` - ) + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Due:'), + h('span', { class: 'node-rule-detail-value ml-1 font-medium text-blue-600' }, this.dueLabel) ]) : null ]) }); @@ -224,14 +238,13 @@ export const GatewayNode = markRaw({ .join(', '); return paths || 'Unconfigured paths'; + }, + + defaultPath() { + return this.data?.defaultPath || 'Default'; } }, render() { - // Create the badge content - const badgeContent = h('span', { - class: 'node-badge bg-orange-100 text-orange-600 px-1 text-xs rounded' - }, `${this.totalPaths} path${this.totalPaths !== 1 ? 's' : ''}`); - return h(CustomNode, { id: this.id, type: 'gateway', @@ -241,12 +254,19 @@ export const GatewayNode = markRaw({ 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('p', { class: 'node-description' }, this.data?.description || 'Decision based on conditions'), - h('div', { class: 'node-conditions flex items-center justify-between text-xs' }, [ - h('span', { class: 'node-conditions-label' }, 'Paths:'), - h('span', { class: 'node-conditions-value ml-1 font-medium text-orange-600' }, this.conditionSummary) + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Paths:'), + h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' }, + this.totalPaths === 0 ? 'None' : this.totalPaths + ) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Default:'), + h('span', { class: 'node-rule-detail-value ml-1 font-medium text-orange-600' }, + this.defaultPath + ) ]) ]) }); @@ -269,10 +289,6 @@ export const FormNode = markRaw({ } }, 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', @@ -282,14 +298,19 @@ export const FormNode = markRaw({ 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('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Form:'), h('span', { - class: this.hasForm ? 'node-form-value text-purple-600 font-medium' : 'node-form-value text-gray-400 italic' + class: this.hasForm ? 'node-rule-detail-value ml-1 font-medium text-purple-600' : 'node-rule-detail-value ml-1 italic text-gray-400' }, this.formName) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Status:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium text-purple-600' + }, this.hasForm ? 'Configured' : 'Not configured') ]) ]) }); @@ -303,6 +324,12 @@ export const ScriptNode = markRaw({ nodeLabel() { // Get label from either prop or data, with fallback return this.label || (this.data && this.data.label) || 'Script'; + }, + scriptLanguage() { + return this.data?.language || 'Not specified'; + }, + hasScript() { + return !!this.data?.script; } }, render() { @@ -314,12 +341,18 @@ export const ScriptNode = markRaw({ data: this.data, onClick: () => this.$emit('node-click', this.id) }, { - icon: () => h('i', { class: 'material-icons script-icon' }, 'code'), + icon: () => h('i', { class: 'material-icons text-gray-500' }, 'code'), default: () => h('div', { class: 'node-details' }, [ h('p', { class: 'node-description' }, this.data?.description || 'Script execution'), - h('div', { class: 'node-script-info' }, [ - h('span', { class: 'node-script-label' }, 'Language: '), - h('span', { class: 'node-script-value' }, this.data?.language || 'Not specified') + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Language:'), + h('span', { class: 'node-rule-detail-value ml-1 font-medium text-gray-600' }, this.scriptLanguage) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Script:'), + h('span', { + class: 'node-rule-detail-value ml-1 font-medium text-gray-600' + }, this.hasScript ? 'Defined' : 'Not defined') ]) ]) }); @@ -338,13 +371,12 @@ export const ApiCallNode = markRaw({ }, apiMethod() { return this.data?.apiMethod || 'GET'; + }, + isConfigured() { + return !!this.data?.apiUrl; } }, render() { - const badgeContent = h('span', { - class: 'node-badge bg-indigo-100 text-indigo-600 px-1 text-xs rounded' - }, 'API'); - return h(CustomNode, { id: this.id, type: 'api', @@ -354,20 +386,20 @@ export const ApiCallNode = markRaw({ onClick: () => this.$emit('node-click', this.id) }, { icon: () => h('i', { class: 'material-icons text-indigo-500' }, 'api'), - badge: () => badgeContent, default: () => h('div', { class: 'node-details' }, [ h('p', { class: 'node-description' }, this.data?.description || 'External API call'), - h('div', { class: 'node-api-info' }, [ - h('span', { class: 'node-api-label' }, 'URL: '), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'Method:'), h('span', { - class: 'node-api-value text-indigo-600 font-medium' - }, this.apiUrl) - ]), - h('div', { class: 'node-api-method-info' }, [ - h('span', { class: 'node-api-method-label' }, 'Method: '), - h('span', { - class: 'node-api-method-value' + class: 'node-rule-detail-value ml-1 font-medium text-indigo-600' }, this.apiMethod) + ]), + h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [ + h('span', { class: 'node-rule-detail-label' }, 'URL:'), + h('span', { + class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-indigo-600' : 'node-rule-detail-value ml-1 italic text-gray-400', + style: 'max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;' + }, this.apiUrl) ]) ]) }); @@ -769,28 +801,31 @@ export const nodeStyles = ` display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + max-width: 90px; } -.node-gateway .node-conditions { +.node-gateway .node-rule-detail { display: flex; font-size: 10px; color: #666; align-items: center; justify-content: center; width: 100%; + margin-bottom: 3px; } -.node-gateway .node-conditions-label { +.node-gateway .node-rule-detail-label { font-weight: 500; margin-right: 4px; } -.node-gateway .node-conditions-value { +.node-gateway .node-rule-detail-value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 10px; - max-width: 80px; + max-width: 60px; + text-align: center; } .node-gateway .material-icons { @@ -798,4 +833,79 @@ export const nodeStyles = ` color: #f97316; margin-bottom: 4px; } + +/* Update node-specific styles to be more consistent */ +.node-form { + width: 180px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 50px; + display: flex; + flex-direction: column; + padding: 0; + border: 1px solid #ddd; + border-left: 4px solid #9333ea; /* Purple border to match icon color */ +} + +.node-api { + width: 180px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 50px; + display: flex; + flex-direction: column; + padding: 0; + border: 1px solid #ddd; + border-left: 4px solid #6366f1; /* Indigo border to match icon color */ +} + +/* Node details styles for consistency */ +.node-rule-detail { + display: flex; + font-size: 10px; + color: #666; + align-items: center; + margin-bottom: 2px; +} + +.node-rule-detail-label { + font-weight: 500; + margin-right: 4px; +} + +.node-rule-detail-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Add task node specific styling to be consistent with business rule */ +.node-task { + width: 180px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 50px; + display: flex; + flex-direction: column; + padding: 0; + border: 1px solid #ddd; + border-left: 4px solid #3b82f6; /* Blue border to match icon color */ +} + +/* Script node styling */ +.node-script { + width: 180px; + background: white; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-height: 50px; + display: flex; + flex-direction: column; + padding: 0; + border: 1px solid #ddd; + border-left: 4px solid #6b7280; /* Gray border to match icon color */ +} `; \ No newline at end of file diff --git a/components/process-flow/TaskNodeConfiguration.vue b/components/process-flow/TaskNodeConfiguration.vue index cae4c64..41c7f13 100644 --- a/components/process-flow/TaskNodeConfiguration.vue +++ b/components/process-flow/TaskNodeConfiguration.vue @@ -353,10 +353,10 @@ function saveChanges() { diff --git a/pages/process-builder/index.vue b/pages/process-builder/index.vue index 6757125..d836a0d 100644 --- a/pages/process-builder/index.vue +++ b/pages/process-builder/index.vue @@ -323,8 +323,9 @@ const handleConditionUpdate = (conditions) => { 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 + + // If no matching group found, and this isn't the default path, update edge label + if (!matchingGroup && edge.label !== selectedNodeData.value.data.defaultPath) { return { ...edge, label: selectedNodeData.value.data.defaultPath || 'Default' @@ -708,6 +709,14 @@ const handleBusinessRuleUpdate = (data) => { updateNodeInStore(); } }; + +// Add a method to handle default path updates +const handleDefaultPathUpdate = (path) => { + if (selectedNodeData.value && selectedNodeData.value.type === 'gateway') { + selectedNodeData.value.data.defaultPath = path; + updateNodeInStore(); + } +};