diff --git a/client/components/open/forms/OpenForm.vue b/client/components/open/forms/OpenForm.vue index e7744d94..52638d31 100644 --- a/client/components/open/forms/OpenForm.vue +++ b/client/components/open/forms/OpenForm.vue @@ -110,6 +110,7 @@ :theme="theme" class="mt-2 px-8 mx-1" @click.stop="nextPage" + :loading="dataForm.busy" > {{ currentFieldsPageBreak.next_btn_text }} @@ -442,9 +443,17 @@ export default { return false }, nextPage() { - this.currentFieldGroupIndex += 1 - window.scrollTo({top: 0, behavior: 'smooth'}) - return false + const fieldsToValidate = this.currentFields.map(f => f.id) + this.dataForm.busy = true + this.dataForm.validate('POST', '/forms/' + this.form.slug + '/answer', {}, fieldsToValidate) + .then((data) => { + this.currentFieldGroupIndex += 1 + this.dataForm.busy = false + window.scrollTo({top: 0, behavior: 'smooth'}) + }).catch(err => { + this.dataForm.busy = false + }) + return false; }, isFieldHidden(field) { return (new FormLogicPropertyResolver(field, this.dataFormValue)).isHidden() diff --git a/client/composables/lib/vForm/Form.js b/client/composables/lib/vForm/Form.js index 04156188..b8e3c384 100644 --- a/client/composables/lib/vForm/Form.js +++ b/client/composables/lib/vForm/Form.js @@ -146,6 +146,43 @@ class Form { }) } + validate(method, url, config = {}, fieldsToValidate = {}) { + this.startProcessing() + const headers = { + 'Precognition': true, + 'Precognition-Validate-Only': Array.from(fieldsToValidate).join(), + ...config.headers + } + config = { + body: {}, + params: {}, + url: url, + method: method, + headers, + ...config, + } + if (method.toLowerCase() === "get") { + config.params = { ...this.data(), ...config.params } + } else { + config.body = { ...this.data(), ...config.data } + + if (hasFiles(config.data) && !config.transformRequest) { + config.transformRequest = [(data) => serialize(data)] + } + } + return new Promise((resolve, reject) => { + opnFetch(config.url, config) + .then((data) => { + this.finishProcessing() + resolve(data) + }) + .catch((error) => { + this.handleErrors(error) + reject(error) + }) + }) + } + handleErrors(error) { this.busy = false diff --git a/routes/api.php b/routes/api.php index 636a6d70..50f5a66f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,6 +22,7 @@ use App\Http\Controllers\SubscriptionController; use App\Http\Controllers\TemplateController; use App\Http\Controllers\WorkspaceController; use App\Http\Middleware\Form\ResolveFormMiddleware; +use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; @@ -236,7 +237,7 @@ Route::group(['prefix' => 'appsumo'], function () { */ Route::prefix('forms')->name('forms.')->group(function () { Route::middleware('protected-form')->group(function () { - Route::post('{slug}/answer', [PublicFormController::class, 'answer'])->name('answer'); + Route::post('{slug}/answer', [PublicFormController::class, 'answer'])->name('answer')->middleware(HandlePrecognitiveRequests::class); // Form content endpoints (user lists, relation lists etc.) Route::get( diff --git a/tests/Feature/Forms/AnswerFormTest.php b/tests/Feature/Forms/AnswerFormTest.php index 24508e3d..b3bd8349 100644 --- a/tests/Feature/Forms/AnswerFormTest.php +++ b/tests/Feature/Forms/AnswerFormTest.php @@ -237,3 +237,69 @@ it('can not submit form with failed custom validation condition', function () { 'message' => $validationMessage, ]); }); + + +it('can validate form answer with precognition', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + $properties = $form->properties; + $properties[0]['required'] = true; + $properties[3]['required'] = true; + $properties[6]['required'] = true; + $properties[9]['required'] = true; + + $form->properties = $properties; + $form->update(); + + // Empty submission data should fail validation, with all 4 required fields + $response = $this->postJson(route('forms.answer', $form->slug), []); + $errors = $response->json()['errors']; + $this->assertEquals(sizeof($errors), 4); + $response->assertStatus(422); + + // Fill in data for only Name. + $submissionData = []; + foreach ($properties as $property) { + if ($property['name'] == 'Name') { + $submissionData[$property['id']] = 'Name'; + } else { + $submissionData[$property['id']] = null; + } + } + + // Select only first 3 fields for precognition validation + $validateOnlyFields = [ + $properties[0]['id'], + $properties[1]['id'], + $properties[2]['id'] + ]; + + $precognitionValidateOnly = implode(',', $validateOnlyFields); + + // Partial submission data should pass validation for the precognition only fields. + $response = $this->withPrecognition()->withHeaders([ + 'Precognition-Validate-Only' => $precognitionValidateOnly + ]) + ->postJson(route('forms.answer', $form->slug), $submissionData); + + $response->assertSuccessfulPrecognition(); + + + // Select only next fields for precognition validation + $validateOnlyFields = $validateOnlyFields = [ + $properties[3]['id'], + $properties[4]['id'], + $properties[5]['id'] + ]; + $precognitionValidateOnly = implode(',', $validateOnlyFields); + + // Partial submission data should fail validation, but for only one required field specified for precognition validation. + $response = $this->withPrecognition()->withHeaders([ + 'Precognition-Validate-Only' => $precognitionValidateOnly + ]) + ->postJson(route('forms.answer', $form->slug), $submissionData); + $errors = $response->json()['errors']; + $this->assertEquals(sizeof($errors), 1); + $response->assertStatus(422); +});