Partial submissions (#705)
* Implement partial form submissions feature * Add status filtering for form submissions * Add Partial Submission in Analytics * improve partial submission * fix lint * Add type checking for submission ID in form submission job * on form stats Partial Submissions only if enable * Partial Submissions is PRO Feature * Partial Submissions is PRO Feature * improvement migration * Update form submission status labels to 'Submitted' and 'In Progress' * start partial sync when dataFormValue update * badge size xs * Refactor partial submission hash management * Refactor partial form submission handling in PublicFormController * fix submissiona * Refactor form submission ID handling and metadata processing - Improve submission ID extraction and decoding across controllers - Add robust handling for submission hash and ID conversion - Enhance metadata processing in StoreFormSubmissionJob - Simplify submission storage logic with clearer metadata extraction - Minor UI improvements in FormSubmissions and OpenTable components * Enhance form submission settings UI with advanced partial submission options - Restructure partial submissions toggle with more descriptive label - Add advanced submission options section with Pro tag - Improve help text for partial submissions feature - Update ProTag with more detailed upgrade modal description * Refactor partial form submission sync mechanism - Improve partial submission synchronization in usePartialSubmission composable - Replace interval-based sync with Vue's reactive watch - Add robust handling for different form data input patterns - Implement onBeforeUnmount hook for final sync attempt - Enhance data synchronization reliability and performance * Improve partial form submission validation and synchronization * fix lint * Refactor submission identifier processing in PublicFormController - Updated the docblock for the method responsible for processing submission identifiers to clarify its functionality. The method now explicitly states that it converts a submission hash or string ID into a numeric submission_id, ensuring consistent internal storage format. These changes aim to improve code documentation and enhance understanding of the method's purpose and behavior. * Enhance Form Logic Condition Checker to Exclude Partial Submissions - Updated the query in FormLogicConditionChecker to exclude submissions with a status of 'partial', ensuring that only complete submissions are processed. - Minor formatting adjustment in the docblock of PublicFormController for improved clarity. These changes aim to refine submission handling and enhance the accuracy of form logic evaluations. * Partial Submission Test * Refactor FormSubmissionController and PartialSubmissionTest for Consistency - Updated the `FormSubmissionController` to improve code consistency by adjusting the formatting of anonymous functions in the `filter` and `first` methods. - Modified `PartialSubmissionTest` to simplify the `Storage::fake()` method call, removing the unnecessary 'local' parameter for better clarity. These changes aim to enhance code readability and maintainability across the form submission handling and testing components. * Enhance FormSubmissionController and EditSubmissionTest for Clarity - Added validation to the `FormSubmissionController` by introducing `$submissionData = $request->validated();` to ensure that only validated data is processed for form submissions. - Improved code readability in the `FormSubmissionController` by adjusting the formatting of anonymous functions in the `filter` and `first` methods. - Removed unnecessary blank lines in the `EditSubmissionTest` to streamline the test setup. These changes aim to enhance data integrity during form submissions and improve overall code clarity and maintainability. --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
14
client/composables/forms/pendingSubmission.js
vendored
14
client/composables/forms/pendingSubmission.js
vendored
@@ -31,6 +31,17 @@ export const pendingSubmission = (form) => {
|
||||
return pendingSubmission ? JSON.parse(pendingSubmission) : defaultValue
|
||||
}
|
||||
|
||||
const setSubmissionHash = (hash) => {
|
||||
set({
|
||||
...get(),
|
||||
submission_hash: hash
|
||||
})
|
||||
}
|
||||
|
||||
const getSubmissionHash = () => {
|
||||
return get()?.submission_hash ?? null
|
||||
}
|
||||
|
||||
const setTimer = (value) => {
|
||||
if (import.meta.server) return
|
||||
useStorage(formPendingSubmissionTimerKey.value).value = value
|
||||
@@ -46,10 +57,13 @@ export const pendingSubmission = (form) => {
|
||||
}
|
||||
|
||||
return {
|
||||
formPendingSubmissionKey,
|
||||
enabled,
|
||||
set,
|
||||
get,
|
||||
remove,
|
||||
setSubmissionHash,
|
||||
getSubmissionHash,
|
||||
setTimer,
|
||||
removeTimer,
|
||||
getTimer,
|
||||
|
||||
131
client/composables/forms/usePartialSubmission.js
vendored
Normal file
131
client/composables/forms/usePartialSubmission.js
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
import { opnFetch } from "./../useOpnApi.js"
|
||||
import { pendingSubmission as pendingSubmissionFunction } from "./pendingSubmission.js"
|
||||
import { watch, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
// Create a Map to store submission hashes for different forms
|
||||
const submissionHashes = ref(new Map())
|
||||
|
||||
export const usePartialSubmission = (form, formData = {}) => {
|
||||
const pendingSubmission = pendingSubmissionFunction(form)
|
||||
|
||||
let syncTimeout = null
|
||||
let dataWatcher = null
|
||||
|
||||
const getSubmissionHash = () => {
|
||||
return pendingSubmission.getSubmissionHash() ?? submissionHashes.value.get(pendingSubmission.formPendingSubmissionKey.value)
|
||||
}
|
||||
|
||||
const setSubmissionHash = (hash) => {
|
||||
submissionHashes.value.set(pendingSubmission.formPendingSubmissionKey.value, hash)
|
||||
pendingSubmission.setSubmissionHash(hash)
|
||||
}
|
||||
|
||||
const debouncedSync = () => {
|
||||
if (syncTimeout) clearTimeout(syncTimeout)
|
||||
syncTimeout = setTimeout(() => {
|
||||
syncToServer()
|
||||
}, 1000) // 1 second debounce
|
||||
}
|
||||
|
||||
const syncToServer = async () => {
|
||||
// Check if partial submissions are enabled and if we have data
|
||||
if (!form?.enable_partial_submissions) return
|
||||
|
||||
// Get current form data - handle both function and direct object patterns
|
||||
const currentData = typeof formData.value?.data === 'function'
|
||||
? formData.value.data()
|
||||
: formData.value
|
||||
|
||||
// Skip if no data or empty data
|
||||
if (!currentData || Object.keys(currentData).length === 0) return
|
||||
|
||||
try {
|
||||
const response = await opnFetch(`/forms/${form.slug}/answer`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
...currentData,
|
||||
'is_partial': true,
|
||||
'submission_hash': getSubmissionHash()
|
||||
}
|
||||
})
|
||||
if (response.submission_hash) {
|
||||
setSubmissionHash(response.submission_hash)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync partial submission', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Add these handlers as named functions so we can remove them later
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
debouncedSync()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
debouncedSync()
|
||||
}
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
syncToServer()
|
||||
}
|
||||
|
||||
const startSync = () => {
|
||||
if (dataWatcher) return
|
||||
|
||||
// Initial sync
|
||||
debouncedSync()
|
||||
|
||||
// Watch formData directly with Vue's reactivity
|
||||
dataWatcher = watch(
|
||||
formData,
|
||||
() => {
|
||||
debouncedSync()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Add event listeners for critical moments
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.addEventListener('blur', handleBlur)
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
}
|
||||
|
||||
const stopSync = () => {
|
||||
submissionHashes.value = new Map()
|
||||
|
||||
if (dataWatcher) {
|
||||
dataWatcher()
|
||||
dataWatcher = null
|
||||
}
|
||||
|
||||
if (syncTimeout) {
|
||||
clearTimeout(syncTimeout)
|
||||
syncTimeout = null
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.removeEventListener('blur', handleBlur)
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
}
|
||||
|
||||
// Ensure cleanup when component is unmounted
|
||||
onBeforeUnmount(() => {
|
||||
stopSync()
|
||||
|
||||
// Final sync attempt before unmounting
|
||||
if(getSubmissionHash()) {
|
||||
syncToServer()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
startSync,
|
||||
stopSync,
|
||||
syncToServer,
|
||||
getSubmissionHash,
|
||||
setSubmissionHash
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user