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:
Chirag Chhatrala
2025-04-28 21:03:55 +05:30
committed by GitHub
parent 89885d418e
commit ff1a4d17d8
23 changed files with 784 additions and 92 deletions

View File

@@ -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,

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