Improve form property logic validation for checkbox conditions

- Update FormPropertyLogicRule to handle operators without values
- Add support for checkbox conditions like 'is_checked' and 'is_not_checked'
- Refactor logic validation in both API and client-side implementations
- Remove unnecessary console.log statements
- Update error modal text for better user experience
This commit is contained in:
Julien Nahum 2025-02-19 15:11:27 +01:00
parent efd31133cc
commit 28248259be
7 changed files with 121 additions and 52 deletions

View File

@ -42,73 +42,72 @@ class FormPropertyLogicRule implements DataAwareRule, ValidationRule
if (!isset($condition['value'])) { if (!isset($condition['value'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition body'; $this->conditionErrors[] = 'missing condition body';
return; return;
} }
if (!isset($condition['value']['property_meta'])) { if (!isset($condition['value']['property_meta'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition property'; $this->conditionErrors[] = 'missing condition property';
return; return;
} }
if (!isset($condition['value']['property_meta']['type'])) { if (!isset($condition['value']['property_meta']['type'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition property type'; $this->conditionErrors[] = 'missing condition property type';
return; return;
} }
if (!isset($condition['value']['operator'])) { if (!isset($condition['value']['operator'])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition operator'; $this->conditionErrors[] = 'missing condition operator';
return;
}
if (!isset($condition['value']['value'])) {
$this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition value';
return; return;
} }
$typeField = $condition['value']['property_meta']['type']; $typeField = $condition['value']['property_meta']['type'];
$operator = $condition['value']['operator']; $operator = $condition['value']['operator'];
$this->operator = $operator; $this->operator = $operator;
$value = $condition['value']['value'];
if (!isset(self::getConditionMapping()[$typeField])) { if (!isset(self::getConditionMapping()[$typeField])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'configuration not found for condition type'; $this->conditionErrors[] = 'configuration not found for condition type';
return; return;
} }
if (!isset(self::getConditionMapping()[$typeField]['comparators'][$operator])) { if (!isset(self::getConditionMapping()[$typeField]['comparators'][$operator])) {
$this->isConditionCorrect = false; $this->isConditionCorrect = false;
$this->conditionErrors[] = 'configuration not found for condition operator'; $this->conditionErrors[] = 'configuration not found for condition operator';
return; return;
} }
$type = self::getConditionMapping()[$typeField]['comparators'][$operator]['expected_type'] ?? null; $comparatorDef = self::getConditionMapping()[$typeField]['comparators'][$operator];
$needsValue = !empty((array)$comparatorDef);
if (is_array($type)) { if ($needsValue && !isset($condition['value']['value'])) {
$foundCorrectType = false; $this->isConditionCorrect = false;
foreach ($type as $subtype) { $this->conditionErrors[] = 'missing condition value';
if ($this->valueHasCorrectType($subtype, $value)) { return;
$foundCorrectType = true; }
if ($needsValue) {
$type = $comparatorDef['expected_type'] ?? null;
$value = $condition['value']['value'];
if (is_array($type)) {
$foundCorrectType = false;
foreach ($type as $subtype) {
if ($this->valueHasCorrectType($subtype, $value)) {
$foundCorrectType = true;
}
}
if (!$foundCorrectType) {
$this->isConditionCorrect = false;
$this->conditionErrors[] = 'wrong type of condition value';
}
} else {
if (!$this->valueHasCorrectType($type, $value)) {
$this->isConditionCorrect = false;
$this->conditionErrors[] = 'wrong type of condition value';
} }
}
if (!$foundCorrectType) {
$this->isConditionCorrect = false;
}
} else {
if (!$this->valueHasCorrectType($type, $value)) {
$this->isConditionCorrect = false;
$this->conditionErrors[] = 'wrong type of condition value';
} }
} }
} }

View File

@ -199,3 +199,57 @@ it('can validate form logic rules for conditions', function () {
$this->assertFalse($validatorObj->passes()); $this->assertFalse($validatorObj->passes());
expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe('The logic conditions for Name are not complete. Error detail(s): missing operator'); expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe('The logic conditions for Name are not complete. Error detail(s): missing operator');
}); });
it('can validate form logic rules for operators without values', function () {
$rules = [
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()],
];
// Test checkbox is_checked without value
$data = [
'properties' => [
[
'id' => 'checkbox1',
'name' => 'Checkbox Field',
'type' => 'checkbox',
'logic' => [
'conditions' => [
'operatorIdentifier' => 'and',
'children' => [
[
'identifier' => 'test-id',
'value' => [
'operator' => 'is_checked',
'property_meta' => [
'id' => 'test-id',
'type' => 'checkbox'
]
]
]
]
],
'actions' => ['show-block']
]
]
]
];
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertTrue($validatorObj->passes());
// Test checkbox is_checked with value (should still pass for backward compatibility)
$data['properties'][0]['logic']['conditions']['children'][0]['value']['value'] = true;
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertTrue($validatorObj->passes());
// Test checkbox is_not_checked without value
$data['properties'][0]['logic']['conditions']['children'][0]['value']['operator'] = 'is_not_checked';
unset($data['properties'][0]['logic']['conditions']['children'][0]['value']['value']);
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertTrue($validatorObj->passes());
// Test checkbox with operator that doesn't exist
$data['properties'][0]['logic']['conditions']['children'][0]['value']['operator'] = 'invalid_operator';
$validatorObj = $this->app['validator']->make($data, $rules);
$this->assertFalse($validatorObj->passes());
expect($validatorObj->errors()->messages()['properties.0.logic'][0])->toBe('The logic conditions for Checkbox Field are not complete. Error detail(s): configuration not found for condition operator');
});

View File

@ -236,7 +236,6 @@ export default {
saveForm() { saveForm() {
// Apply defaults to the form // Apply defaults to the form
const defaultedData = setFormDefaults(this.form.data()) const defaultedData = setFormDefaults(this.form.data())
console.log('defaultedData', defaultedData)
this.form.fill(defaultedData) this.form.fill(defaultedData)
this.form.properties = validatePropertiesLogic(this.form.properties) this.form.properties = validatePropertiesLogic(this.form.properties)

View File

@ -4,8 +4,8 @@
@close="$emit('close')" @close="$emit('close')"
> >
<div class="-mx-5"> <div class="-mx-5">
<h2 class="text-red-600 text-2xl font-bold mb-4 px-4"> <h2 class="text-red-600 text-2xl font-medium mb-4 px-4">
Error saving your form We couldn't save your form
</h2> </h2>
<div <div

View File

@ -833,7 +833,11 @@ export default {
// Apply type-specific defaults from blocks_types.json if available // Apply type-specific defaults from blocks_types.json if available
if (this.field.type in blocksTypes && blocksTypes[this.field.type]?.default_values) { if (this.field.type in blocksTypes && blocksTypes[this.field.type]?.default_values) {
Object.assign(this.field, blocksTypes[this.field.type].default_values) Object.keys(blocksTypes[this.field.type].default_values).forEach(key => {
if (!_has(this.field, key)) {
this.field[key] = blocksTypes[this.field.type].default_values[key]
}
})
} }
// Apply additional defaults from defaultFieldValues if needed // Apply additional defaults from defaultFieldValues if needed

View File

@ -3,6 +3,8 @@ import FormPropertyLogicRule from "~/lib/forms/FormPropertyLogicRule.js"
export const validatePropertiesLogic = (properties) => { export const validatePropertiesLogic = (properties) => {
properties.forEach((field) => { properties.forEach((field) => {
const isValid = new FormPropertyLogicRule(field).isValid() const isValid = new FormPropertyLogicRule(field).isValid()
console.log('field', field)
console.log('isValid', isValid, field.name)
if (!isValid) { if (!isValid) {
field.logic = { field.logic = {
conditions: null, conditions: null,

View File

@ -22,6 +22,7 @@ class FormPropertyLogicRule {
isValid() { isValid() {
if (this.logic && this.logic["conditions"]) { if (this.logic && this.logic["conditions"]) {
console.log('logic', this.logic)
this.checkConditions(this.logic["conditions"]) this.checkConditions(this.logic["conditions"])
this.checkActions( this.checkActions(
this.logic && this.logic["actions"] ? this.logic["actions"] : null, this.logic && this.logic["actions"] ? this.logic["actions"] : null,
@ -62,8 +63,7 @@ class FormPropertyLogicRule {
condition["value"] === undefined || condition["value"] === undefined ||
condition["value"]["property_meta"] === undefined || condition["value"]["property_meta"] === undefined ||
condition["value"]["property_meta"]["type"] === undefined || condition["value"]["property_meta"]["type"] === undefined ||
condition["value"]["operator"] === undefined || condition["value"]["operator"] === undefined
condition["value"]["value"] === undefined
) { ) {
this.isConditionCorrect = false this.isConditionCorrect = false
return return
@ -71,7 +71,6 @@ class FormPropertyLogicRule {
const typeField = condition["value"]["property_meta"]["type"] const typeField = condition["value"]["property_meta"]["type"]
const operator = condition["value"]["operator"] const operator = condition["value"]["operator"]
const value = condition["value"]["value"]
if ( if (
this.CONDITION_MAPPING[typeField] === undefined || this.CONDITION_MAPPING[typeField] === undefined ||
@ -81,23 +80,34 @@ class FormPropertyLogicRule {
return return
} }
const type = // Check if operator needs a value based on comparator definition
this.CONDITION_MAPPING[typeField]["comparators"][operator][ const comparatorDef = this.CONDITION_MAPPING[typeField]["comparators"][operator]
"expected_type" const needsValue = Object.keys(comparatorDef).length > 0
]
if (Array.isArray(type)) { if (needsValue && condition["value"]["value"] === undefined) {
let foundCorrectType = false this.isConditionCorrect = false
type.forEach((subtype) => { return
if (this.valueHasCorrectType(subtype, value)) { }
foundCorrectType = true
// Only check value type if comparator expects one
if (needsValue) {
const type = comparatorDef["expected_type"]
const value = condition["value"]["value"]
if (Array.isArray(type)) {
let foundCorrectType = false
type.forEach((subtype) => {
if (this.valueHasCorrectType(subtype, value)) {
foundCorrectType = true
}
})
if (!foundCorrectType) {
this.isConditionCorrect = false
}
} else {
if (!this.valueHasCorrectType(type, value)) {
this.isConditionCorrect = false
} }
})
if (!foundCorrectType) {
this.isConditionCorrect = false
}
} else {
if (!this.valueHasCorrectType(type, value)) {
this.isConditionCorrect = false
} }
} }
} }
@ -151,3 +161,4 @@ class FormPropertyLogicRule {
} }
export default FormPropertyLogicRule export default FormPropertyLogicRule