Skip to content

Commit

Permalink
Refactor partial form submission sync mechanism
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
JhumanJ committed Mar 6, 2025
1 parent ce1e21d commit d0f73cf
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 26 deletions.
18 changes: 9 additions & 9 deletions api/app/Http/Controllers/Forms/PublicFormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,19 @@ public function showAsset($assetFileName)
private function handlePartialSubmissions(Request $request)
{
$form = $request->form;
$submissionId = null;

// Process submission data to extract submission ID
$submissionData = $this->processSubmissionIdentifiers($request, $request->all());
$submissionId = $submissionData['submission_id'] ?? null;

$submissionResponse = $form->submissions()->updateOrCreate([
'id' => $submissionId
], [
'data' => $submissionData,
'status' => FormSubmission::STATUS_PARTIAL
]);
$submissionId = $submissionResponse->id;
// Explicitly mark this as a partial submission
$submissionData['is_partial'] = true;

// Use the same job as regular submissions to ensure consistent processing
$job = new StoreFormSubmissionJob($form, $submissionData);
$job->handle();

// Get the submission ID
$submissionId = $job->getSubmissionId();

return $this->success([
'message' => 'Partial submission saved',
Expand Down
25 changes: 21 additions & 4 deletions api/app/Jobs/Form/StoreFormSubmissionJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@
* The job accepts all data in the submissionData array, including metadata fields:
* - submission_id: ID of an existing submission to update (must be an integer)
* - completion_time: Time in seconds it took to complete the form
* - is_partial: Whether this is a partial submission
* - is_partial: Whether this is a partial submission (will be stored with STATUS_PARTIAL)
* If not specified, submissions are treated as complete by default.
*
* These metadata fields will be automatically extracted and removed from the stored form data.
*
* For partial submissions:
* - The submission will be stored with STATUS_PARTIAL
* - All file uploads and signatures will be processed normally
* - The submission can later be updated to STATUS_COMPLETED when the user completes the form
*/
class StoreFormSubmissionJob implements ShouldQueue
{
Expand All @@ -42,6 +48,7 @@ class StoreFormSubmissionJob implements ShouldQueue
public ?int $submissionId = null;
private ?array $formData = null;
private ?int $completionTime = null;
private bool $isPartial = false;

/**
* Create a new job instance.
Expand Down Expand Up @@ -72,7 +79,10 @@ public function handle()
// Add the submission ID to the form data after storing the submission
$this->formData['submission_id'] = $this->submissionId;

FormSubmitted::dispatch($this->form, $this->formData);
// Only trigger integrations for completed submissions, not partial ones
if (!$this->isPartial) {
FormSubmitted::dispatch($this->form, $this->formData);
}
}

/**
Expand All @@ -81,6 +91,7 @@ public function handle()
* This method extracts and removes metadata fields from the submission data:
* - submission_id
* - completion_time
* - is_partial
*/
private function extractMetadata(): void
{
Expand All @@ -98,8 +109,9 @@ private function extractMetadata(): void
unset($this->submissionData['submission_id']);
}

// Remove is_partial flag if present
// Extract is_partial flag if present, otherwise default to false
if (isset($this->submissionData['is_partial'])) {
$this->isPartial = (bool)$this->submissionData['is_partial'];
unset($this->submissionData['is_partial']);
}
}
Expand Down Expand Up @@ -133,7 +145,12 @@ private function storeSubmission(array $formData)

$submission->data = $formData;
$submission->completion_time = $this->completionTime;
$submission->status = FormSubmission::STATUS_COMPLETED;

// Set the status based on whether this is a partial submission
$submission->status = $this->isPartial
? FormSubmission::STATUS_PARTIAL
: FormSubmission::STATUS_COMPLETED;

$submission->save();

// Store the submission ID
Expand Down
48 changes: 35 additions & 13 deletions client/composables/forms/usePartialSubmission.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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)

const SYNC_INTERVAL = 30000 // 30 seconds
let syncInterval = null
let syncTimeout = null
let dataWatcher = null

const getSubmissionHash = () => {
return submissionHashes.value.get(pendingSubmission.formPendingSubmissionKey.value)
Expand All @@ -27,13 +27,22 @@ export const usePartialSubmission = (form, formData = {}) => {
}

const syncToServer = async () => {
if (!form?.enable_partial_submissions || !formData.value.data() || Object.keys(formData.value.data()).length === 0) return
// 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: {
...formData.value.data(),
...currentData,
'is_partial': true,
'submission_hash': getSubmissionHash()
}
Expand Down Expand Up @@ -62,27 +71,32 @@ export const usePartialSubmission = (form, formData = {}) => {
}

const startSync = () => {
if (syncInterval) return
if (dataWatcher) return

// Initial sync
debouncedSync()

// Regular interval sync
syncInterval = setInterval(() => {
debouncedSync()
}, SYNC_INTERVAL)
// Watch formData directly with Vue's reactivity
dataWatcher = watch(
formData,
() => {
debouncedSync()
},
{ deep: true }
)

// Add event listeners
// Add event listeners for critical moments
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('blur', handleBlur)
window.addEventListener('beforeunload', handleBeforeUnload)
}

const stopSync = () => {
if (syncInterval) {
clearInterval(syncInterval)
syncInterval = null
if (dataWatcher) {
dataWatcher()
dataWatcher = null
}

if (syncTimeout) {
clearTimeout(syncTimeout)
syncTimeout = null
Expand All @@ -94,6 +108,14 @@ export const usePartialSubmission = (form, formData = {}) => {
window.removeEventListener('beforeunload', handleBeforeUnload)
}

// Ensure cleanup when component is unmounted
onBeforeUnmount(() => {
stopSync()

// Final sync attempt before unmounting
syncToServer()
})

return {
startSync,
stopSync,
Expand Down

0 comments on commit d0f73cf

Please sign in to comment.