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:
@@ -17,8 +17,27 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Job to store form submissions
|
||||
*
|
||||
* This job handles the storage of form submissions, including processing of metadata
|
||||
* and special field types like files and signatures.
|
||||
*
|
||||
* 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 (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
|
||||
{
|
||||
use Dispatchable;
|
||||
@@ -26,15 +45,19 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public ?string $submissionId = null;
|
||||
public ?int $submissionId = null;
|
||||
private ?array $formData = null;
|
||||
private ?int $completionTime = null;
|
||||
private bool $isPartial = false;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param Form $form The form being submitted
|
||||
* @param array $submissionData Form data including metadata fields (submission_id, completion_time, etc.)
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Form $form, public array $submissionData, public ?int $completionTime = null)
|
||||
public function __construct(public Form $form, public array $submissionData)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,60 +68,95 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Extract metadata from submission data
|
||||
$this->extractMetadata();
|
||||
|
||||
// Process form data
|
||||
$this->formData = $this->getFormData();
|
||||
$this->addHiddenPrefills($this->formData);
|
||||
|
||||
// Store the submission
|
||||
$this->storeSubmission($this->formData);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from submission data
|
||||
*
|
||||
* This method extracts and removes metadata fields from the submission data:
|
||||
* - submission_id
|
||||
* - completion_time
|
||||
* - is_partial
|
||||
*/
|
||||
private function extractMetadata(): void
|
||||
{
|
||||
// Extract completion time
|
||||
if (isset($this->submissionData['completion_time'])) {
|
||||
$this->completionTime = $this->submissionData['completion_time'];
|
||||
unset($this->submissionData['completion_time']);
|
||||
}
|
||||
|
||||
// Extract direct submission ID if present
|
||||
if (isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
|
||||
if (is_numeric($this->submissionData['submission_id'])) {
|
||||
$this->submissionId = (int)$this->submissionData['submission_id'];
|
||||
}
|
||||
unset($this->submissionData['submission_id']);
|
||||
}
|
||||
|
||||
// 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']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submission ID
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getSubmissionId()
|
||||
{
|
||||
return $this->submissionId;
|
||||
}
|
||||
|
||||
public function setSubmissionId(int $id)
|
||||
{
|
||||
$this->submissionId = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the submission in the database
|
||||
*
|
||||
* @param array $formData
|
||||
*/
|
||||
private function storeSubmission(array $formData)
|
||||
{
|
||||
// Create or update record
|
||||
if ($previousSubmission = $this->submissionToUpdate()) {
|
||||
$previousSubmission->data = $formData;
|
||||
$previousSubmission->completion_time = $this->completionTime;
|
||||
$previousSubmission->save();
|
||||
$this->submissionId = $previousSubmission->id;
|
||||
} else {
|
||||
$response = $this->form->submissions()->create([
|
||||
'data' => $formData,
|
||||
'completion_time' => $this->completionTime,
|
||||
]);
|
||||
$this->submissionId = $response->id;
|
||||
}
|
||||
}
|
||||
// Find existing submission or create a new one
|
||||
$submission = $this->submissionId
|
||||
? $this->form->submissions()->findOrFail($this->submissionId)
|
||||
: new FormSubmission();
|
||||
|
||||
/**
|
||||
* Search for Submission record to update and returns it
|
||||
*/
|
||||
private function submissionToUpdate(): ?FormSubmission
|
||||
{
|
||||
if ($this->submissionId) {
|
||||
return $this->form->submissions()->findOrFail($this->submissionId);
|
||||
}
|
||||
if ($this->form->editable_submissions && isset($this->submissionData['submission_id']) && $this->submissionData['submission_id']) {
|
||||
$submissionId = $this->submissionData['submission_id'] ? Hashids::decode($this->submissionData['submission_id']) : false;
|
||||
$submissionId = $submissionId[0] ?? null;
|
||||
|
||||
return $this->form->submissions()->findOrFail($submissionId);
|
||||
// Set submission properties
|
||||
if (!$this->submissionId) {
|
||||
$submission->form_id = $this->form->id;
|
||||
}
|
||||
|
||||
return null;
|
||||
$submission->data = $formData;
|
||||
$submission->completion_time = $this->completionTime;
|
||||
|
||||
// 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
|
||||
$this->submissionId = $submission->id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +267,7 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$completeNewFilename = $newPath . '/' . $fileNameParser->getMovedFileName();
|
||||
|
||||
\Log::debug('Moving file to permanent storage.', [
|
||||
Log::debug('Moving file to permanent storage.', [
|
||||
'uuid' => $fileNameParser->uuid,
|
||||
'destination' => $completeNewFilename,
|
||||
'form_id' => $this->form->id,
|
||||
@@ -264,13 +322,20 @@ class StoreFormSubmissionJob implements ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the processed form data after all transformations
|
||||
* Get the processed form data including the submission ID
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProcessedData(): array
|
||||
{
|
||||
if ($this->formData === null) {
|
||||
$this->formData = $this->getFormData();
|
||||
}
|
||||
return $this->formData;
|
||||
|
||||
// Ensure the submission ID is included in the returned data
|
||||
$data = $this->formData;
|
||||
$data['submission_id'] = $this->submissionId;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user