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 }}
-