import { toValue } from 'vue' import { opnFetch } from '~/composables/useOpnApi.js' import clonedeep from 'clone-deep' /** * @fileoverview Composable for initializing form data, with complete handling of * form state persistence, URL parameters, and default values. */ export function useFormInitialization(formConfig, form, pendingSubmission) { /** * Main method to initialize the form data. * Follows a clear priority order: * 1. Load from submission ID (if provided) * 2. Load from pendingSubmission (localStorage) - client-side only * 3. Apply URL parameters * 4. Apply default values for fields * * @param {Object} options - Initialization options * @param {String} [options.submissionId] - ID of submission to load * @param {URLSearchParams} [options.urlParams] - URL parameters * @param {Object} [options.defaultData] - Default data to apply * @param {Array} [options.fields] - Form fields for special handling */ const initialize = async (options = {}) => { const config = toValue(formConfig) // 1. Reset form state form.reset() form.errors.clear() // 2. Try loading from submission ID if (options.submissionId) { const loaded = await tryLoadFromSubmissionId(options.submissionId) if (loaded) return // Exit if loaded successfully } // 3. Try loading from pendingSubmission if (!(options.skipPendingSubmission ?? false) && tryLoadFromPendingSubmission()) { updateSpecialFields() return // Exit if loaded successfully } // 4. Apply URL parameters if (!(options.skipUrlParams ?? false) && options.urlParams) { applyUrlParameters(options.urlParams) } // 5. Apply special field handling updateSpecialFields() // 6. Apply default data from config or options const defaultValuesToApply = options.defaultData || config?.default_data if (defaultValuesToApply) { applyDefaultValues(defaultValuesToApply, config?.properties) } // 7. Process any select fields to ensure IDs are converted to names // This is crucial when receiving data that might contain IDs instead of names const currentData = form.data() if (Object.keys(currentData).length > 0) { resetAndFill(currentData) } } /** * Wrapper for form.resetAndFill that converts select option IDs to names * @param {Object} formData - Form data to clean and fill */ const resetAndFill = (formData) => { if (!formData) { form.reset() return } // Clone the data to avoid mutating the original const cleanData = clonedeep(formData) // Process select fields to convert IDs to names if (!formConfig.value || !formConfig.value.properties || !Array.isArray(formConfig.value.properties)) { // If properties aren't available, just use the data as is form.resetAndFill(cleanData) return } // Iterate through form fields to process select fields formConfig.value.properties.forEach(field => { // Basic validation if (!field || typeof field !== 'object') return if (!field.id || !field.type) return // Skip only when value is truly undefined or null if (cleanData[field.id] === undefined || cleanData[field.id] === null) return // Process checkbox fields - convert string and numeric values to boolean if (field.type === 'checkbox') { const value = cleanData[field.id] if (typeof value === 'string' && value.toLowerCase() === 'true' || value === '1' || value === 1) { cleanData[field.id] = true } else if (typeof value === 'string' && value.toLowerCase() === 'false' || value === '0' || value === 0) { cleanData[field.id] = false } } // Only process select, multi_select fields else if (['select', 'multi_select'].includes(field.type)) { // Make sure the field has options if (!field[field.type] || !Array.isArray(field[field.type].options)) return const options = field[field.type].options // Process array values (multi-select) if (Array.isArray(cleanData[field.id])) { cleanData[field.id] = cleanData[field.id].map(optionId => { const option = options.find(opt => opt.id === optionId) return option ? option.name : optionId }) } // Process single values (select) else { const option = options.find(opt => opt.id === cleanData[field.id]) if (option) { cleanData[field.id] = option.name } } } }) // Fill with cleaned data form.resetAndFill(cleanData) } /** * Applies URL parameters to the form data. * @param {URLSearchParams} params - The URL search parameters. */ const applyUrlParameters = (params) => { if (!params) return // First, handle regular parameters params.forEach((value, key) => { // Skip array parameters for now if (key.endsWith('[]')) return try { // Try to parse JSON if the value starts with '{' const parsedValue = (typeof value === 'string' && value.startsWith('{')) ? JSON.parse(value) : value form[key] = parsedValue } catch { // If parsing fails, use the original value form[key] = value } }) // Handle array parameters (key[]) const paramKeys = [...new Set([...params.keys()])] paramKeys.forEach(key => { if (key.endsWith('[]')) { const arrayValues = params.getAll(key) if (arrayValues.length > 0) { const baseKey = key.slice(0, -2) form[baseKey] = arrayValues } } }) } /** * Applies default data to form fields that don't already have values. * @param {Object} defaultData - Default data object. */ const applyDefaultValues = (defaultData) => { if (!defaultData || Object.keys(defaultData).length === 0) return form.resetAndFill(defaultData) } /** * Updates special fields like dates with today's date if configured. * @param {Array} fields - Form fields */ const updateSpecialFields = () => { formConfig.value.properties.forEach(field => { // Handle date fields with prefill_today if (field.type === 'date' && field.prefill_today) { form[field.id] = new Date().toISOString() } // Handle matrix fields with prefill data else if (field.type === 'matrix' && !form[field.id] && field.prefill) { form[field.id] = {...field.prefill} } else if (field.id && !form[field.id]) { form[field.id] = field.prefill } }) } /** * Attempts to load form data from an existing submission. * @param {String} submissionId - ID of the submission to load * @returns {Promise} - Whether loading was successful */ const tryLoadFromSubmissionId = async (submissionId) => { const submissionIdValue = toValue(submissionId) if (!submissionIdValue) return false const config = toValue(formConfig) // Get the form config value const slug = config?.slug // Extract the slug if (!slug) { console.error('Cannot load submission: Form slug is missing from config.') form.reset() // Reset if config is invalid return false } // Use the correct route format: /forms/{slug}/submissions/{submission_id} return opnFetch(`/forms/${slug}/submissions/${submissionIdValue}`) .then(submissionData => { if (submissionData.data) { resetAndFill({ ...submissionData.data, submission_id: submissionIdValue }) return true } else { console.warn(`Submission ${submissionIdValue} for form ${slug} loaded but returned no data.`) form.reset() return false } }) .catch(error => { console.error(`Error loading submission ${submissionIdValue} for form ${slug}:`, error) form.reset() return false }) } /** * Attempts to load form data from pendingSubmission in localStorage. * @returns {Boolean} - Whether loading was successful */ const tryLoadFromPendingSubmission = () => { // Skip on server or if pendingSubmission is not available if (import.meta.server || !pendingSubmission) { return false } // Check if auto-save is enabled for this form if (!pendingSubmission.enabled?.value) { return false } // Get the saved data const pendingData = pendingSubmission.get() if (!pendingData || Object.keys(pendingData).length === 0) { return false } resetAndFill(pendingData) return true } return { initialize, applyUrlParameters, applyDefaultValues, resetAndFill // Export our wrapped function for use elsewhere } }