From d63ecd43cc2a94afa0643ac9c22e2dc49fda5caa Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Tue, 25 Mar 2025 18:35:16 +0100 Subject: [PATCH] Refactor Form Submission and Field Management Logic - Simplified the constructor in StoreFormSubmissionJob for improved readability. - Enhanced the storeFile method to handle null and empty values more robustly, ensuring better validation of file names. - Updated StorageFileNameParser to return null for empty file names, improving error handling. - Refactored OpenForm.vue to optimize form initialization and prevent unnecessary reinitializations, enhancing performance. - Introduced new methods for managing field groups and preventing recursive updates in OpenForm.vue, improving the overall user experience. - Enhanced FormEditorSidebar.vue to utilize Pinia store for better state management and clarity. - Improved FormFieldEdit.vue to prevent page jumps during field editing, ensuring a smoother user experience. - Added new getters in working_form.js for better page management and navigation within forms. These changes aim to enhance the maintainability and performance of the form handling logic, providing a more efficient and user-friendly experience. --- api/app/Jobs/Form/StoreFormSubmissionJob.php | 7 +- .../Service/Storage/StorageFileNameParser.php | 2 +- client/components/open/forms/OpenForm.vue | 142 ++++++++++-- .../form-components/FormEditorPreview.vue | 1 - .../form-components/FormEditorSidebar.vue | 27 ++- .../form-logic-components/ColumnCondition.vue | 8 +- .../open/forms/fields/FormFieldEdit.vue | 10 + client/components/pages/NotFoundForm.vue | 16 +- .../pages/auth/components/QuickRegister.vue | 6 - client/stores/working_form.js | 204 ++++++++++++++++-- 10 files changed, 348 insertions(+), 75 deletions(-) diff --git a/api/app/Jobs/Form/StoreFormSubmissionJob.php b/api/app/Jobs/Form/StoreFormSubmissionJob.php index d5dd8214..7621e5c1 100644 --- a/api/app/Jobs/Form/StoreFormSubmissionJob.php +++ b/api/app/Jobs/Form/StoreFormSubmissionJob.php @@ -176,9 +176,9 @@ class StoreFormSubmissionJob implements ShouldQueue * - file_name-{uuid}.{ext} * - {uuid} */ - private function storeFile(?string $value) + private function storeFile($value, ?bool $isPublic = null) { - if ($value == null) { + if (is_null($value) || empty($value)) { return null; } @@ -196,6 +196,9 @@ class StoreFormSubmissionJob implements ShouldQueue } $fileNameParser = StorageFileNameParser::parse($value); + if (!$fileNameParser || !$fileNameParser->uuid) { + return null; + } // Make sure we retrieve the file in tmp storage, move it to persistent $fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser->uuid; diff --git a/api/app/Service/Storage/StorageFileNameParser.php b/api/app/Service/Storage/StorageFileNameParser.php index 6377c6a7..9e3dd84a 100644 --- a/api/app/Service/Storage/StorageFileNameParser.php +++ b/api/app/Service/Storage/StorageFileNameParser.php @@ -33,7 +33,7 @@ class StorageFileNameParser $fileName = substr($this->fileName, 0, 50) . '_' . $this->uuid . '.' . $this->extension; $fileName = preg_replace('#\p{C}+#u', '', $fileName); // avoid CorruptedPathDetected exceptions - return S3KeyCleaner::sanitize($fileName); + return $fileName ? S3KeyCleaner::sanitize($fileName) : null; } return $this->uuid; diff --git a/client/components/open/forms/OpenForm.vue b/client/components/open/forms/OpenForm.vue index 6ffba160..ff22ff2d 100644 --- a/client/components/open/forms/OpenForm.vue +++ b/client/components/open/forms/OpenForm.vue @@ -200,11 +200,10 @@ export default { data() { return { - // Page index - /** - * Used to force refresh components by changing their keys - */ isAutoSubmit: false, + isInitialLoad: true, + // Flag to prevent recursion in field group updates + isUpdatingFieldGroups: false, } }, @@ -337,16 +336,24 @@ export default { }, watch: { - form: { - deep: true, - handler() { - this.initForm() - } + // Monitor only critical changes that require full reinitialization + 'form.database_id': function() { + // Only reinitialize when database changes + this.initForm() }, - fields: { + 'fields.length': function() { + // Only reinitialize when fields are added or removed + this.updateFieldGroupsSafely() + }, + // Watch for changes to individual field properties + 'fields': { deep: true, handler() { - this.initForm() + // Skip update if only triggered by internal fieldGroups changes + if (this.isUpdatingFieldGroups) return + + // Safely update field groups without causing recursive updates + this.updateFieldGroupsSafely() } }, dataFormValue: { @@ -460,21 +467,48 @@ export default { * Form initialization */ async initForm() { + // Only do a full initialization when necessary + // Store current page index and form data to avoid overwriting existing values + const currentFormData = this.dataForm ? clonedeep(this.dataForm.data()) : {} + + // Handle special cases first if (this.defaultDataForm) { + // If we have default data form, initialize with that await nextTick(() => { this.dataForm.resetAndFill(this.defaultDataForm) }) + this.updateFieldGroupsSafely() return } - - if (await this.tryInitFormFromEditableSubmission()) return - if (this.tryInitFormFromPendingSubmission()) return - await nextTick(() => { - this.formPageIndex = 0 - this.initFormWithDefaultValues() - }) + // Initialize the field groups without resetting form data + this.updateFieldGroupsSafely() + + // Check if we need to handle form submission states + if (await this.checkForEditableSubmission()) { + return + } + + if (this.checkForPendingSubmission()) { + return + } + + // Standard initialization with default values + this.initFormWithDefaultValues(currentFormData) }, + + checkForEditableSubmission() { + return this.tryInitFormFromEditableSubmission() + }, + + checkForPendingSubmission() { + if (this.tryInitFormFromPendingSubmission()) { + this.updateFieldGroupsSafely() + return true + } + return false + }, + async tryInitFormFromEditableSubmission() { if (this.isPublicFormPage && this.form.editable_submissions) { const submissionId = useRoute().query?.submission_id @@ -489,6 +523,7 @@ export default { } return false }, + tryInitFormFromPendingSubmission() { if (this.isPublicFormPage && this.form.auto_save) { const pendingData = this.pendingSubmission.get() @@ -500,6 +535,7 @@ export default { } return false }, + updatePendingDataFields(pendingData) { this.fields.forEach(field => { if (field.type === 'date' && field.prefill_today) { @@ -507,17 +543,26 @@ export default { } }) }, - initFormWithDefaultValues() { - const formData = {} + + initFormWithDefaultValues(currentFormData = {}) { + // Only set page 0 on first load, otherwise maintain current position + if (this.formPageIndex === undefined || this.isInitialLoad) { + this.formPageIndex = 0 + this.isInitialLoad = false + } + + // Initialize form data with default values + const formData = { ...currentFormData } const urlPrefill = this.isPublicFormPage ? new URLSearchParams(window.location.search) : null this.fields.forEach(field => { - if (field.type.startsWith('nf-')) return + if (field.type.startsWith('nf-') && !['nf-page-body-input', 'nf-page-logo', 'nf-page-cover'].includes(field.type)) return this.handleUrlPrefill(field, formData, urlPrefill) this.handleDefaultPrefill(field, formData) }) - + + // Reset form with new data this.dataForm.resetAndFill(formData) }, handleUrlPrefill(field, formData, urlPrefill) { @@ -620,6 +665,59 @@ export default { } this.formPageIndex = this.fieldGroups.length - 1 }, + + // New method for updating field groups + updateFieldGroups() { + if (!this.fields || this.fields.length === 0) return + + // Preserve the current page index if possible + const currentPageIndex = this.formPageIndex + + // Use a local variable instead of directly modifying computed property + // We'll use this to determine totalPages and currentPageIndex + const calculatedGroups = this.fields.reduce((groups, field, index) => { + // If the field is a page break, start a new group + if (field.type === 'nf-page-break' && index !== 0) { + groups.push([]) + } + // Add the field to the current group + if (groups.length === 0) groups.push([]) + groups[groups.length - 1].push(field) + return groups + }, []) + + // If we don't have any groups (shouldn't happen), create a default group + if (calculatedGroups.length === 0) { + calculatedGroups.push([]) + } + + // Update page navigation + const totalPages = calculatedGroups.length + + // Try to maintain the current page index if valid + if (currentPageIndex !== undefined && currentPageIndex < totalPages) { + this.formPageIndex = currentPageIndex + } else { + this.formPageIndex = 0 + } + + // Force a re-render of the component, which will update fieldGroups computed property + this.$forceUpdate() + }, + + // Helper method to prevent recursive updates + updateFieldGroupsSafely() { + // Set flag to prevent recursive updates + this.isUpdatingFieldGroups = true + + // Call the actual update method + this.updateFieldGroups() + + // Clear the flag after a short delay to allow Vue to process the update + this.$nextTick(() => { + this.isUpdatingFieldGroups = false + }) + } } } diff --git a/client/components/open/forms/components/form-components/FormEditorPreview.vue b/client/components/open/forms/components/form-components/FormEditorPreview.vue index a3cb998e..a2938201 100644 --- a/client/components/open/forms/components/form-components/FormEditorPreview.vue +++ b/client/components/open/forms/components/form-components/FormEditorPreview.vue @@ -107,7 +107,6 @@ import { computed, ref, watch, onMounted } from 'vue' import OpenCompleteForm from '../../OpenCompleteForm.vue' import {handleDarkMode, useDarkMode} from "~/lib/forms/public-page.js" -import { default as _has } from 'lodash/has' import { useRecordsStore } from '~/stores/records' import { useWorkingFormStore } from '~/stores/working_form' import { storeToRefs } from 'pinia' diff --git a/client/components/open/forms/components/form-components/FormEditorSidebar.vue b/client/components/open/forms/components/form-components/FormEditorSidebar.vue index 21e1de79..9b37e974 100644 --- a/client/components/open/forms/components/form-components/FormEditorSidebar.vue +++ b/client/components/open/forms/components/form-components/FormEditorSidebar.vue @@ -25,6 +25,7 @@