diff --git a/app/Rules/FormPropertyLogicRule.php b/app/Rules/FormPropertyLogicRule.php index 21fc6a447..222e8fbaa 100644 --- a/app/Rules/FormPropertyLogicRule.php +++ b/app/Rules/FormPropertyLogicRule.php @@ -605,6 +605,8 @@ private function checkActions($conditions) break; } } + } else { + $this->isActionCorrect = false; } } @@ -620,10 +622,9 @@ public function passes($attribute, $value) $this->setProperty($attribute); if(isset($value["conditions"])){ $this->checkConditions($value["conditions"]); + $this->checkActions($value['actions'] ?? null); } - if(isset($value["actions"])){ - $this->checkActions($value["actions"]); - } + return ($this->isConditionCorrect && $this->isActionCorrect); } diff --git a/resources/data/open_filters.json b/resources/data/open_filters.json index 16291fb66..564a867f9 100644 --- a/resources/data/open_filters.json +++ b/resources/data/open_filters.json @@ -342,13 +342,13 @@ "multi_select": { "comparators": { "contains": { - "expected_type": "object", + "expected_type": ["object", "string"], "format": { "type": "uuid" } }, "does_not_contain": { - "expected_type": "object", + "expected_type": ["object", "string"], "format": { "type": "uuid" } diff --git a/resources/js/components/open/forms/components/FormEditor.vue b/resources/js/components/open/forms/components/FormEditor.vue index 513c09688..83335ca2f 100644 --- a/resources/js/components/open/forms/components/FormEditor.vue +++ b/resources/js/components/open/forms/components/FormEditor.vue @@ -69,6 +69,7 @@ import FormEditorPreview from './form-components/FormEditorPreview.vue' import FormSecurityPrivacy from './form-components/FormSecurityPrivacy.vue' import FormCustomSeo from './form-components/FormCustomSeo.vue' import saveUpdateAlert from '../../../../mixins/forms/saveUpdateAlert.js' +import fieldsLogic from '../../../../mixins/forms/fieldsLogic.js' export default { name: 'FormEditor', @@ -85,7 +86,7 @@ export default { FormSecurityPrivacy, FormCustomSeo }, - mixins: [saveUpdateAlert], + mixins: [saveUpdateAlert, fieldsLogic], props: { isEdit: { required: false, @@ -176,6 +177,7 @@ export default { this.showFormErrorModal = true }, saveForm() { + this.form.properties = this.validateFieldsLogic(this.form.properties) if(this.isGuest) { this.saveFormGuest() } else if (this.isEdit) { diff --git a/resources/js/forms/FormPropertyLogicRule.js b/resources/js/forms/FormPropertyLogicRule.js new file mode 100644 index 000000000..f7ce4b654 --- /dev/null +++ b/resources/js/forms/FormPropertyLogicRule.js @@ -0,0 +1,123 @@ +import OpenFilters from '../../data/open_filters.json' +class FormPropertyLogicRule { + property = null + logic = null + isConditionCorrect = true + isActionCorrect = true + ACTIONS_VALUES = [ + 'show-block', + 'hide-block', + 'make-it-optional', + 'require-answer', + 'enable-block', + 'disable-block' + ] + CONDITION_MAPPING = OpenFilters + + constructor (property) { + this.property = property + this.logic = (property.logic !== undefined && property.logic) ? property.logic : null + } + + isValid () { + if (this.logic && this.logic['conditions']) { + this.checkConditions(this.logic['conditions']) + this.checkActions((this.logic && this.logic['actions']) ? this.logic['actions'] : null) + } + + return this.isConditionCorrect && this.isActionCorrect + } + + checkConditions (conditions) { + if (conditions && conditions['operatorIdentifier']) { + if ((conditions['operatorIdentifier'] !== 'and') && (conditions['operatorIdentifier'] !== 'or')) { + this.isConditionCorrect = false + return + } + + if (conditions['operatorIdentifier']['children'] !== undefined || !Array.isArray(conditions['children'])) { + this.isConditionCorrect = false + return + } + + conditions['children'].forEach(childrenCondition => { + this.checkConditions(childrenCondition) + }) + } else if (conditions && conditions['identifier']) { + this.checkBaseCondition(conditions) + } + } + + checkBaseCondition (condition) { + if (condition['value'] === undefined || + condition['value']['property_meta'] === undefined || + condition['value']['property_meta']['type'] === undefined || + condition['value']['operator'] === undefined || + condition['value']['value'] === undefined + ) { + this.isConditionCorrect = false + return + } + + const typeField = condition['value']['property_meta']['type'] + const operator = condition['value']['operator'] + const value = condition['value']['value'] + + if (this.CONDITION_MAPPING[typeField] === undefined || + this.CONDITION_MAPPING[typeField]['comparators'][operator] === undefined + ) { + this.isConditionCorrect = false + return + } + + const type = this.CONDITION_MAPPING[typeField]['comparators'][operator]['expected_type'] + 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 + } + } + } + + valueHasCorrectType (type, value) { + if ( + (type === 'string' && typeof value !== 'string') || + (type === 'boolean' && typeof value !== 'boolean') || + (type === 'number' && typeof value !== 'number') || + (type === 'object' && !Array.isArray(value)) + ) { + return false + } + return true + } + + checkActions (conditions) { + if (Array.isArray(conditions) && conditions.length > 0) { + conditions.forEach(val => { + if (this.ACTIONS_VALUES.indexOf(val) === -1 || + (['nf-text', 'nf-code', 'nf-page-break', 'nf-divider', 'nf-image'].indexOf(this.property["type"]) > -1 && ['hide-block', 'show-block'].indexOf(val) === -1) || + (this.property["hidden"] !== undefined && this.property["hidden"] && ['show-block', 'require-answer'].indexOf(val) === -1) || + (this.property["required"] !== undefined && this.property["required"] && ['make-it-optional', 'hide-block', 'disable-block'].indexOf(val) === -1) || + (this.property["disabled"] !== undefined && this.property["disabled"] && ['enable-block', 'require-answer', 'make-it-optional'].indexOf(val) === -1) + ) { + this.isActionCorrect = false + return + } + }) + } else { + this.isActionCorrect = false + } + } + +} + +export default FormPropertyLogicRule \ No newline at end of file diff --git a/resources/js/mixins/forms/fieldsLogic.js b/resources/js/mixins/forms/fieldsLogic.js new file mode 100644 index 000000000..014ee652e --- /dev/null +++ b/resources/js/mixins/forms/fieldsLogic.js @@ -0,0 +1,17 @@ +import FormPropertyLogicRule from '../../forms/FormPropertyLogicRule.js' +export default { + methods: { + validateFieldsLogic (properties) { + properties.forEach((field) => { + const isValid = (new FormPropertyLogicRule(field)).isValid() + if(!isValid){ + field.logic = { + conditions: null, + actions: [] + } + } + }) + return properties + } + } +} diff --git a/tests/Feature/Forms/FormPropertyLogicTest.php b/tests/Feature/Forms/FormPropertyLogicTest.php index ad3b1d7f2..41a4c86b4 100644 --- a/tests/Feature/Forms/FormPropertyLogicTest.php +++ b/tests/Feature/Forms/FormPropertyLogicTest.php @@ -34,7 +34,22 @@ 'hidden' => true, 'required' => false, 'logic' => [ - "conditions" => null, + "conditions" => [ + "operatorIdentifier"=> "and", + "children"=> [ + [ + "identifier"=> "title", + "value"=> [ + "operator"=> "equals", + "property_meta"=> [ + "id"=> "title", + "type"=> "text" + ], + "value"=> "TEST" + ] + ] + ] + ], "actions" => ['hide-block'] ] ] @@ -51,7 +66,22 @@ 'name' => "Custom Test", 'type' => 'nf-text', 'logic' => [ - "conditions" => null, + "conditions" => [ + "operatorIdentifier"=> "and", + "children"=> [ + [ + "identifier"=> "title", + "value"=> [ + "operator"=> "equals", + "property_meta"=> [ + "id"=> "title", + "type"=> "text" + ], + "value"=> "TEST" + ] + ] + ] + ], "actions" => ['require-answer'] ] ] @@ -93,7 +123,7 @@ ] ] ], - "actions" => [] + "actions" => ['hide-block'] ] ] ] @@ -126,7 +156,7 @@ ] ] ], - "actions" => [] + "actions" => ['hide-block'] ] ] ] @@ -160,7 +190,7 @@ ] ] ], - "actions" => [] + "actions" => ['hide-block'] ] ] ]