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:
@@ -16,6 +16,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PublicFormController extends Controller
|
||||
{
|
||||
@@ -85,38 +86,128 @@ class PublicFormController extends Controller
|
||||
return redirect()->to($internal_url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle partial form submissions
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
private function handlePartialSubmissions(Request $request)
|
||||
{
|
||||
$form = $request->form;
|
||||
|
||||
// Process submission data to extract submission ID
|
||||
$submissionData = $this->processSubmissionIdentifiers($request, $request->all());
|
||||
|
||||
// Validate that at least one field has a value
|
||||
$hasValue = false;
|
||||
foreach ($submissionData as $key => $value) {
|
||||
if (Str::isUuid($key) && !empty($value)) {
|
||||
$hasValue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasValue) {
|
||||
return $this->error([
|
||||
'message' => 'At least one field must have a value for partial submissions.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 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',
|
||||
'submission_hash' => Hashids::encode($submissionId)
|
||||
]);
|
||||
}
|
||||
|
||||
public function answer(AnswerFormRequest $request, FormSubmissionProcessor $formSubmissionProcessor)
|
||||
{
|
||||
$form = $request->form;
|
||||
$isFirstSubmission = ($form->submissions_count === 0);
|
||||
$submissionId = false;
|
||||
|
||||
// Handle partial submissions
|
||||
$isPartial = $request->get('is_partial') ?? false;
|
||||
if ($isPartial && $form->enable_partial_submissions && $form->is_pro) {
|
||||
return $this->handlePartialSubmissions($request);
|
||||
}
|
||||
|
||||
// Get validated data (includes all metadata)
|
||||
$submissionData = $request->validated();
|
||||
$completionTime = $request->get('completion_time') ?? null;
|
||||
unset($submissionData['completion_time']); // Remove completion_time from the main data array
|
||||
|
||||
$job = new StoreFormSubmissionJob($form, $submissionData, $completionTime);
|
||||
// Process submission hash and ID
|
||||
$submissionData = $this->processSubmissionIdentifiers($request, $submissionData);
|
||||
|
||||
// Create the job with all data (including metadata)
|
||||
$job = new StoreFormSubmissionJob($form, $submissionData);
|
||||
|
||||
// Process the submission
|
||||
if ($formSubmissionProcessor->shouldProcessSynchronously($form)) {
|
||||
$job->handle();
|
||||
$submissionId = Hashids::encode($job->getSubmissionId());
|
||||
$encodedSubmissionId = Hashids::encode($job->getSubmissionId());
|
||||
// Update submission data with generated values for redirect URL
|
||||
$submissionData = $job->getProcessedData();
|
||||
} else {
|
||||
StoreFormSubmissionJob::dispatch($form, $submissionData, $completionTime);
|
||||
$job->handle();
|
||||
$encodedSubmissionId = Hashids::encode($job->getSubmissionId());
|
||||
}
|
||||
|
||||
// Return the response
|
||||
return $this->success(array_merge([
|
||||
'message' => 'Form submission saved.',
|
||||
'submission_id' => $submissionId,
|
||||
'submission_id' => $encodedSubmissionId,
|
||||
'is_first_submission' => $isFirstSubmission,
|
||||
], $formSubmissionProcessor->getRedirectData($form, $submissionData)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes submission identifiers to ensure consistent numeric format
|
||||
*
|
||||
* Takes a submission hash or string ID and converts it to a numeric submission_id.
|
||||
* This allows submissions to be identified by either a hashed value or direct ID
|
||||
* while ensuring consistent internal storage format.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param array $submissionData
|
||||
* @return array
|
||||
*/
|
||||
private function processSubmissionIdentifiers(Request $request, array $submissionData): array
|
||||
{
|
||||
// Handle submission hash if present (convert to numeric submission_id)
|
||||
$submissionHash = $request->get('submission_hash');
|
||||
if ($submissionHash) {
|
||||
$decodedHash = Hashids::decode($submissionHash);
|
||||
if (!empty($decodedHash)) {
|
||||
$submissionData['submission_id'] = (int)($decodedHash[0] ?? null);
|
||||
}
|
||||
unset($submissionData['submission_hash']);
|
||||
}
|
||||
|
||||
// Handle string submission_id if present (convert to numeric)
|
||||
if (isset($submissionData['submission_id']) && is_string($submissionData['submission_id']) && !is_numeric($submissionData['submission_id'])) {
|
||||
$decodedId = Hashids::decode($submissionData['submission_id']);
|
||||
if (!empty($decodedId)) {
|
||||
$submissionData['submission_id'] = (int)($decodedId[0] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return $submissionData;
|
||||
}
|
||||
|
||||
public function fetchSubmission(Request $request, string $slug, string $submissionId)
|
||||
{
|
||||
$submissionId = ($submissionId) ? Hashids::decode($submissionId) : false;
|
||||
$submissionId = isset($submissionId[0]) ? $submissionId[0] : false;
|
||||
// Decode the submission ID using the same approach as in processSubmissionIdentifiers
|
||||
$decodedId = Hashids::decode($submissionId);
|
||||
$submissionId = !empty($decodedId) ? (int)($decodedId[0]) : false;
|
||||
|
||||
$form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail();
|
||||
if ($form->workspace == null || !$form->editable_submissions || !$submissionId) {
|
||||
return $this->error([
|
||||
|
||||
Reference in New Issue
Block a user