opnform-host-nginx/client/lib/forms/composables/useFormInitialization.js

266 lines
8.7 KiB
JavaScript

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