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.
This commit is contained in:
Afiq 2025-08-08 10:37:10 +08:00
parent 649a956063
commit b7d6f42e76
2 changed files with 227 additions and 46 deletions

View File

@ -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 '';

View File

@ -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 State -->
<div v-else-if="error" :class="isIframeMode ? 'p-6 text-center' : 'bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center'">
<Icon name="material-symbols:error-outline" class="w-16 h-16 text-red-400 mx-auto mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">Error</h3>
<h3 class="text-lg font-medium text-gray-900 mb-2">Process Error</h3>
<p class="text-gray-600 mb-6">{{ error }}</p>
<RsButton v-if="!isIframeMode" @click="goHome" variant="primary">
<Icon name="material-symbols:home" class="mr-2" />
Go Home
</RsButton>
<div class="flex justify-center gap-3">
<RsButton @click="() => loadProcess(0)" variant="secondary">
<Icon name="material-symbols:refresh" class="mr-2" />
Try Again
</RsButton>
<RsButton v-if="!isIframeMode" @click="goHome" variant="primary">
<Icon name="material-symbols:home" class="mr-2" />
Go Home
</RsButton>
</div>
</div>
<!-- Process Complete - Hidden when hideComplete=true -->
@ -1877,7 +2026,7 @@ const getWorkflowSubmitButtonStyle = () => {
The workflow "{{ process.processName }}" has been completed successfully.
</p>
<div v-if="!isIframeMode" class="flex justify-center gap-3">
<RsButton @click="loadProcess" variant="secondary">
<RsButton @click="() => loadProcess(0)" variant="secondary">
<Icon name="material-symbols:refresh" class="mr-2" />
Run Again
</RsButton>