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.
This commit is contained in:
Afiq 2025-08-06 18:31:56 +08:00
parent 8a6f87ebf1
commit 577128a799
7 changed files with 335 additions and 141 deletions

View File

@ -26,8 +26,8 @@
<FormKit v-else-if="isInputType" :id="`preview-${component.id}`" :type="component.type" :name="component.props.name"
:label="component.props.label" :help="component.props.help" :placeholder="component.props.placeholder"
:validation="component.props.validation" :validation-visibility="isPreview ? 'live' : 'blur'"
:readonly="component.props.readonly || isPreview"
:disabled="isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
:readonly="component.props.readonly || !isPreview"
:disabled="!isPreview || (component.props.readonly && ['select', 'checkbox', 'radio'].includes(component.type))"
:options="component.props.options || undefined" :value="component.props.value || undefined"
:accept="component.props.accept || undefined" :max="component.props.max || undefined"
:mask="component.props.mask || undefined" :digits="component.props.digits || undefined"
@ -144,7 +144,7 @@
</div>
<!-- Default group preview (in edit mode) -->
<div v-if="isPreview" class="repeating-groups space-y-4">
<div v-if="!isPreview" class="repeating-groups space-y-4">
<div class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
<div class="flex justify-between items-center mb-3">
<h4 class="text-sm font-medium text-gray-700">Item 1</h4>
@ -168,7 +168,7 @@
</button>
</div>
<!-- Functional groups (in form view) -->
<!-- Functional groups (in preview mode) -->
<div v-else class="repeating-groups space-y-4">
<div v-for="(group, groupIndex) in (safeGetField(component.props.name, previewFormData) || [])" :key="groupIndex"
class="group-item border border-gray-200 rounded-md p-3 bg-gray-50">
@ -250,7 +250,7 @@
</div>
<div class="space-y-2">
<div v-if="isPreview" class="list-items space-y-2">
<div v-if="!isPreview" class="list-items space-y-2">
<div v-for="(item, index) in component.props.defaultItems" :key="index" class="flex items-center">
<input :type="component.props.itemType || 'text'" :value="item" :placeholder="component.props.placeholder"
disabled
@ -263,7 +263,7 @@
</button>
</div>
<!-- Add button for preview -->
<!-- Add button for edit mode -->
<button type="button"
class="inline-flex items-center px-3 py-1.5 border border-blue-600 text-blue-600 bg-white hover:bg-blue-50 rounded text-sm">
<Icon name="material-symbols:add-circle-outline" class="w-4 h-4 mr-1" />
@ -464,7 +464,12 @@
<component-preview :component="childComponent" :is-preview="true" />
</template>
</div>
<!-- No placeholder in preview mode when empty -->
<!-- Show subtle indication for empty sections in preview mode (optional) -->
<div v-else-if="component.props.showEmptyMessage !== false" class="empty-section-preview">
<p class="text-sm text-gray-400 text-center py-4">
{{ component.props.emptyMessage || 'This section is empty' }}
</p>
</div>
</div>
<!-- In form builder mode, show drop zone -->
@ -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;

View File

@ -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')
}

View File

@ -0,0 +1,108 @@
<template>
<div class="conditional-logic-demo p-6 bg-gray-50 rounded-lg">
<h3 class="text-lg font-semibold mb-4">Conditional Logic Optimization Demo</h3>
<!-- Before Optimization Example -->
<div class="mb-6">
<h4 class="text-md font-medium text-red-600 mb-2"> Before Optimization (Duplicate Handlers)</h4>
<div class="bg-red-50 p-4 rounded border border-red-200">
<pre class="text-sm text-red-800 whitespace-pre-wrap">{{ beforeOptimization }}</pre>
</div>
</div>
<!-- After Optimization Example -->
<div class="mb-6">
<h4 class="text-md font-medium text-green-600 mb-2"> After Optimization (Grouped Handlers)</h4>
<div class="bg-green-50 p-4 rounded border border-green-200">
<pre class="text-sm text-green-800 whitespace-pre-wrap">{{ afterOptimization }}</pre>
</div>
</div>
<!-- Benefits -->
<div class="bg-blue-50 p-4 rounded border border-blue-200">
<h4 class="text-md font-medium text-blue-800 mb-2">🚀 Benefits of Optimization</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li> <strong>Reduced Memory Usage:</strong> Only one handler per watched field instead of multiple duplicates</li>
<li> <strong>Better Performance:</strong> Fewer function calls when field values change</li>
<li> <strong>Cleaner Code:</strong> Grouped logic is easier to read and maintain</li>
<li> <strong>No Conflicts:</strong> Eliminates potential conflicts between duplicate handlers</li>
</ul>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
// Example scenario: 3 fields (field2, field3, field4) all depend on field1
const beforeOptimization = computed(() => `// BEFORE: Multiple duplicate handlers for the same field
// This creates performance issues and potential conflicts
// Conditional logic for field: field2
onFieldChange('field1', function() {
if (getField('field1') === 'show') {
showField('field2');
} else {
hideField('field2');
}
});
// Conditional logic for field: field3
onFieldChange('field1', function() {
if (getField('field1') === 'show') {
showField('field3');
} else {
hideField('field3');
}
});
// Conditional logic for field: field4
onFieldChange('field1', function() {
if (getField('field1') === 'show') {
showField('field4');
} else {
hideField('field4');
}
});
// Problem: 3 duplicate handlers listening to the same field!`)
const afterOptimization = computed(() => `// AFTER: Single optimized handler for all dependent fields
// Much more efficient and prevents conflicts
// Optimized field change handler for: field1
// Handles 3 dependent field(s): field2, field3, field4
onFieldChange('field1', function() {
// Logic for text field: field2
if (getField('field1') === 'show') {
showField('field2');
} else {
hideField('field2');
}
// Logic for text field: field3
if (getField('field1') === 'show') {
showField('field3');
} else {
hideField('field3');
}
// Logic for text field: field4
if (getField('field1') === 'show') {
showField('field4');
} else {
hideField('field4');
}
});
// Solution: Only 1 handler managing all dependent fields!`)
</script>
<style scoped>
pre {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
}
</style>

