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 <julien@nahum.net>
This commit is contained in:
parent
f9dacd0a74
commit
6673dff504
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use App\Models\Forms\Form;
|
use App\Models\Forms\Form;
|
||||||
|
use App\Rules\CustomFieldValidationRule;
|
||||||
use App\Rules\StorageFile;
|
use App\Rules\StorageFile;
|
||||||
use App\Rules\ValidHCaptcha;
|
use App\Rules\ValidHCaptcha;
|
||||||
use App\Rules\ValidPhoneInputRule;
|
use App\Rules\ValidPhoneInputRule;
|
||||||
|
|
@ -50,18 +51,17 @@ class AnswerFormRequest extends FormRequest
|
||||||
*/
|
*/
|
||||||
public function rules()
|
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) {
|
foreach ($this->form->properties as $property) {
|
||||||
$rules = [];
|
$rules = [];
|
||||||
|
|
||||||
/*if (!$this->form->is_pro) { // If not pro then not check logic
|
/*if (!$this->form->is_pro) { // If not pro then not check logic
|
||||||
$property['logic'] = false;
|
$property['logic'] = false;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// For get values instead of Id for select/multi select options
|
// For get values instead of Id for select/multi select options
|
||||||
$data = $this->toArray();
|
$data = $this->toArray();
|
||||||
$selectionFields = collect($this->form->properties)->filter(function ($pro) {
|
|
||||||
return in_array($pro['type'], ['select', 'multi_select']);
|
|
||||||
});
|
|
||||||
foreach ($selectionFields as $field) {
|
foreach ($selectionFields as $field) {
|
||||||
if (isset($data[$field['id']]) && is_array($data[$field['id']])) {
|
if (isset($data[$field['id']]) && is_array($data[$field['id']])) {
|
||||||
$data[$field['id']] = array_map(function ($val) use ($field) {
|
$data[$field['id']] = array_map(function ($val) use ($field) {
|
||||||
|
|
@ -87,6 +87,11 @@ class AnswerFormRequest extends FormRequest
|
||||||
$rules[] = 'nullable';
|
$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 "."
|
// Clean id to escape "."
|
||||||
$propertyId = $property['id'];
|
$propertyId = $property['id'];
|
||||||
if (in_array($property['type'], ['multi_select'])) {
|
if (in_array($property['type'], ['multi_select'])) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use App\Service\Forms\FormLogicConditionChecker;
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
||||||
|
class CustomFieldValidationRule implements Rule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new rule instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(public array $validation, public array $formData)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the validation rule passes.
|
||||||
|
*
|
||||||
|
* @param string $attribute
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function passes($attribute, $value)
|
||||||
|
{
|
||||||
|
return FormLogicConditionChecker::conditionsMet($this->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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<collapse
|
||||||
|
v-model="show"
|
||||||
|
class="p-2 w-full"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h3 class="font-semibold block text-lg">
|
||||||
|
Validation
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-400 text-xs mb-3">
|
||||||
|
Add some custom validation (save form before testing)
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<div class="py-2">
|
||||||
|
<p class="font-semibold text-sm text-gray-700">
|
||||||
|
Conditions for this field to be accepted
|
||||||
|
</p>
|
||||||
|
<condition-editor
|
||||||
|
ref="filter-editor"
|
||||||
|
v-model="validation.conditions"
|
||||||
|
class="mt-1 border-t border rounded-md mb-3"
|
||||||
|
:form="form"
|
||||||
|
/>
|
||||||
|
<text-input
|
||||||
|
name="error_message"
|
||||||
|
class=""
|
||||||
|
:form="field.validation"
|
||||||
|
label="Error message when validation fails"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</collapse>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConditionEditor from "./form-logic-components/ConditionEditor.client.vue"
|
||||||
|
import { default as _has } from "lodash/has"
|
||||||
|
export default {
|
||||||
|
name: 'FormValidation',
|
||||||
|
components: {ConditionEditor},
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: false,
|
||||||
|
validation: this.field.validation?.error_conditions || {
|
||||||
|
conditions: null,
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
error_message: this.field.validation?.error_message || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
logic: {
|
||||||
|
handler() {
|
||||||
|
this.field.validation.error_conditions = this.validation
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
"field.id": {
|
||||||
|
handler() {
|
||||||
|
// On field change, reset validation
|
||||||
|
this.validation = this.field.validation.error_conditions || {
|
||||||
|
conditions: null,
|
||||||
|
actions: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!_has(this.field, "validation")) {
|
||||||
|
this.field.validation = {
|
||||||
|
error_conditions: this.validation,
|
||||||
|
error_message: this.error_message }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods:{}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -566,6 +566,12 @@
|
||||||
:form="form"
|
:form="form"
|
||||||
:field="field"
|
:field="field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<custom-field-validation
|
||||||
|
class="py-2 px-4 border-b"
|
||||||
|
:form="form"
|
||||||
|
:field="field"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -574,12 +580,13 @@ import timezones from '~/data/timezones.json'
|
||||||
import countryCodes from '~/data/country_codes.json'
|
import countryCodes from '~/data/country_codes.json'
|
||||||
import CountryFlag from 'vue-country-flag-next'
|
import CountryFlag from 'vue-country-flag-next'
|
||||||
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
|
import FormBlockLogicEditor from '../../components/form-logic-components/FormBlockLogicEditor.vue'
|
||||||
|
import CustomFieldValidation from '../../components/CustomFieldValidation.vue'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { default as _has } from 'lodash/has'
|
import { default as _has } from 'lodash/has'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldOptions',
|
name: 'FieldOptions',
|
||||||
components: { CountryFlag, FormBlockLogicEditor },
|
components: { CountryFlag, FormBlockLogicEditor, CustomFieldValidation },
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
use Tests\Helpers\FormSubmissionDataFactory;
|
use Tests\Helpers\FormSubmissionDataFactory;
|
||||||
|
|
||||||
it('can answer a form', function () {
|
it('can answer a form', function () {
|
||||||
|
|
@ -145,3 +146,94 @@ it('can not submit form with future dates', function () {
|
||||||
'message' => 'The Date must be a date before tomorrow.',
|
'message' => '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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue