From b7d6f42e76b024654806fd6e06b94dc8511aae30 Mon Sep 17 00:00:00 2001
From: Afiq
Date: Fri, 8 Aug 2025 10:37:10 +0800
Subject: [PATCH] Enhance Component Preview and Workflow Loading Logic
- Updated ComponentPreview.vue to improve handling of select fields, ensuring the first option value is returned as the default if no form data exists.
- Refactored handleFieldInput to support asynchronous updates for select fields, enhancing reactivity and ensuring the latest value is used from the form store.
- Introduced retry logic with exponential backoff in loadProcess and loadFormData functions within [id].vue, improving resilience against temporary fetch failures.
- Enhanced error handling and logging for loading processes and forms, providing clearer feedback during failures and retries.
- Updated form data initialization to set default values for select fields when loading forms, ensuring a smoother user experience.
---
components/ComponentPreview.vue | 46 ++++++-
pages/workflow/[id].vue | 227 ++++++++++++++++++++++++++------
2 files changed, 227 insertions(+), 46 deletions(-)
diff --git a/components/ComponentPreview.vue b/components/ComponentPreview.vue
index 8c01205..05078d3 100644
--- a/components/ComponentPreview.vue
+++ b/components/ComponentPreview.vue
@@ -1590,15 +1590,43 @@ const getFieldValue = (component) => {
}
}
+ // For select fields, return first option value as default if no form data exists
+ if (component.type === 'select' && component.props.options && component.props.options.length > 0) {
+ return component.props.options[0].value || '';
+ }
+
// Return component default value if no form data exists
return component.props.value || '';
};
-const handleFieldInput = (component, event) => {
+const handleFieldInput = async (component, event) => {
if (!props.isPreview) return;
const fieldName = component.props.name;
- const newValue = event;
+
+ // Extract the actual value from the event based on field type
+ let newValue;
+ if (component.type === 'select') {
+ // For select fields, get the value from the event target or use the event itself if it's already a value
+ newValue = (event && event.target) ? event.target.value : event;
+ console.log(`[ComponentPreview] Select field '${fieldName}' input - raw event:`, event);
+ console.log(`[ComponentPreview] Select field '${fieldName}' extracted value:`, newValue);
+
+ // Wait for Vue's reactivity cycle to complete
+ await nextTick();
+
+ // Try to get the current value from the form store after nextTick
+ const currentStoreValue = formStore.previewFormData[fieldName];
+ console.log(`[ComponentPreview] Select field '${fieldName}' store value after nextTick:`, currentStoreValue);
+
+ // Use the most recent value (either extracted or from store)
+ if (currentStoreValue && currentStoreValue !== newValue) {
+ console.log(`[ComponentPreview] Using store value instead:`, currentStoreValue);
+ newValue = currentStoreValue;
+ }
+ } else {
+ newValue = event;
+ }
// Simple update for regular fields only
const updatedData = { ...formStore.previewFormData, [fieldName]: newValue };
@@ -1611,30 +1639,34 @@ const handleFieldChange = (component, event) => {
};
// Helper function to safely get field values for repeating groups without warnings
-const getRepeatingGroupFieldValue = (group, fieldName, fieldType) => {
+const getRepeatingGroupFieldValue = (group, fieldName, fieldType, component = null) => {
if (!group || typeof group !== 'object') {
- return getDefaultValueForType(fieldType);
+ return getDefaultValueForType(fieldType, component);
}
if (group.hasOwnProperty(fieldName)) {
const value = group[fieldName];
if (value === undefined || value === null) {
- return getDefaultValueForType(fieldType);
+ return getDefaultValueForType(fieldType, component);
}
return value;
}
- return getDefaultValueForType(fieldType);
+ return getDefaultValueForType(fieldType, component);
};
// Helper function to get default values based on field type
-const getDefaultValueForType = (fieldType) => {
+const getDefaultValueForType = (fieldType, component = null) => {
switch (fieldType) {
case 'number':
return 0;
case 'checkbox':
return [];
case 'select':
+ // If component is provided and has options, use first option value
+ if (component && component.props && component.props.options && component.props.options.length > 0) {
+ return component.props.options[0].value || '';
+ }
return '';
default:
return '';
diff --git a/pages/workflow/[id].vue b/pages/workflow/[id].vue
index aebe191..ecc7726 100644
--- a/pages/workflow/[id].vue
+++ b/pages/workflow/[id].vue
@@ -163,15 +163,15 @@ const captureUrlParameters = () => {
};
// Load process data
-const loadProcess = async () => {
+const loadProcess = async (retryCount = 0) => {
try {
loading.value = true;
error.value = null;
- console.log('[Workflow] Loading process definition...');
+ console.log(`[Workflow] Loading process definition... (attempt ${retryCount + 1})`);
const response = await $fetch(`/api/process/${processId.value}`);
- if (response.success) {
+ if (response.success && response.process) {
process.value = response.process; // includes processDefinition
// Check if process is published
@@ -203,15 +203,31 @@ const loadProcess = async () => {
await startProcessExecution();
} else {
- error.value = response.message || 'Failed to load process';
- notifyParentOfError(error.value);
+ throw new Error(response.message || 'Invalid process response');
}
- } catch (err) {
- console.error('[Workflow] Error loading process:', err);
- error.value = 'Failed to load process data';
+ } catch (fetchError) {
+ console.error(`[Workflow] Error loading process (attempt ${retryCount + 1}):`, fetchError);
+
+ // Retry logic with exponential backoff
+ if (retryCount < 2) {
+ const delay = Math.pow(2, retryCount) * 1500; // 1.5s, 3s
+ console.log(`[Workflow] Retrying process load in ${delay}ms (attempt ${retryCount + 2}/3)`);
+
+ setTimeout(async () => {
+ await loadProcess(retryCount + 1);
+ }, delay);
+ return;
+ }
+
+ // After all retries failed
+ error.value = `Failed to load process after ${retryCount + 1} attempts: ${fetchError.message || 'Network error'}`;
notifyParentOfError(error.value);
- } finally {
loading.value = false;
+ } finally {
+ // Only set loading to false if we're not retrying
+ if (retryCount >= 2 || process.value) {
+ loading.value = false;
+ }
}
};
@@ -275,6 +291,7 @@ function getNextNodeIdForDecision(currentNodeId) {
const { conditions = [], defaultPath } = currentNodeObj.data || {};
console.log('[Gateway Debug] Conditions:', conditions);
console.log('[Gateway Debug] Default path:', defaultPath);
+ console.log('[Gateway Debug] Current process variables:', processVariables.value);
// Evaluate condition groups (each group represents a path)
for (const conditionGroup of conditions) {
@@ -341,25 +358,46 @@ function evaluateConditionGroup(conditionGroup, variables) {
const condition = conditionGroup.conditions[i];
const conditionResult = evaluateCondition(condition, variables);
const operator = condition.logicalOperator || 'and';
+
+ console.log(`[Gateway Debug] Group '${conditionGroup.output}': evaluating condition ${i}, operator='${operator}', previous result=${result}, current condition result=${conditionResult}`);
+
if (operator === 'and') {
result = result && conditionResult;
} else if (operator === 'or') {
result = result || conditionResult;
}
- console.log(`[Gateway Debug] Group '${conditionGroup.output}': after condition ${i}, operator='${operator}', conditionResult=`, conditionResult, ', groupResult=', result);
+ console.log(`[Gateway Debug] Group '${conditionGroup.output}': after condition ${i}, final result=`, result);
}
+ console.log(`[Gateway Debug] Group '${conditionGroup.output}': FINAL GROUP RESULT=`, result);
return result;
}
// Helper: Evaluate a single condition
function evaluateCondition(condition, variables) {
- const { variable, operator, value, valueType, minValue, maxValue } = condition;
+ const { variable, operator, value, minValue, maxValue } = condition;
const variableValue = variables[variable];
+ console.log(`[Gateway Debug] Evaluating condition: variable='${variable}', operator='${operator}', value='${value}', variableValue='${variableValue}'`);
+
+ // Handle undefined/null variables
+ if (variableValue === undefined || variableValue === null) {
+ console.warn(`[Gateway Debug] Variable '${variable}' is undefined/null for condition evaluation`);
+
+ // For empty/not_empty checks, handle undefined/null explicitly
+ if (operator === 'empty' || operator === 'is_empty') {
+ return true;
+ } else if (operator === 'not_empty' || operator === 'is_not_empty') {
+ return false;
+ }
+
+ // For other operators, undefined/null values should generally fail comparisons
+ return false;
+ }
+
// Handle boolean type conversions
let compareValue = value;
- if (valueType === 'boolean') {
+ if (condition.valueType === 'boolean') {
if (typeof value === 'string') {
compareValue = value.toLowerCase() === 'true';
} else {
@@ -372,12 +410,13 @@ function evaluateCondition(condition, variables) {
case 'eq':
case 'equals':
case '==':
- result = variableValue == compareValue;
+ // Use strict equality for better type safety
+ result = String(variableValue) === String(compareValue);
break;
case 'neq':
case 'not_equals':
case '!=':
- result = variableValue != compareValue;
+ result = String(variableValue) !== String(compareValue);
break;
case 'gt':
case 'greater_than':
@@ -445,7 +484,16 @@ function evaluateCondition(condition, variables) {
break;
case 'not_empty':
case 'is_not_empty':
- result = variableValue && variableValue !== '' && variableValue !== null && variableValue !== undefined;
+ // Check for meaningful values - handle strings, arrays, objects
+ if (typeof variableValue === 'string') {
+ result = variableValue.trim() !== '';
+ } else if (Array.isArray(variableValue)) {
+ result = variableValue.length > 0;
+ } else if (typeof variableValue === 'object' && variableValue !== null) {
+ result = Object.keys(variableValue).length > 0;
+ } else {
+ result = variableValue !== null && variableValue !== undefined && variableValue !== '';
+ }
break;
case 'is_true':
result = Boolean(variableValue) === true;
@@ -497,7 +545,7 @@ function evaluateCondition(condition, variables) {
console.warn('[Workflow] Unknown condition operator:', operator);
result = false;
}
- console.log(`[Gateway Debug] Condition: variable='${variable}', operator='${operator}', value=`, value, ', minValue=', minValue, ', maxValue=', maxValue, '| variableValue=', variableValue, '| result=', result);
+ console.log(`[Gateway Debug] Condition DETAILED: variable='${variable}', operator='${operator}', expectedValue='${value}', actualVariableValue='${variableValue}', types: expected=${typeof value}, actual=${typeof variableValue}, result=${result}`);
return result;
}
@@ -644,14 +692,50 @@ function applyInputMappings(nodeData, processVars, formData) {
function applyOutputMappings(nodeData, formData, processVars) {
const { outputMappings = [] } = nodeData;
console.log('[Workflow] Applying output mappings:', outputMappings);
+ console.log('[Workflow] Form data available for mapping:', Object.keys(formData));
outputMappings.forEach(mapping => {
const { formField, processVariable } = mapping;
- if (formData[formField] !== undefined) {
- processVars[processVariable] = formData[formField];
- console.log(`[Workflow] Mapped ${formField} -> ${processVariable}:`, formData[formField]);
+
+ // Validate that the form field exists in the form data
+ if (!(formField in formData)) {
+ console.warn(`[Workflow] Form field '${formField}' not found in form data for mapping to '${processVariable}'`);
+ console.warn(`[Workflow] Available form fields:`, Object.keys(formData));
+ return;
}
+
+ const fieldValue = formData[formField];
+
+ // Handle undefined, null, and empty values
+ if (fieldValue === undefined) {
+ console.warn(`[Workflow] Form field '${formField}' is undefined, skipping mapping to '${processVariable}'`);
+ return;
+ }
+
+ // Map the value with proper type handling
+ let mappedValue = fieldValue;
+
+ // Special handling for common field types
+ if (typeof fieldValue === 'string') {
+ mappedValue = fieldValue.trim(); // Remove whitespace
+ } else if (Array.isArray(fieldValue)) {
+ // For multi-select fields, join values or take first value based on expected variable type
+ const variableSource = process.value?.variables || process.value?.processVariables;
+ const variableConfig = variableSource?.[processVariable];
+
+ if (variableConfig?.type === 'string' && fieldValue.length > 0) {
+ mappedValue = fieldValue[0]; // Take first selected value for string variables
+ } else {
+ mappedValue = fieldValue; // Keep as array for other types
+ }
+ }
+
+ // Set the process variable
+ processVars[processVariable] = mappedValue;
+ console.log(`[Workflow] Successfully mapped '${formField}' (${typeof fieldValue}) -> '${processVariable}':`, mappedValue);
});
+
+ console.log('[Workflow] Process variables after output mapping:', processVars);
}
// Apply button mappings to assign button click data to process variables
@@ -676,6 +760,7 @@ function applyButtonMappings(nodeData, buttonEvent, processVars) {
break;
case 'custom':
valueToAssign = customValue || '';
+ console.log(`[Workflow] Button custom value assignment: customValue=${customValue} (type: ${typeof customValue}), valueToAssign=${valueToAssign} (type: ${typeof valueToAssign})`);
break;
default:
valueToAssign = true;
@@ -1067,17 +1152,38 @@ const getStepInfo = (node) => {
};
// Load form data from database
-const loadFormData = async (formId) => {
+const loadFormData = async (formId, retryCount = 0) => {
try {
- if (!formId) return null;
-
- const response = await $fetch(`/api/forms/${formId}`);
- if (response.success) {
- return response.form;
+ if (!formId) {
+ console.warn('[Workflow] No form ID provided to loadFormData');
+ return null;
}
- return null;
- } catch (error) {
- console.error('Error loading form:', error);
+
+ console.log(`[Workflow] Loading form data for ID: ${formId} (attempt ${retryCount + 1})`);
+ const response = await $fetch(`/api/forms/${formId}`);
+
+ if (response.success && response.form) {
+ console.log(`[Workflow] Form data loaded successfully for ID: ${formId}`);
+ return response.form;
+ } else {
+ console.warn(`[Workflow] Form load failed - invalid response for ID: ${formId}`, response);
+ throw new Error(`Invalid form response: ${response.message || 'Unknown error'}`);
+ }
+ } catch (fetchError) {
+ console.error(`[Workflow] Error loading form ${formId}:`, fetchError);
+
+ // Retry logic with exponential backoff
+ if (retryCount < 3) {
+ const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
+ console.log(`[Workflow] Retrying form load in ${delay}ms (attempt ${retryCount + 2}/4)`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+ return loadFormData(formId, retryCount + 1);
+ }
+
+ // After all retries failed, set error state and return null
+ error.value = `Failed to load form after ${retryCount + 1} attempts: ${fetchError.message || 'Network error'}`;
+ console.error(`[Workflow] Form loading completely failed for ID: ${formId} after ${retryCount + 1} attempts`);
return null;
}
};
@@ -1101,10 +1207,18 @@ const handleScriptFieldChange = ({ fieldName, value }) => {
// Handle form data updates from ComponentPreview
const handleFormDataUpdate = (updatedData) => {
- // Update the form data with the new data
+ console.log('[Workflow] Form data update from ComponentPreview:', updatedData);
+
+ // Log the current formData state before update
+ console.log('[Workflow] Current formData before update:', formData.value);
+
+ // Update the form data with the new data (simple merge)
formData.value = { ...formData.value, ...updatedData };
- // Also update form store to keep them in sync
- formStore.updatePreviewFormData(formData.value);
+
+ // Log the new formData state after update
+ console.log('[Workflow] FormData after update:', formData.value);
+
+ // Don't update form store here - let the watcher handle it to avoid double updates
};
// Handle button click events and apply button mappings
@@ -1114,8 +1228,16 @@ const handleButtonClick = (buttonEvent) => {
// Apply button mappings if configured
if (currentNode.value?.data?.buttonMappings?.length > 0) {
applyButtonMappings(currentNode.value.data, buttonEvent, processVariables.value);
-
- // Check if any button mapping has navigation action
+ }
+
+ // Apply output mappings to capture current form data before navigation
+ if (currentNode.value?.data?.outputMappings?.length > 0) {
+ console.log('[Workflow] Applying output mappings before button navigation');
+ applyOutputMappings(currentNode.value.data, formData.value, processVariables.value);
+ }
+
+ // Check if any button mapping has navigation action
+ if (currentNode.value?.data?.buttonMappings?.length > 0) {
const buttonMapping = currentNode.value.data.buttonMappings.find(
mapping => mapping.formButton === buttonEvent.buttonName && mapping.navigationAction === 'continue'
);
@@ -1306,6 +1428,13 @@ watch(currentStep, async (newStep) => {
const formId = currentNode.value.data?.formId;
if (formId) {
currentForm.value = await loadFormData(formId);
+
+ // Check if form loading failed
+ if (!currentForm.value) {
+ console.error(`[Workflow] Form loading failed for ID: ${formId}, showing error state`);
+ stepLoading.value = false;
+ return; // Don't proceed with form setup if loading failed
+ }
// Apply input mappings to pre-fill form
if (currentNode.value.data?.inputMappings) {
applyInputMappings(
@@ -1327,6 +1456,19 @@ watch(currentStep, async (newStep) => {
// Initialize repeating groups and tables in form data
const updatedFormData = { ...formData.value };
+
+ // Initialize all form fields with proper default values
+ currentForm.value.formComponents.forEach(component => {
+ const fieldName = component.props?.name;
+
+ // Initialize select fields with first option if not already set
+ if (component.type === 'select' && fieldName && !updatedFormData.hasOwnProperty(fieldName)) {
+ if (component.props.options && component.props.options.length > 0) {
+ updatedFormData[fieldName] = component.props.options[0].value || '';
+ console.log(`[Workflow] Initialized select field '${fieldName}' with default value:`, updatedFormData[fieldName]);
+ }
+ }
+ });
currentForm.value.formComponents.forEach(component => {
if (component.type === 'repeating-group' && component.props?.name) {
const groupName = component.props.name;
@@ -1448,6 +1590,7 @@ const onFormKitSubmit = () => {
handleFormSubmit();
};
+
// New: validate and submit handler for button
const validateAndSubmit = () => {
if (formRef.value && formRef.value.node && typeof formRef.value.node.submit === 'function') {
@@ -1861,12 +2004,18 @@ const getWorkflowSubmitButtonStyle = () => {
-
Error
+
Process Error
{{ error }}
-
-
- Go Home
-
+
+ loadProcess(0)" variant="secondary">
+
+ Try Again
+
+
+
+ Go Home
+
+
@@ -1877,7 +2026,7 @@ const getWorkflowSubmitButtonStyle = () => {
The workflow "{{ process.processName }}" has been completed successfully.
-
+ loadProcess(0)" variant="secondary">
Run Again