From ee54214fc7ba9a5c71a0799370144755be58ef01 Mon Sep 17 00:00:00 2001 From: Brendan Bond Date: Sat, 7 Sep 2024 15:41:14 -0500 Subject: [PATCH] Merge pull request #105 from formio/FIO-8414/5x_validation_on_required_datagrid FIO-8414: Fix required validation not working in Data Grid --- .../fixtures/forDataGridRequired.json | 91 +++++++++++++++++++ src/process/__tests__/fixtures/index.ts | 3 +- src/process/__tests__/process.test.ts | 23 ++++- .../conditions/__tests__/conditions.test.ts | 17 ++-- .../validation/rules/validateRequired.ts | 6 +- src/process/validation/util.ts | 29 ++++++ src/utils/conditions.ts | 22 ++--- src/utils/formUtil.ts | 4 +- src/utils/operators/IsEmptyValue.js | 13 +-- 9 files changed, 174 insertions(+), 34 deletions(-) create mode 100644 src/process/__tests__/fixtures/forDataGridRequired.json diff --git a/src/process/__tests__/fixtures/forDataGridRequired.json b/src/process/__tests__/fixtures/forDataGridRequired.json new file mode 100644 index 00000000..30aec20e --- /dev/null +++ b/src/process/__tests__/fixtures/forDataGridRequired.json @@ -0,0 +1,91 @@ +{ + "form": { + "name": "dsf", + "path": "dsf", + "type": "form", + "display": "form", + "components": [ + { + "label": "Data Grid", + "reorder": false, + "addAnotherPosition": "bottom", + "layoutFixed": false, + "enableRowGroups": false, + "initEmpty": false, + "tableView": false, + "defaultValue": [ + {} + ], + "validate": { + "required": true + }, + "validateWhenHidden": false, + "key": "dataGrid", + "type": "datagrid", + "input": true, + "components": [ + { + "label": "Columns", + "columns": [ + { + "components": [ + { + "label": "Text Field", + "applyMaskOn": "change", + "tableView": true, + "validateWhenHidden": false, + "key": "textField", + "type": "textfield", + "input": true + } + ], + "width": 6, + "offset": 0, + "push": 0, + "pull": 0, + "size": "md", + "currentWidth": 6 + }, + { + "components": [], + "width": 6, + "offset": 0, + "push": 0, + "pull": 0, + "size": "md", + "currentWidth": 6 + } + ], + "key": "columns", + "type": "columns", + "input": false, + "tableView": false + } + ] + }, + { + "type": "button", + "label": "Submit", + "key": "submit", + "disableOnInvalid": true, + "input": true, + "tableView": false + } + ], + "created": "2024-08-07T08:41:53.926Z", + "modified": "2024-08-07T08:41:53.932Z", + "machineName": "tbtzzegecytgzpi:dsf" + }, + "submission": { + "data": { + + "dataGrid": [ + { + "textField": "" + } + ], + "submit": true + } + + } +} \ No newline at end of file diff --git a/src/process/__tests__/fixtures/index.ts b/src/process/__tests__/fixtures/index.ts index 05ef6349..b9aaf101 100644 --- a/src/process/__tests__/fixtures/index.ts +++ b/src/process/__tests__/fixtures/index.ts @@ -3,8 +3,9 @@ import clearOnHideWithHiddenParent from './clearOnHideWithHiddenParent.json'; import skipValidForConditionallyHiddenComp from './skipValidForConditionallyHiddenComp.json'; import skipValidForLogicallyHiddenComp from './skipValidForLogicallyHiddenComp.json'; import skipValidWithHiddenParentComp from './skipValidWithHiddenParentComp.json'; +import forDataGridRequired from './forDataGridRequired.json'; import data1a from './data1a.json'; import form1 from './form1.json'; import subs from './subs.json'; -export { clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, skipValidForLogicallyHiddenComp, skipValidForConditionallyHiddenComp, skipValidWithHiddenParentComp, data1a, form1, subs }; +export { clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, skipValidForLogicallyHiddenComp, skipValidForConditionallyHiddenComp, skipValidWithHiddenParentComp, forDataGridRequired, data1a, form1, subs }; diff --git a/src/process/__tests__/process.test.ts b/src/process/__tests__/process.test.ts index 726cee92..8f4c9118 100644 --- a/src/process/__tests__/process.test.ts +++ b/src/process/__tests__/process.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert' import type { ContainerComponent, ValidationScope } from 'types'; import { getComponent } from 'utils/formUtil'; import { process, processSync, ProcessTargets } from '../index'; -import { clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, skipValidForConditionallyHiddenComp, skipValidForLogicallyHiddenComp, skipValidWithHiddenParentComp } from './fixtures' +import { clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, forDataGridRequired, skipValidForConditionallyHiddenComp, skipValidForLogicallyHiddenComp, skipValidWithHiddenParentComp } from './fixtures' /* describe('Process Tests', () => { it('Should perform the processes using the processReduced method.', async () => { @@ -3646,6 +3646,27 @@ describe('Process Tests', () => { }); }); + it('Should validate when all child components are empty in required Data Grid', async () => { + const { form, submission } = forDataGridRequired; + const context = { + form, + submission, + data: submission.data, + components: form.components, + processors: ProcessTargets.submission, + scope: {}, + config: { + server: true, + }, + }; + + processSync(context); + context.processors = ProcessTargets.evaluator; + processSync(context); + + expect((context.scope as ValidationScope).errors).to.have.length(1); + }); + describe('For EditGrid:', () => { const components = [ { diff --git a/src/process/conditions/__tests__/conditions.test.ts b/src/process/conditions/__tests__/conditions.test.ts index 4ede936e..9b7441a5 100644 --- a/src/process/conditions/__tests__/conditions.test.ts +++ b/src/process/conditions/__tests__/conditions.test.ts @@ -5,14 +5,15 @@ import { ConditionsScope, ProcessContext } from 'types'; import { get } from 'lodash'; const processForm = (form: any, submission: any) => { - const context: ProcessContext = { - processors: [conditionProcessInfo], - components: form.components, - data: submission.data, - scope: {}, - }; - processSync(context); - return context; + const context: ProcessContext = { + processors: [conditionProcessInfo], + components: form.components, + data: submission.data, + form, + scope: {} + }; + processSync(context); + return context; }; describe('Condition processor', () => { diff --git a/src/process/validation/rules/validateRequired.ts b/src/process/validation/rules/validateRequired.ts index 23733e8a..5ceb1341 100644 --- a/src/process/validation/rules/validateRequired.ts +++ b/src/process/validation/rules/validateRequired.ts @@ -7,7 +7,7 @@ import { AddressComponent, DayComponent } from 'types'; -import { isEmptyObject } from '../util'; +import { isEmptyObject, doesArrayDataHaveValue } from '../util'; import { isComponentNestedDataType } from 'utils/formUtil'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; @@ -47,6 +47,10 @@ const valueIsPresent = (value: any, considerFalseTruthy: boolean, isNestedDataty else if (typeof value === 'object' && !isNestedDatatype) { return Object.values(value).some((val) => valueIsPresent(val, considerFalseTruthy, isNestedDatatype)); } + // If value is an array, check it's children have value + else if (Array.isArray(value) && value.length) { + return doesArrayDataHaveValue(value); + } return true; } diff --git a/src/process/validation/util.ts b/src/process/validation/util.ts index 78e665e4..9be41fc4 100644 --- a/src/process/validation/util.ts +++ b/src/process/validation/util.ts @@ -2,6 +2,9 @@ import { FieldError } from 'error'; import { Component, ValidationContext } from 'types'; import { Evaluator, unescapeHTML } from 'utils'; import { VALIDATION_ERRORS } from './i18n'; +import _isEmpty from 'lodash/isEmpty'; +import _isObject from 'lodash/isObject'; +import _isPlainObject from 'lodash/isPlainObject'; export function isComponentPersistent(component: Component) { return component.persistent ? component.persistent : true; @@ -92,3 +95,29 @@ export const interpolateErrors = (errors: FieldError[], lang: string = 'en') => }; }); }; + +export const hasValue = (value: any) => { + if (_isObject(value)) { + return !_isEmpty(value); + } + + return (typeof value === 'number' && !Number.isNaN(value)) || !!value; +} + +export const doesArrayDataHaveValue = (dataValue: any[] = []): boolean => { + if (!Array.isArray(dataValue)) { + return !!dataValue; + } + + if (!dataValue.length) { + return false; + } + + const isArrayDataComponent = dataValue.every(_isPlainObject); + + if (isArrayDataComponent) { + return dataValue.some(value => Object.values(value).some(hasValue)); + } + + return dataValue.some(hasValue); +}; diff --git a/src/utils/conditions.ts b/src/utils/conditions.ts index ae76e48d..ade9d279 100644 --- a/src/utils/conditions.ts +++ b/src/utils/conditions.ts @@ -50,11 +50,11 @@ export function checkCustomConditional(condition: string, context: ConditionsCon /** * Checks the legacy conditionals. - * - * @param conditional - * @param context - * @param checkDefault - * @returns + * + * @param conditional + * @param context + * @param checkDefault + * @returns */ export function checkLegacyConditional(conditional: LegacyConditional, context: ConditionsContext): boolean | null { const { row, data, component } = context; @@ -75,9 +75,9 @@ export function checkLegacyConditional(conditional: LegacyConditional, context: /** * Checks the JSON Conditionals. - * @param conditional + * @param conditional * @param context - * @returns + * @returns */ export function checkJsonConditional(conditional: JSONConditional, context: ConditionsContext): boolean | null { const { evalContext } = context; @@ -90,9 +90,9 @@ export function checkJsonConditional(conditional: JSONConditional, context: Cond /** * Checks the simple conditionals. - * @param conditional - * @param context - * @returns + * @param conditional + * @param context + * @returns */ export function checkSimpleConditional(conditional: SimpleConditional, context: ConditionsContext): boolean | null { const { component, data, row, instance, form, components = [] } = context; @@ -116,7 +116,7 @@ export function checkSimpleConditional(conditional: SimpleConditional, context: const ConditionOperator = ConditionOperators[operator]; return ConditionOperator - ? new ConditionOperator().getResult({ value, comparedValue, instance, component, conditionComponent, conditionComponentPath }) + ? new ConditionOperator().getResult({ value, comparedValue, instance, component, conditionComponent, conditionComponentPath, data}) : true; }), (res) => (res !== null)); diff --git a/src/utils/formUtil.ts b/src/utils/formUtil.ts index 5591225f..17e4a46c 100644 --- a/src/utils/formUtil.ts +++ b/src/utils/formUtil.ts @@ -1229,8 +1229,8 @@ function isValueEmpty(component: Component, value: any) { return value == null || value === '' || (isArray(value) && value.length === 0) || compValueIsEmptyArray; } -export function isComponentDataEmpty(component: Component, data: any, path: string): boolean { - const value = get(data, path); +export function isComponentDataEmpty(component: Component, data: any, path: string, valueCond?:any): boolean { + const value = isNil(valueCond) ? get(data, path): valueCond; if (isCheckboxComponent(component)) { return isValueEmpty(component, value) || value === false; } else if (isDataGridComponent(component) || isEditGridComponent(component) || isDataTableComponent(component) || hasChildComponents(component)) { diff --git a/src/utils/operators/IsEmptyValue.js b/src/utils/operators/IsEmptyValue.js index 5f55172e..63fad224 100644 --- a/src/utils/operators/IsEmptyValue.js +++ b/src/utils/operators/IsEmptyValue.js @@ -1,5 +1,5 @@ +import { isComponentDataEmpty } from 'utils/formUtil'; import ConditionOperator from './ConditionOperator'; -import { isEmpty } from 'lodash'; export default class IsEmptyValue extends ConditionOperator { static get operatorKey() { @@ -14,15 +14,8 @@ export default class IsEmptyValue extends ConditionOperator { return false; } - execute({ value, instance, conditionComponentPath }) { - const isEmptyValue = isEmpty(value); - - if (instance && instance.root) { - const conditionTriggerComponent = instance.root.getComponent(conditionComponentPath); - return conditionTriggerComponent ? conditionTriggerComponent.isEmpty() : isEmptyValue; - } - - return isEmptyValue; + execute({ value, conditionComponentPath, data, conditionComponent}) { + return isComponentDataEmpty(conditionComponent, data, conditionComponentPath, value); } getResult(options) {