View File

@ -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 = () => {

View File

@ -296,7 +296,7 @@
v-for="(component, index) in formStore.formComponents"
:key="index"
>
<component-preview :component="component" :is-preview="false" />
<component-preview :component="component" :is-preview="true" />
</template>
</div>
@ -904,6 +904,20 @@
</details>
</div>
<!-- Conditional Logic Optimization -->
<div class="mb-4 p-3 bg-amber-50 rounded border border-amber-200">
<details>
<summary class="text-sm font-medium text-amber-800 cursor-pointer"> Conditional Logic Optimization</summary>
<div class="mt-2 text-xs text-amber-700 space-y-1">
<div><strong>Automatic Optimization:</strong> When multiple fields use <code>onFieldChange</code> with the same trigger field:</div>
<div> The form builder automatically groups them into a single handler</div>
<div> No need to worry about duplicate event listeners</div>
<div> Better performance and memory usage</div>
<div> Use field settings UI for visual condition setup, or write custom logic here</div>
</div>
</details>
</div>
<!-- Utility Functions -->
<div class="mb-4 p-3 bg-purple-50 rounded border border-purple-200">
<details>
@ -980,6 +994,20 @@
</div>
</details>
</div>
<!-- Performance Optimization Notice -->
<div class="mb-4 p-3 bg-blue-50 rounded border border-blue-200">
<details>
<summary class="text-sm font-medium text-blue-800 cursor-pointer"> Performance Optimization</summary>
<div class="mt-2 text-xs text-blue-700 space-y-1">
<div><strong>Smart Field Watching:</strong> The form builder automatically optimizes conditional logic</div>
<div> Groups multiple field conditions that watch the same trigger field</div>
<div> Prevents duplicate <code>onFieldChange</code> handlers</div>
<div> Improves performance and reduces memory usage</div>
<div> Example: If 5 fields depend on "customer_type", only 1 handler is created</div>
</div>
</details>
</div>
</div>
<RsCodeMirror

View File

@ -1730,7 +1730,7 @@ const getWorkflowSubmitButtonStyle = () => {
<template v-for="component in currentForm.formComponents" :key="component.id || component.props.name">
<ComponentPreview
:component="component"
:is-preview="false"
:is-preview="true"
:field-states="fieldStates"
@form-data-updated="handleFormDataUpdate"
/>

View File

@ -83,44 +83,7 @@ export default defineEventHandler(async (event) => {
console.log('Case instance created:', caseInstance.caseUUID);
// Get the process definition
const processDefinition = process.processDefinition || {};
const nodes = processDefinition.nodes || [];
const edges = processDefinition.edges || [];
// Find all form nodes
const formNodes = nodes.filter(node => node.type === 'form');
if (formNodes.length === 0) {
console.log('No form nodes found in process');
return {
success: false,
error: 'Process does not contain any forms'
};
}
console.log(`Found ${formNodes.length} form nodes`);
// Create tasks for all forms
const tasks = [];
for (const formNode of formNodes) {
console.log('Creating task for form:', formNode.data?.label);
const task = await prisma.task.create({
data: {
taskUUID: uuidv4(),
caseID: caseInstance.caseID,
taskName: formNode.data?.label || 'Complete Form',
taskType: 'form',
taskStatus: 'pending',
taskAssignedTo: currentUser.userID,
taskFormID: formNode.data?.formId,
taskCreatedDate: new Date(),
taskModifiedDate: new Date()
}
});
tasks.push(task);
console.log('Task created:', task.taskUUID);
}
console.log('Process started successfully - tasks will be created dynamically during workflow execution');
// Add to case timeline
console.log('Adding to case timeline...');
@ -143,14 +106,7 @@ export default defineEventHandler(async (event) => {
name: caseInstance.caseName,
status: caseInstance.caseStatus,
startedAt: caseInstance.caseCreatedDate
},
tasks: tasks.map(task => ({
id: task.taskUUID,
name: task.taskName,
type: task.taskType,
formId: task.taskFormID,
status: task.taskStatus
}))
}
}
};
} catch (error) {