From 6673dff504af521ba79a1ab9351ff03539e565bf Mon Sep 17 00:00:00 2001 From: Favour Olayinka Date: Wed, 29 May 2024 10:40:14 +0100 Subject: [PATCH] Conditioned field validation (#418) * wip: validation condition input * form custom validation condition * Default message on form condition validation * field validation condition test * fix linting * update tests, add pass test * Polish UI --------- Co-authored-by: Julien Nahum --- app/Http/Requests/AnswerFormRequest.php | 13 ++- app/Rules/CustomFieldValidationRule.php | 40 ++++++++ .../components/CustomFieldValidation.vue | 87 ++++++++++++++++++ .../forms/fields/components/FieldOptions.vue | 9 +- tests/Feature/Forms/AnswerFormTest.php | 92 +++++++++++++++++++ 5 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 app/Rules/CustomFieldValidationRule.php create mode 100644 client/components/open/forms/components/CustomFieldValidation.vue diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php index cc070f52..3ba1caae 100644 --- a/app/Http/Requests/AnswerFormRequest.php +++ b/app/Http/Requests/AnswerFormRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use App\Models\Forms\Form; +use App\Rules\CustomFieldValidationRule; use App\Rules\StorageFile; use App\Rules\ValidHCaptcha; use App\Rules\ValidPhoneInputRule; @@ -50,18 +51,17 @@ class AnswerFormRequest extends FormRequest */ public function rules() { + $selectionFields = collect($this->form->properties)->filter(function ($pro) { + return in_array($pro['type'], ['select', 'multi_select']); + }); foreach ($this->form->properties as $property) { $rules = []; - /*if (!$this->form->is_pro) { // If not pro then not check logic $property['logic'] = false; }*/ // For get values instead of Id for select/multi select options $data = $this->toArray(); - $selectionFields = collect($this->form->properties)->filter(function ($pro) { - return in_array($pro['type'], ['select', 'multi_select']); - }); foreach ($selectionFields as $field) { if (isset($data[$field['id']]) && is_array($data[$field['id']])) { $data[$field['id']] = array_map(function ($val) use ($field) { @@ -87,6 +87,11 @@ class AnswerFormRequest extends FormRequest $rules[] = 'nullable'; } + // User custom validation + if(!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) { + $rules[] = (new CustomFieldValidationRule($property['validation'], $data)); + } + // Clean id to escape "." $propertyId = $property['id']; if (in_array($property['type'], ['multi_select'])) { diff --git a/app/Rules/CustomFieldValidationRule.php b/app/Rules/CustomFieldValidationRule.php new file mode 100644 index 00000000..a4b2598b --- /dev/null +++ b/app/Rules/CustomFieldValidationRule.php @@ -0,0 +1,40 @@ +validation['error_conditions']['conditions'], $this->formData); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return isset($this->validation['error_message']) ? $this->validation['error_message'] : 'Invalid input'; + } +} diff --git a/client/components/open/forms/components/CustomFieldValidation.vue b/client/components/open/forms/components/CustomFieldValidation.vue new file mode 100644 index 00000000..4beb9dd6 --- /dev/null +++ b/client/components/open/forms/components/CustomFieldValidation.vue @@ -0,0 +1,87 @@ + + + diff --git a/client/components/open/forms/fields/components/FieldOptions.vue b/client/components/open/forms/fields/components/FieldOptions.vue index cba64e5c..1f9bf484 100644 --- a/client/components/open/forms/fields/components/FieldOptions.vue +++ b/client/components/open/forms/fields/components/FieldOptions.vue @@ -566,6 +566,12 @@ :form="form" :field="field" /> + + @@ -574,12 +580,13 @@ import timezones from '~/data/timezones.json' import countryCodes from '~/data/country_codes.json' import CountryFlag from 'vue-country-flag-next' import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue' +import CustomFieldValidation from '../../components/CustomFieldValidation.vue' import { format } from 'date-fns' import { default as _has } from 'lodash/has' export default { name: 'FieldOptions', - components: { CountryFlag, FormBlockLogicEditor }, + components: { CountryFlag, FormBlockLogicEditor, CustomFieldValidation }, props: { field: { type: Object, diff --git a/tests/Feature/Forms/AnswerFormTest.php b/tests/Feature/Forms/AnswerFormTest.php index 81a28f49..24508e3d 100644 --- a/tests/Feature/Forms/AnswerFormTest.php +++ b/tests/Feature/Forms/AnswerFormTest.php @@ -1,5 +1,6 @@ 'The Date must be a date before tomorrow.', ]); }); + + +it('can submit form with passed custom validation condition', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + $targetField = collect($form->properties)->where('name', 'Number')->first(); + $condition = [ + 'actions' => [], + 'conditions' => [ + 'operatorIdentifier' => 'or', + 'children' => [ + [ + 'identifier' => $targetField['id'], + 'value' => [ + 'operator' => 'greater_than', + 'property_meta' => [ + 'id' => $targetField['id'], + 'type' => 'number', + ], + 'value' => 20, + ], + ], + ], + ], + ]; + $submissionData = []; + $validationMessage = 'Number too low'; + $form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, $targetField) { + if (in_array($property['name'], ['Name'])) { + $property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; + $submissionData[$targetField['id']] = 100; + } + return $property; + })->toArray(); + + $form->update(); + $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); + + $response = $this->postJson(route('forms.answer', $form->slug), $formData); + $response->assertSuccessful() + ->assertJson([ + 'type' => 'success', + 'message' => 'Form submission saved.', + ]); +}); + +it('can not submit form with failed custom validation condition', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + $targetField = collect($form->properties)->where('name', 'Email')->first(); + $condition = [ + 'actions' => [], + 'conditions' => [ + 'operatorIdentifier' => 'and', + 'children' => [ + [ + 'identifier' => $targetField['id'], + 'value' => [ + 'operator' => 'equals', + 'property_meta' => [ + 'id' => $targetField['id'], + 'type' => 'email', + ], + 'value' => 'test@gmail.com', + ], + ], + ], + ], + ]; + $submissionData = []; + $validationMessage = 'Can only use test@gmail.com'; + $form->properties = collect($form->properties)->map(function ($property) use (&$submissionData, &$condition, &$validationMessage, &$targetField) { + if (in_array($property['name'], ['Name'])) { + $property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage]; + $submissionData[$targetField['id']] = 'fail@gmail.com'; + } + return $property; + })->toArray(); + + $form->update(); + + $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData); + + $this->postJson(route('forms.answer', $form->slug), $formData) + ->assertStatus(422) + ->assertJson([ + 'message' => $validationMessage, + ]); +});