Refactor Form Submission Logic and Improve File Handling
- Updated `FormController.php` to correct the file path concatenation for temporary file retrieval, enhancing clarity in file management. - Modified `FormSubmissionResource.php` to improve the filtering logic for file fields, ensuring only valid files are processed. - Streamlined the constructor in `StoreFormSubmissionJob.php` for better readability and consistency. - Enhanced file handling in `StoreFormSubmissionJob.php` by refining the logic for moving files from temporary to permanent storage, ensuring accurate file management. - Improved the `FormSubmissionFormatter.php` to handle file URLs more robustly with error handling for signed routes. - Updated `PartialSubmissionTest.php` to remove the hardcoded success message, improving test reliability. - Refactored `OpenFormField.vue` to simplify the disabled state logic for form fields, enhancing maintainability. - Adjusted `usePartialSubmission.js` to improve the synchronization mechanism during visibility changes, ensuring data integrity. These changes aim to enhance the overall functionality, maintainability, and reliability of the form submission process and file handling logic.
This commit is contained in:
parent
053abbf31b
commit
2edd0816d4
|
|
@ -228,7 +228,7 @@ class FormController extends Controller
|
|||
$fileNameParser = StorageFileNameParser::parse($request->url);
|
||||
|
||||
// Make sure we retrieve the file in tmp storage, move it to persistent
|
||||
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser->uuid;
|
||||
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . $fileNameParser->uuid;
|
||||
if (!Storage::exists($fileName)) {
|
||||
// File not found, we skip
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class FormSubmissionResource extends JsonResource
|
|||
foreach ($fileFields as $field) {
|
||||
if (isset($data[$field['id']]) && !empty($data[$field['id']])) {
|
||||
$data[$field['id']] = collect($data[$field['id']])->filter(function ($file) {
|
||||
return $file !== null && $file;
|
||||
return !is_null($file) && !empty($file);
|
||||
})->map(function ($file) {
|
||||
return [
|
||||
'file_url' => \URL::signedRoute(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ use Illuminate\Queue\InteractsWithQueue;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Job to store form submissions
|
||||
|
|
@ -68,20 +67,11 @@ 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;
|
||||
|
||||
// Only trigger integrations for completed submissions, not partial ones
|
||||
if (!$this->isPartial) {
|
||||
FormSubmitted::dispatch($this->form, $this->formData);
|
||||
}
|
||||
|
|
@ -97,21 +87,16 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
*/
|
||||
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']);
|
||||
|
|
@ -135,27 +120,18 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
*/
|
||||
private function storeSubmission(array $formData)
|
||||
{
|
||||
// Find existing submission or create a new one
|
||||
$submission = $this->submissionId
|
||||
? $this->form->submissions()->findOrFail($this->submissionId)
|
||||
: new FormSubmission();
|
||||
|
||||
// Set submission properties
|
||||
if (!$this->submissionId) {
|
||||
$submission->form_id = $this->form->id;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
@ -170,10 +146,8 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
{
|
||||
$data = $this->submissionData;
|
||||
$finalData = [];
|
||||
|
||||
$properties = collect($this->form->properties);
|
||||
|
||||
// Do required transformation per type (e.g. file uploads)
|
||||
foreach ($data as $answerKey => $answerValue) {
|
||||
$field = $properties->where('id', $answerKey)->first();
|
||||
if (!$field) {
|
||||
|
|
@ -185,45 +159,58 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
|| $field['type'] == 'files'
|
||||
) {
|
||||
if (is_array($answerValue)) {
|
||||
$finalData[$field['id']] = [];
|
||||
foreach ($answerValue as $file) {
|
||||
$finalData[$field['id']][] = $this->storeFile($file);
|
||||
$processedFiles = [];
|
||||
foreach ($answerValue as $fileItem) {
|
||||
if (is_string($fileItem) && !empty($fileItem)) {
|
||||
$singleStoredFile = $this->storeFile($fileItem);
|
||||
if ($singleStoredFile) {
|
||||
$processedFiles[] = $singleStoredFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
$finalData[$field['id']] = $processedFiles;
|
||||
} else {
|
||||
$finalData[$field['id']] = $this->storeFile($answerValue);
|
||||
if (is_string($answerValue) && !empty($answerValue)) {
|
||||
$singleFileResult = $this->storeFile($answerValue);
|
||||
$finalData[$field['id']] = $singleFileResult;
|
||||
} else {
|
||||
$finalData[$field['id']] = $this->storeFile($answerValue); // Handles null/empty $answerValue by returning null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard field processing (text, ID generation, etc.)
|
||||
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
|
||||
$finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid()->toString() : 'Please upgrade your OpenForm subscription to use our ID generation features';
|
||||
} elseif ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
|
||||
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
|
||||
} else {
|
||||
if ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
|
||||
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
|
||||
} else {
|
||||
$finalData[$field['id']] = $answerValue;
|
||||
}
|
||||
$finalData[$field['id']] = $answerValue;
|
||||
}
|
||||
}
|
||||
|
||||
// For Singrature
|
||||
// Special field types
|
||||
if ($field['type'] == 'signature') {
|
||||
$finalData[$field['id']] = $this->storeSignature($answerValue);
|
||||
}
|
||||
|
||||
// For Phone
|
||||
if ($field['type'] == 'phone_number' && $answerValue && ctype_alpha(substr($answerValue, 0, 2)) && (!isset($field['use_simple_text_input']) || !$field['use_simple_text_input'])) {
|
||||
$finalData[$field['id']] = substr($answerValue, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return $finalData;
|
||||
}
|
||||
|
||||
// This is use when updating a record, and file uploads aren't changed.
|
||||
private function isSkipForUpload($value)
|
||||
{
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$parser = StorageFileNameParser::parse($value);
|
||||
$canonicalStoredName = $parser->getMovedFileName();
|
||||
|
||||
return Storage::exists($newPath . '/' . $value);
|
||||
if (!$canonicalStoredName) {
|
||||
return false; // Input $value couldn't be resolved to a canonical stored name format
|
||||
}
|
||||
|
||||
$destinationPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$fullPathToCheck = $destinationPath . '/' . $canonicalStoredName;
|
||||
return Storage::exists($fullPathToCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -239,61 +226,58 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
if (is_null($value) || empty($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3
|
||||
// Handle pre-existing full URLs (e.g., from prefill)
|
||||
if (filter_var($value, FILTER_VALIDATE_URL) !== false && str_contains($value, parse_url(config('app.url'))['host'])) {
|
||||
$fileName = explode('?', basename($value))[0];
|
||||
$path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName;
|
||||
$path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName; // Assuming assets are in a defined path
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
Storage::move($path, $newPath . '/' . $fileName);
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
if ($this->isSkipForUpload($value)) {
|
||||
return $value;
|
||||
$shouldSkip = $this->isSkipForUpload($value);
|
||||
|
||||
if ($shouldSkip) {
|
||||
// File (based on canonical name derived from $value) already exists in permanent storage.
|
||||
// Return its canonical name.
|
||||
$parser = StorageFileNameParser::parse($value);
|
||||
return $parser->getMovedFileName() ?? $value; // Fallback to $value if canonical somehow fails (defensive)
|
||||
}
|
||||
|
||||
$fileNameParser = StorageFileNameParser::parse($value);
|
||||
// Process as a new file upload (or one whose temp version needs to be moved)
|
||||
$fileNameParser = StorageFileNameParser::parse($value); // $value is the temp file reference (e.g., originalname_uuid.ext or uuid)
|
||||
|
||||
if (!$fileNameParser || !$fileNameParser->uuid) {
|
||||
return null;
|
||||
return null; // Cannot derive UUID from the reference
|
||||
}
|
||||
|
||||
// Make sure we retrieve the file in tmp storage, move it to persistent
|
||||
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser->uuid;
|
||||
if (!Storage::exists($fileName)) {
|
||||
// File not found, we skip
|
||||
return null;
|
||||
$fileNameInTmp = PublicFormController::TMP_FILE_UPLOAD_PATH . $fileNameParser->uuid;
|
||||
if (!Storage::exists($fileNameInTmp)) {
|
||||
return null; // Temporary file not found
|
||||
}
|
||||
$movedFileName = $fileNameParser->getMovedFileName(); // This is the canonical name for storage
|
||||
if (empty($movedFileName)) {
|
||||
return null; // Canonical name generation failed
|
||||
}
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$completeNewFilename = $newPath . '/' . $fileNameParser->getMovedFileName();
|
||||
|
||||
Log::debug('Moving file to permanent storage.', [
|
||||
'uuid' => $fileNameParser->uuid,
|
||||
'destination' => $completeNewFilename,
|
||||
'form_id' => $this->form->id,
|
||||
'form_slug' => $this->form->slug,
|
||||
]);
|
||||
Storage::move($fileName, $completeNewFilename);
|
||||
|
||||
return $fileNameParser->getMovedFileName();
|
||||
$completeNewFilename = $newPath . '/' . $movedFileName;
|
||||
Storage::move($fileNameInTmp, $completeNewFilename);
|
||||
return $movedFileName;
|
||||
}
|
||||
|
||||
private function storeSignature(?string $value)
|
||||
{
|
||||
if ($value && preg_match('/^[\/\w\-. ]+$/', $value)) { // If it's filename
|
||||
return $this->storeFile($value);
|
||||
// If $value looks like a filename (already processed, e.g. during skip or previous handling)
|
||||
if ($value && preg_match('/^[\/\w\-. ]+$/', $value)) {
|
||||
return $this->storeFile($value); // Re-run through storeFile for consistency / skip logic
|
||||
}
|
||||
|
||||
// If $value is base64 data
|
||||
if ($value == null || !isset(explode(',', $value)[1])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fileName = 'sign_' . (string) Str::uuid() . '.png';
|
||||
$newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id);
|
||||
$completeNewFilename = $newPath . '/' . $fileName;
|
||||
|
||||
Storage::put($completeNewFilename, base64_decode(explode(',', $value)[1]));
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +288,6 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
*/
|
||||
private function addHiddenPrefills(array &$formData): void
|
||||
{
|
||||
// Find hidden properties with prefill, set values
|
||||
collect($this->form->properties)->filter(function ($property) {
|
||||
return isset($property['hidden'])
|
||||
&& isset($property['prefill'])
|
||||
|
|
@ -331,11 +314,8 @@ class StoreFormSubmissionJob implements ShouldQueue
|
|||
if ($this->formData === null) {
|
||||
$this->formData = $this->getFormData();
|
||||
}
|
||||
|
||||
// Ensure the submission ID is included in the returned data
|
||||
$data = $this->formData;
|
||||
$data['submission_id'] = $this->submissionId;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@ class FormSubmissionFormatter
|
|||
$formId = $this->form->id;
|
||||
$returnArray[$field['name']] = implode(
|
||||
', ',
|
||||
collect($data[$field['id']])->map(function ($file) use ($formId) {
|
||||
collect($data[$field['id']])->filter(function ($file) {
|
||||
return !is_null($file) && !empty($file);
|
||||
})->map(function ($file) use ($formId) {
|
||||
return $this->getFileUrl($formId, $file);
|
||||
})->toArray()
|
||||
);
|
||||
|
|
@ -289,9 +291,14 @@ class FormSubmissionFormatter
|
|||
|
||||
private function getFileUrl($formId, $file)
|
||||
{
|
||||
return $this->useSignedUrlForFiles ? \URL::signedRoute(
|
||||
'open.forms.submissions.file',
|
||||
[$formId, $file]
|
||||
) : route('open.forms.submissions.file', [$formId, $file]);
|
||||
try {
|
||||
return $this->useSignedUrlForFiles ? \URL::signedRoute(
|
||||
'open.forms.submissions.file',
|
||||
[$formId, $file]
|
||||
) : route('open.forms.submissions.file', [$formId, $file]);
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ it('can submit form partially and complete it later using submission hash', func
|
|||
->assertSuccessful()
|
||||
->assertJson([
|
||||
'type' => 'success',
|
||||
'message' => 'Partial submission saved',
|
||||
]);
|
||||
|
||||
$submissionHash = $partialResponse->json('submission_hash');
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
v-if="getFieldComponents"
|
||||
v-bind="inputProperties(field)"
|
||||
:required="isFieldRequired"
|
||||
:disabled="isFieldDisabled ? true : null"
|
||||
:disabled="isFieldDisabled"
|
||||
:is-admin-preview="isAdminPreview"
|
||||
/>
|
||||
<template v-else>
|
||||
|
|
@ -175,6 +175,7 @@ const form = computed(() => props.formManager?.config?.value || {})
|
|||
const dataForm = computed(() => props.formManager?.form || {})
|
||||
const darkMode = computed(() => props.formManager?.darkMode?.value || false)
|
||||
const showHidden = computed(() => props.formManager?.strategy?.value?.display?.showHiddenFields || false)
|
||||
const enableDisabledFields = computed(() => props.formManager?.strategy?.value?.display?.enableDisabledFields || false)
|
||||
|
||||
// Setup stores and reactive state
|
||||
const workingFormStore = useWorkingFormStore()
|
||||
|
|
@ -238,9 +239,10 @@ const isFieldRequired = computed(() =>
|
|||
(new FormLogicPropertyResolver(props.field, dataForm.value)).isRequired()
|
||||
)
|
||||
|
||||
const isFieldDisabled = computed(() =>
|
||||
(new FormLogicPropertyResolver(props.field, dataForm.value)).isDisabled()
|
||||
)
|
||||
const isFieldDisabled = computed(() => {
|
||||
if (enableDisabledFields.value) return false
|
||||
return (new FormLogicPropertyResolver(props.field, dataForm.value)).isDisabled()
|
||||
})
|
||||
|
||||
const beingEdited = computed(() =>
|
||||
isAdminPreview.value &&
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export function usePartialSubmission(formConfig, formDataRef, pendingSubmissionS
|
|||
clearTimeout(syncTimeout)
|
||||
}
|
||||
|
||||
// Set a new timeout - increased to 2 seconds for less frequent syncing
|
||||
syncTimeout = setTimeout(() => {
|
||||
syncToServer()
|
||||
}, 2000) // 2 second debounce
|
||||
|
|
@ -93,13 +92,13 @@ export function usePartialSubmission(formConfig, formDataRef, pendingSubmissionS
|
|||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
// When tab becomes hidden, sync immediately
|
||||
syncImmediately()
|
||||
debouncedSync()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// When window loses focus, sync immediately
|
||||
syncImmediately()
|
||||
debouncedSync()
|
||||
}
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export function createFormModeStrategy(mode) {
|
|||
// Display behaviors
|
||||
display: {
|
||||
showHiddenFields: false,
|
||||
enableDisabledFields: false,
|
||||
showFormCleanings: true,
|
||||
showFontLink: false
|
||||
},
|
||||
|
|
@ -71,6 +72,7 @@ export function createFormModeStrategy(mode) {
|
|||
strategy.validation.performActualSubmission = false
|
||||
|
||||
strategy.display.showHiddenFields = true
|
||||
strategy.display.enableDisabledFields = true
|
||||
break
|
||||
|
||||
case FormMode.EDIT:
|
||||
|
|
|
|||
Loading…
Reference in New Issue