Add 'exists_in_submissions' and 'does_not_exist_in_submissions' valid… (#725)
* Add 'exists_in_submissions' and 'does_not_exist_in_submissions' validation conditions - Implement new validation conditions in FormLogicConditionChecker to check for existing submissions. - Update open_filters.json and client-side filters to include the new conditions. - Enhance FormLogicTest with test cases for the new validation conditions. - Modify UI components to support the new logic conditions in form validation. * Fix FormLogicConditionChecker * Improve custom_validation_only * fix test --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
2c746437c9
commit
fba2207e0f
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace App\Service\Forms;
|
namespace App\Service\Forms;
|
||||||
|
|
||||||
|
use App\Models\Forms\FormSubmission;
|
||||||
|
|
||||||
class FormLogicConditionChecker
|
class FormLogicConditionChecker
|
||||||
{
|
{
|
||||||
public function __construct(private ?array $conditions, private ?array $formData)
|
public function __construct(private ?array $conditions, private ?array $formData)
|
||||||
|
|
@ -304,6 +306,48 @@ class FormLogicConditionChecker
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkExistsInSubmissions($condition, $fieldValue): bool
|
||||||
|
{
|
||||||
|
if (!$fieldValue || !isset($condition['property_meta']['id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formId = $this->formData['form']['id'] ?? null;
|
||||||
|
if (!$formId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormSubmission::where('form_id', $formId)
|
||||||
|
->where(function ($query) use ($condition, $fieldValue) {
|
||||||
|
$fieldId = $condition['property_meta']['id'];
|
||||||
|
|
||||||
|
if (env('DB_CONNECTION') == 'mysql') {
|
||||||
|
// For scalar values
|
||||||
|
$query->where(function ($q) use ($fieldId, $fieldValue) {
|
||||||
|
$q->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(data, '$.\"$fieldId\"')) = ?", [$fieldValue]);
|
||||||
|
|
||||||
|
// For array values
|
||||||
|
if (is_array($fieldValue)) {
|
||||||
|
$q->orWhereRaw("JSON_CONTAINS(JSON_EXTRACT(data, '$.\"$fieldId\"'), ?)", [json_encode($fieldValue)]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$query->where(function ($q) use ($fieldId, $fieldValue) {
|
||||||
|
// For scalar values
|
||||||
|
$q->whereRaw("data->? = ?::jsonb", [$fieldId, json_encode($fieldValue)]);
|
||||||
|
|
||||||
|
// For array values
|
||||||
|
if (is_array($fieldValue)) {
|
||||||
|
$q->orWhereRaw("data->? @> ?::jsonb", [
|
||||||
|
$fieldId,
|
||||||
|
json_encode($fieldValue)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})->exists();
|
||||||
|
}
|
||||||
|
|
||||||
private function textConditionMet(array $propertyCondition, $value): bool
|
private function textConditionMet(array $propertyCondition, $value): bool
|
||||||
{
|
{
|
||||||
switch ($propertyCondition['operator']) {
|
switch ($propertyCondition['operator']) {
|
||||||
|
|
@ -348,6 +392,10 @@ class FormLogicConditionChecker
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 'exists_in_submissions':
|
||||||
|
return $this->checkExistsInSubmissions($propertyCondition, $value);
|
||||||
|
case 'does_not_exist_in_submissions':
|
||||||
|
return !$this->checkExistsInSubmissions($propertyCondition, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -384,6 +432,10 @@ class FormLogicConditionChecker
|
||||||
return $this->checkLength($propertyCondition, $value, '<');
|
return $this->checkLength($propertyCondition, $value, '<');
|
||||||
case 'content_length_less_than_or_equal_to':
|
case 'content_length_less_than_or_equal_to':
|
||||||
return $this->checkLength($propertyCondition, $value, '<=');
|
return $this->checkLength($propertyCondition, $value, '<=');
|
||||||
|
case 'exists_in_submissions':
|
||||||
|
return $this->checkExistsInSubmissions($propertyCondition, $value);
|
||||||
|
case 'does_not_exist_in_submissions':
|
||||||
|
return !$this->checkExistsInSubmissions($propertyCondition, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -136,6 +152,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -206,6 +238,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -276,6 +324,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -362,6 +426,22 @@
|
||||||
},
|
},
|
||||||
"content_length_less_than_or_equal_to": {
|
"content_length_less_than_or_equal_to": {
|
||||||
"expected_type": "number"
|
"expected_type": "number"
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -607,3 +607,102 @@ it('skips validation for fields hidden by logic conditions', function () {
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('cannot submit form with failed exists_in_submissions validation condition', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->createForm($user, $workspace);
|
||||||
|
$targetField = collect($form->properties)->where('name', 'Email')->first();
|
||||||
|
|
||||||
|
// First set up the validation condition
|
||||||
|
$condition = [
|
||||||
|
'actions' => [],
|
||||||
|
'conditions' => [
|
||||||
|
'operatorIdentifier' => 'and',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'identifier' => $targetField['id'],
|
||||||
|
'value' => [
|
||||||
|
'operator' => 'exists_in_submissions',
|
||||||
|
'property_meta' => [
|
||||||
|
'id' => $targetField['id'],
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$validationMessage = 'Email already exists in previous submissions';
|
||||||
|
|
||||||
|
$form->properties = collect($form->properties)->map(function ($property) use ($condition, $validationMessage, $targetField) {
|
||||||
|
if (in_array($property['name'], ['Name'])) {
|
||||||
|
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage];
|
||||||
|
}
|
||||||
|
return $property;
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$form->update();
|
||||||
|
|
||||||
|
$formData = [$targetField['id'] => 'existing@test.com'];
|
||||||
|
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'message' => $validationMessage,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot submit form with failed does_not_exist_in_submissions validation condition', function () {
|
||||||
|
$user = $this->actingAsUser();
|
||||||
|
$workspace = $this->createUserWorkspace($user);
|
||||||
|
$form = $this->createForm($user, $workspace);
|
||||||
|
$targetField = collect($form->properties)->where('name', 'Email')->first();
|
||||||
|
|
||||||
|
// First set up the validation condition
|
||||||
|
$condition = [
|
||||||
|
'actions' => [],
|
||||||
|
'conditions' => [
|
||||||
|
'operatorIdentifier' => 'and',
|
||||||
|
'children' => [
|
||||||
|
[
|
||||||
|
'identifier' => $targetField['id'],
|
||||||
|
'value' => [
|
||||||
|
'operator' => 'does_not_exist_in_submissions',
|
||||||
|
'property_meta' => [
|
||||||
|
'id' => $targetField['id'],
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$validationMessage = 'Email already exists in previous submissions';
|
||||||
|
|
||||||
|
$form->properties = collect($form->properties)->map(function ($property) use ($condition, $validationMessage, $targetField) {
|
||||||
|
if (in_array($property['name'], ['Name'])) {
|
||||||
|
$property['validation'] = ['error_conditions' => $condition, 'error_message' => $validationMessage];
|
||||||
|
}
|
||||||
|
return $property;
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$form->update();
|
||||||
|
|
||||||
|
$formData = [$targetField['id'] => 'existing@test.com'];
|
||||||
|
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'Form submission saved.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJson([
|
||||||
|
'message' => $validationMessage,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
v-model="validation.conditions"
|
v-model="validation.conditions"
|
||||||
class="mt-1 border-t border rounded-md"
|
class="mt-1 border-t border rounded-md"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
:custom-validation="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export default {
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
modelValue: { type: Object, required: true },
|
modelValue: { type: Object, required: true },
|
||||||
|
customValidation: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
|
|
@ -102,14 +103,14 @@ export default {
|
||||||
return componentData
|
return componentData
|
||||||
},
|
},
|
||||||
operators() {
|
operators() {
|
||||||
return Object.keys(
|
return Object.entries(this.available_filters[this.property.type].comparators)
|
||||||
this.available_filters[this.property.type].comparators,
|
.filter(([key, value]) => this.customValidation || (!this.customValidation && !value.custom_validation_only))
|
||||||
).map((key) => {
|
.map(([key]) => {
|
||||||
return {
|
return {
|
||||||
value: key,
|
value: key,
|
||||||
name: this.optionFilterNames(key),
|
name: this.optionFilterNames(key),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
needsInput() {
|
needsInput() {
|
||||||
const operator = this.selectedOperator()
|
const operator = this.selectedOperator()
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
form: { type: Object, required: true },
|
form: { type: Object, required: true },
|
||||||
modelValue: { type: Object, required: false },
|
modelValue: { type: Object, required: false },
|
||||||
|
customValidation: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
|
|
||||||
|
|
@ -79,12 +80,19 @@ export default {
|
||||||
.map((property) => {
|
.map((property) => {
|
||||||
const workspaceId = this.form.workspace_id
|
const workspaceId = this.form.workspace_id
|
||||||
const formSlug = this.form.slug
|
const formSlug = this.form.slug
|
||||||
|
const customValidation = this.customValidation
|
||||||
return {
|
return {
|
||||||
identifier: property.id,
|
identifier: property.id,
|
||||||
name: property.name,
|
name: property.name,
|
||||||
component: (function () {
|
component: (function () {
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
extends: ColumnCondition,
|
extends: ColumnCondition,
|
||||||
|
props: {
|
||||||
|
customValidation: {
|
||||||
|
type: Boolean,
|
||||||
|
default: customValidation
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
property() {
|
property() {
|
||||||
return property
|
return property
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -136,6 +152,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -206,6 +238,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -276,6 +324,22 @@
|
||||||
"format": {
|
"format": {
|
||||||
"type": "regex"
|
"type": "regex"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -362,6 +426,22 @@
|
||||||
},
|
},
|
||||||
"content_length_less_than_or_equal_to": {
|
"content_length_less_than_or_equal_to": {
|
||||||
"expected_type": "number"
|
"expected_type": "number"
|
||||||
|
},
|
||||||
|
"exists_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
|
},
|
||||||
|
"does_not_exist_in_submissions": {
|
||||||
|
"expected_type": "boolean",
|
||||||
|
"format": {
|
||||||
|
"type": "enum",
|
||||||
|
"values": [true]
|
||||||
|
},
|
||||||
|
"custom_validation_only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue