From 577128a7991aacb160107cfaf7f4d8b05742a521 Mon Sep 17 00:00:00 2001 From: Afiq Date: Wed, 6 Aug 2025 18:31:56 +0800 Subject: [PATCH] Optimize Conditional Logic Handling and Introduce Demo Component - Refactored ConditionalLogicEngine.vue to optimize conditional logic script generation by grouping handlers for watched fields, reducing duplicate event listeners and improving performance. - Added helper functions for generating condition checks and action codes, enhancing code readability and maintainability. - Introduced ConditionalLogicTestDemo.vue to demonstrate the benefits of optimization, showcasing before and after examples of conditional logic handling. - Updated FormBuilderFieldSettingsModal.vue to include notes on optimization when multiple fields watch the same trigger field, improving user awareness of performance enhancements. - Enhanced ComponentPreview.vue and workflow pages to support preview mode for conditional logic, ensuring consistent behavior across the application. --- components/ComponentPreview.vue | 42 +++-- components/ConditionalLogicEngine.vue | 172 +++++++++++++------ components/ConditionalLogicTestDemo.vue | 108 ++++++++++++ components/FormBuilderFieldSettingsModal.vue | 74 ++++++-- pages/form-builder/index.vue | 30 +++- pages/workflow/[id].vue | 2 +- server/api/process/[id]/start.post.js | 48 +----- 7 files changed, 335 insertions(+), 141 deletions(-) create mode 100644 components/ConditionalLogicTestDemo.vue diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue index 892dbd2..c011a20 100644 --- a/components/ComponentPreview.vue +++ b/components/ComponentPreview.vue @@ -26,8 +26,8 @@ -
+

Item 1

@@ -168,7 +168,7 @@
- +
@@ -250,7 +250,7 @@
-
+
- +
- + +
+

+ {{ component.props.emptyMessage || 'This section is empty' }} +

+
@@ -728,7 +733,7 @@ watch(() => props.component.props.minItems, (newMinItems, oldMinItems) => { // Repeating group and dynamic list functionality const addGroupItem = () => { - if (props.isPreview) return; + if (!props.isPreview) return; const groupName = props.component.props.name; if (!groupName) return; @@ -758,7 +763,7 @@ const addGroupItem = () => { }; const removeGroupItem = (index) => { - if (props.isPreview) return; + if (!props.isPreview) return; const groupName = props.component.props.name; if (!groupName) return; @@ -785,7 +790,7 @@ const removeGroupItem = (index) => { }; const addListItem = () => { - if (props.isPreview) return; + if (!props.isPreview) return; const listName = props.component.props.name; if (!listName) return; @@ -802,7 +807,7 @@ const addListItem = () => { }; const removeListItem = (index) => { - if (props.isPreview) return; + if (!props.isPreview) return; const listName = props.component.props.name; if (!listName) return; @@ -1060,7 +1065,7 @@ const showImportModal = (listName) => { // Handle button click const handleButtonClick = () => { - if (props.isPreview) return; + if (!props.isPreview) return; // Execute custom onClick code if provided if (props.component.props.onClick) { @@ -1159,12 +1164,7 @@ const handleImageClick = (event) => { // Compute style based on grid properties const componentStyle = computed(() => { - // Only apply grid styles in the non-preview mode (actual form) - if (props.isPreview) { - return {}; // Styling is handled by parent in canvas mode - } - - // Apply grid column in preview mode + // Apply grid column styling in both preview and edit modes const gridColumn = props.component.props.gridColumn || 'span 12'; return { @@ -1634,6 +1634,12 @@ const getButtonSizeClass = (size) => { padding: 1rem; } +.empty-section-preview { + border: 1px dashed #e5e7eb; + border-radius: 0.375rem; + background-color: #fafafa; +} + .section-drop-zone { padding: 1rem; min-height: 120px; diff --git a/components/ConditionalLogicEngine.vue b/components/ConditionalLogicEngine.vue index b9f28cd..5fde45e 100644 --- a/components/ConditionalLogicEngine.vue +++ b/components/ConditionalLogicEngine.vue @@ -41,6 +41,56 @@ const getJavaScriptOperator = (operator) => { // Generate conditional logic script from form components const generateConditionalLogicScript = () => { const scriptsArray = [] + const fieldWatchers = new Map() // Group by watched field to avoid duplicates + const initialEvaluations = [] + + // Helper function to generate condition check + const generateConditionCheck = (condition) => { + const { field, operator: condOp, value } = condition + + switch (condOp) { + case 'equals': + return `getField('${field}') === '${value}'` + case 'not_equals': + return `getField('${field}') !== '${value}'` + case 'contains': + return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` + case 'not_contains': + return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` + case 'is_empty': + return `!getField('${field}') || getField('${field}') === ''` + case 'is_not_empty': + return `getField('${field}') && getField('${field}') !== ''` + case 'greater_than': + return `Number(getField('${field}')) > ${Number(value) || 0}` + case 'less_than': + return `Number(getField('${field}')) < ${Number(value) || 0}` + default: + return `getField('${field}') === '${value}'` + } + } + + // Helper function to generate action code + const generateActionCode = (action, fieldName) => { + switch (action) { + case 'show': return `showField('${fieldName}')` + case 'hide': return `hideField('${fieldName}')` + case 'enable': return `enableField('${fieldName}')` + case 'disable': return `disableField('${fieldName}')` + default: return `showField('${fieldName}')` + } + } + + // Helper function to generate inverse action code + const generateInverseActionCode = (action, fieldName) => { + switch (action) { + case 'show': return `hideField('${fieldName}')` + case 'hide': return `showField('${fieldName}')` + case 'enable': return `disableField('${fieldName}')` + case 'disable': return `enableField('${fieldName}')` + default: return `hideField('${fieldName}')` + } + } // Process each component that has conditional logic enabled props.formComponents.forEach(component => { @@ -56,73 +106,81 @@ const generateConditionalLogicScript = () => { if (!fieldName) return // Generate condition checks - const conditionChecks = conditions.map(condition => { - const { field, operator: condOp, value } = condition - - switch (condOp) { - case 'equals': - return `getField('${field}') === '${value}'` - case 'not_equals': - return `getField('${field}') !== '${value}'` - case 'contains': - return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` - case 'not_contains': - return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` - case 'is_empty': - return `!getField('${field}') || getField('${field}') === ''` - case 'is_not_empty': - return `getField('${field}') && getField('${field}') !== ''` - case 'greater_than': - return `Number(getField('${field}')) > ${Number(value) || 0}` - case 'less_than': - return `Number(getField('${field}')) < ${Number(value) || 0}` - default: - return `getField('${field}') === '${value}'` - } - }).join(` ${getJavaScriptOperator(operator)} `) + const conditionChecks = conditions.map(generateConditionCheck).join(` ${getJavaScriptOperator(operator)} `) // Generate action functions - const actionCode = action === 'show' ? `showField('${fieldName}')` : - action === 'hide' ? `hideField('${fieldName}')` : - action === 'enable' ? `enableField('${fieldName}')` : - action === 'disable' ? `disableField('${fieldName}')` : - `showField('${fieldName}')` + const actionCode = generateActionCode(action, fieldName) + const inverseActionCode = generateInverseActionCode(action, fieldName) - const inverseActionCode = action === 'show' ? `hideField('${fieldName}')` : - action === 'hide' ? `showField('${fieldName}')` : - action === 'enable' ? `disableField('${fieldName}')` : - action === 'disable' ? `enableField('${fieldName}')` : - `hideField('${fieldName}')` - - // Generate field change listeners + // Get all watched fields for this component const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))] + // Group logic by watched field to avoid duplicates watchedFields.forEach(watchField => { - const script = ` -// Conditional logic for field: ${fieldName} -onFieldChange('${watchField}', function() { - if (${conditionChecks}) { - ${actionCode}; - } else { - ${inverseActionCode}; - } -});` - scriptsArray.push(script) + if (!fieldWatchers.has(watchField)) { + fieldWatchers.set(watchField, { + fieldName: watchField, + logicBlocks: [] + }) + } + + // Add this component's logic to the watched field + fieldWatchers.get(watchField).logicBlocks.push({ + targetField: fieldName, + conditionChecks, + actionCode, + inverseActionCode, + componentType: component.type + }) }) - // Also add initial evaluation - const initialScript = ` -// Initial evaluation for field: ${fieldName} -(function() { - if (${conditionChecks}) { - ${actionCode}; - } else { - ${inverseActionCode}; - } -})();` - scriptsArray.push(initialScript) + // Add initial evaluation + initialEvaluations.push({ + targetField: fieldName, + conditionChecks, + actionCode, + inverseActionCode, + componentType: component.type + }) }) + // Generate optimized field change handlers (one per watched field) + fieldWatchers.forEach((watcher, watchField) => { + const logicBlocks = watcher.logicBlocks.map(block => ` + // Logic for ${block.componentType} field: ${block.targetField} + if (${block.conditionChecks}) { + ${block.actionCode}; + } else { + ${block.inverseActionCode}; + }`).join('') + + const script = ` +// Optimized field change handler for: ${watchField} +// Handles ${watcher.logicBlocks.length} dependent field(s): ${watcher.logicBlocks.map(b => b.targetField).join(', ')} +onFieldChange('${watchField}', function() {${logicBlocks} +});` + + scriptsArray.push(script) + }) + + // Generate initial evaluations + if (initialEvaluations.length > 0) { + const initialLogicBlocks = initialEvaluations.map(block => ` + // Initial evaluation for ${block.componentType} field: ${block.targetField} + if (${block.conditionChecks}) { + ${block.actionCode}; + } else { + ${block.inverseActionCode}; + }`).join('') + + const initialScript = ` +// Initial conditional logic evaluation +(function() {${initialLogicBlocks} +})();` + + scriptsArray.push(initialScript) + } + return scriptsArray.join('\n\n') } diff --git a/components/ConditionalLogicTestDemo.vue b/components/ConditionalLogicTestDemo.vue new file mode 100644 index 0000000..b152bbe --- /dev/null +++ b/components/ConditionalLogicTestDemo.vue @@ -0,0 +1,108 @@ + + + + + \ No newline at end of file diff --git a/components/FormBuilderFieldSettingsModal.vue b/components/FormBuilderFieldSettingsModal.vue index 0045192..48b3eab 100644 --- a/components/FormBuilderFieldSettingsModal.vue +++ b/components/FormBuilderFieldSettingsModal.vue @@ -2787,8 +2787,22 @@ const generateConditionalLogicCode = () => { const { conditions, action, operator } = configModel.value.conditionalLogic const fieldName = configModel.value.name || 'this_field' - // Generate condition checks - const conditionChecks = conditions.map(condition => { + // Helper function to map operators to valid JavaScript + const getJavaScriptOperator = (operator) => { + switch (operator) { + case 'and': + case 'AND': + return '&&' + case 'or': + case 'OR': + return '||' + default: + return '&&' // Default to AND if unknown operator + } + } + + // Helper function to generate condition check + const generateConditionCheck = (condition) => { const { field, operator: condOp, value } = condition switch (condOp) { @@ -2797,9 +2811,9 @@ const generateConditionalLogicCode = () => { case 'not_equals': return `getField('${field}') !== '${value}'` case 'contains': - return `String(getField('${field}')).includes('${value}')` + return `String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` case 'not_contains': - return `!String(getField('${field}')).includes('${value}')` + return `!String(getField('${field}') || '').toLowerCase().includes('${value}'.toLowerCase())` case 'is_empty': return `!getField('${field}') || getField('${field}') === ''` case 'is_not_empty': @@ -2811,32 +2825,56 @@ const generateConditionalLogicCode = () => { default: return `getField('${field}') === '${value}'` } - }).join(` ${operator} `) + } - // Generate action - const actionCode = action === 'show' ? `showField('${fieldName}')` : - action === 'hide' ? `hideField('${fieldName}')` : - action === 'enable' ? `enableField('${fieldName}')` : - action === 'disable' ? `disableField('${fieldName}')` : - `showField('${fieldName}')` + // Helper function to generate action code + const generateActionCode = (action, fieldName) => { + switch (action) { + case 'show': return `showField('${fieldName}')` + case 'hide': return `hideField('${fieldName}')` + case 'enable': return `enableField('${fieldName}')` + case 'disable': return `disableField('${fieldName}')` + default: return `showField('${fieldName}')` + } + } - // Generate field change listeners + // Helper function to generate inverse action code + const generateInverseActionCode = (action, fieldName) => { + switch (action) { + case 'show': return `hideField('${fieldName}')` + case 'hide': return `showField('${fieldName}')` + case 'enable': return `disableField('${fieldName}')` + case 'disable': return `enableField('${fieldName}')` + default: return `hideField('${fieldName}')` + } + } + + // Generate condition checks + const conditionChecks = conditions.map(generateConditionCheck).join(` ${getJavaScriptOperator(operator)} `) + + // Generate action functions + const actionCode = generateActionCode(action, fieldName) + const inverseActionCode = generateInverseActionCode(action, fieldName) + + // Generate field change listeners - note about optimization const watchedFields = [...new Set(conditions.map(c => c.field).filter(Boolean))] + + // Add helpful comment about optimization when multiple fields watch the same condition + const optimizationNote = watchedFields.length > 1 ? + `// NOTE: When multiple fields use the same trigger field, the FormBuilder automatically\n// optimizes by grouping these handlers to avoid duplicate onFieldChange listeners.\n\n` : + '' + const listenerCode = watchedFields.map(field => `onFieldChange('${field}', function() { if (${conditionChecks}) { ${actionCode}; } else { - ${action === 'show' ? `hideField('${fieldName}')` : - action === 'hide' ? `showField('${fieldName}')` : - action === 'enable' ? `disableField('${fieldName}')` : - action === 'disable' ? `enableField('${fieldName}')` : - `hideField('${fieldName}')`}; + ${inverseActionCode}; } });` ).join('\n\n') - return listenerCode || '// Invalid configuration' + return optimizationNote + (listenerCode || '// Invalid configuration') } const handleSave = () => { diff --git a/pages/form-builder/index.vue b/pages/form-builder/index.vue index 44cdda7..3f89b76 100644 --- a/pages/form-builder/index.vue +++ b/pages/form-builder/index.vue @@ -296,7 +296,7 @@ v-for="(component, index) in formStore.formComponents" :key="index" > - +
@@ -904,6 +904,20 @@
+ +
+
+ ⚡ Conditional Logic Optimization +
+
Automatic Optimization: When multiple fields use onFieldChange with the same trigger field:
+
• The form builder automatically groups them into a single handler
+
• No need to worry about duplicate event listeners
+
• Better performance and memory usage
+
• Use field settings UI for visual condition setup, or write custom logic here
+
+
+
+
@@ -980,6 +994,20 @@
+ + +
+
+ ⚡ Performance Optimization +
+
Smart Field Watching: The form builder automatically optimizes conditional logic
+
• Groups multiple field conditions that watch the same trigger field
+
• Prevents duplicate onFieldChange handlers
+
• Improves performance and reduces memory usage
+
• Example: If 5 fields depend on "customer_type", only 1 handler is created
+
+
+
{