From 99d984275e27283aa460bf84670b89c1b421320e Mon Sep 17 00:00:00 2001 From: SilviaAmAm <silvia@maykinmedia.nl> Date: Thu, 18 Jan 2024 14:48:27 +0100 Subject: [PATCH 1/3] :bug: [open-formulieren/open-forms#3755] Validate min/max datetime If the datetime is entered with the keyboard instead of the widget, no validation was being performed. This adds the validation in the same way as for the date component (see https://github.com/open-formulieren/open-forms/issues/3443) --- src/formio/components/DateTimeField.js | 13 ++++++ src/formio/validators/minMaxDateValidator.js | 24 ++--------- .../validators/minMaxDatetimeValidator.js | 42 +++++++++++++++++++ src/formio/validators/utils.js | 39 +++++++++++++++++ 4 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 src/formio/validators/minMaxDatetimeValidator.js create mode 100644 src/formio/validators/utils.js diff --git a/src/formio/components/DateTimeField.js b/src/formio/components/DateTimeField.js index 9b47fc6ba..a93448a90 100644 --- a/src/formio/components/DateTimeField.js +++ b/src/formio/components/DateTimeField.js @@ -1,8 +1,21 @@ import {Formio} from 'react-formio'; +import MinMaxDatetimeValidator from 'formio/validators/minMaxDatetimeValidator'; + const DateTimeFormio = Formio.Components.components.datetime; class DateTimeField extends DateTimeFormio { + constructor(component, options, data) { + super(component, options, data); + + if (component.datePicker.minDate || component.datePicker.maxDate) { + component.validate.datetimeMinMax = true; + } + + this.validators.push('datetimeMinMax'); + this.validator.validators['datetimeMinMax'] = MinMaxDatetimeValidator; + } + get inputInfo() { const info = super.inputInfo; // apply NLDS CSS classes diff --git a/src/formio/validators/minMaxDateValidator.js b/src/formio/validators/minMaxDateValidator.js index 24af66fb9..d73f13e73 100644 --- a/src/formio/validators/minMaxDateValidator.js +++ b/src/formio/validators/minMaxDateValidator.js @@ -1,25 +1,6 @@ -import {parseISO} from 'date-fns'; import set from 'lodash/set'; -const validateDateBoundaries = (minBoundary, maxBoundary, value) => { - const minDate = minBoundary ? new Date(minBoundary) : null; - const maxDate = maxBoundary ? new Date(maxBoundary) : null; - - if (!minDate && !maxDate) { - return {isValid: true}; - } - - const parsedValue = parseISO(value, 'yyyy-MM-dd', new Date()); - - if (minDate && maxDate) { - const isValid = parsedValue >= minDate && parsedValue <= maxDate; - let errorKeys = isValid ? [] : parsedValue < minDate ? ['minDate'] : ['maxDate']; - return {isValid, errorKeys}; - } - - if (minDate) return {isValid: parsedValue >= minDate, errorKeys: ['minDate']}; - if (maxDate) return {isValid: parsedValue <= maxDate, errorKeys: ['maxDate']}; -}; +import {validateBoundaries} from './utils'; const MinMaxDateValidator = { key: 'validate.dateMinMax', @@ -38,7 +19,8 @@ const MinMaxDateValidator = { check(component, setting, value) { if (!value) return true; - const {isValid, errorKeys} = validateDateBoundaries( + const {isValid, errorKeys} = validateBoundaries( + component.type, component.component.datePicker.minDate, component.component.datePicker.maxDate, value diff --git a/src/formio/validators/minMaxDatetimeValidator.js b/src/formio/validators/minMaxDatetimeValidator.js new file mode 100644 index 000000000..64e37b816 --- /dev/null +++ b/src/formio/validators/minMaxDatetimeValidator.js @@ -0,0 +1,42 @@ +import set from 'lodash/set'; + +import {validateBoundaries} from './utils'; + +const MinMaxDatetimeValidator = { + key: 'validate.datetimeMinMax', + message(component) { + // In the form builder, this property is called 'minDate'/'maxDate' also in the datetime component + const minDatetime = new Date(component.component.minDate); + const maxDatetime = new Date(component.component.maxDate); + + const errorKeys = + component?.openForms?.validationErrorContext?.minMaxDatetimeValidatorErrorKeys; + const errorMessage = errorKeys ? errorKeys[0] : 'invalidDatetime'; + + return component.t(errorMessage, { + minDatetime: minDatetime, + maxDatetime: maxDatetime, + }); + }, + check(component, setting, value) { + if (!value) return true; + + const {isValid, errorKeys} = validateBoundaries( + component.type, + component.component.datePicker.minDate, + component.component.datePicker.maxDate, + value + ); + + if (!isValid) { + set( + component, + 'openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys', + errorKeys + ); + } + return isValid; + }, +}; + +export default MinMaxDatetimeValidator; diff --git a/src/formio/validators/utils.js b/src/formio/validators/utils.js new file mode 100644 index 000000000..48a289762 --- /dev/null +++ b/src/formio/validators/utils.js @@ -0,0 +1,39 @@ +import {parseISO} from 'date-fns'; + +export const validateBoundaries = (componentType, minBoundary, maxBoundary, value) => { + const parsedMinBoundary = minBoundary ? new Date(minBoundary) : null; + const parsedMaxBoundary = maxBoundary ? new Date(maxBoundary) : null; + + if (!parsedMinBoundary && !parsedMaxBoundary) { + return {isValid: true}; + } + + const parsedValue = parseISO(value, 'yyyy-MM-dd', new Date()); + + let errorKeyMinValue, errorKeyMaxValue; + if (componentType === 'date') { + errorKeyMinValue = 'minDate'; + errorKeyMaxValue = 'maxDate'; + } else if (componentType === 'datetime') { + errorKeyMinValue = 'minDatetime'; + errorKeyMaxValue = 'maxDatetime'; + } + + if (parsedMinBoundary && parsedMaxBoundary) { + const isValid = parsedValue >= parsedMinBoundary && parsedValue <= parsedMaxBoundary; + let errorKeys = []; + if (!isValid) { + if (parsedValue < parsedMinBoundary) { + errorKeys.push(errorKeyMinValue); + } else { + errorKeys.push(errorKeyMaxValue); + } + } + return {isValid, errorKeys}; + } + + if (parsedMinBoundary) + return {isValid: parsedValue >= parsedMinBoundary, errorKeys: [errorKeyMinValue]}; + if (parsedMaxBoundary) + return {isValid: parsedValue <= parsedMaxBoundary, errorKeys: [errorKeyMaxValue]}; +}; From 25589d3dc8700f349377293a5fcda523cdc38035 Mon Sep 17 00:00:00 2001 From: SilviaAmAm <silvia@maykinmedia.nl> Date: Thu, 18 Jan 2024 14:49:01 +0100 Subject: [PATCH 2/3] :white_check_mark: [open-formulieren/open-forms#3755] Test min/max datetime validation --- .../formio/components/datetime.spec.js | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/jstests/formio/components/datetime.spec.js diff --git a/src/jstests/formio/components/datetime.spec.js b/src/jstests/formio/components/datetime.spec.js new file mode 100644 index 000000000..75261974b --- /dev/null +++ b/src/jstests/formio/components/datetime.spec.js @@ -0,0 +1,227 @@ +import set from 'lodash/set'; +import {Formio} from 'react-formio'; + +import MinMaxDatetimeValidator from 'formio/validators/minMaxDatetimeValidator'; + +const FormioComponent = Formio.Components.components.component; + +describe('Datetime Component', () => { + test('Datetime validator: no min/max datetime', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: null, + maxDate: null, + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const componentInstance = new FormioComponent(component, {}, {}); + + const isValid = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2020-01-01T10:00:00+01:00' + ); + + expect(isValid).toBeTruthy(); + + done(); + }); + + test('Datetime validator: check min datetime', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: '2023-01-01T10:00:00+01:00', + maxDate: null, + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const componentInstance = new FormioComponent(component, {}, {}); + + const isValid1 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2020-01-01T10:00:00+01:00' + ); + + expect(isValid1).toBeFalsy(); + expect( + componentInstance.openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys + ).toContain('minDatetime'); + + const isValid2 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2024-01-01T10:00:00+01:00' + ); + + expect(isValid2).toBeTruthy(); + + done(); + }); + + test('Datetime validator: check max datetime', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: null, + maxDate: '2023-01-01T10:00:00+01:00', + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const componentInstance = new FormioComponent(component, {}, {}); + + const isValid1 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2024-01-01T10:00:00+01:00' + ); + + expect(isValid1).toBeFalsy(); + expect( + componentInstance.openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys + ).toContain('maxDatetime'); + + const isValid2 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2020-01-01T10:00:00+01:00' + ); + + expect(isValid2).toBeTruthy(); + + done(); + }); + + test('Datetime validator: check max datetime including the current one', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: null, + maxDate: '2023-09-08T10:00:00+01:00', + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const componentInstance = new FormioComponent(component, {}, {}); + + const isValid1 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2023-09-08T10:00:00+01:00' + ); + + expect(isValid1).toBeTruthy(); + + done(); + }); + + test('Datetime validator: error message', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: '2023-09-08T10:00:00+01:00', + maxDate: null, + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const mockTranslation = jest.fn((message, values) => message); + + const componentInstance = new FormioComponent(component, {}, {}); + componentInstance.t = mockTranslation; + + MinMaxDatetimeValidator.message(componentInstance); + + expect(mockTranslation.mock.calls[0][0]).toEqual('invalidDatetime'); + + set(componentInstance, 'openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys', [ + 'minDatetime', + ]); + + MinMaxDatetimeValidator.message(componentInstance); + + expect(mockTranslation.mock.calls[1][0]).toEqual('minDatetime'); + + set(componentInstance, 'openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys', [ + 'maxDatetime', + ]); + + MinMaxDatetimeValidator.message(componentInstance); + + expect(mockTranslation.mock.calls[2][0]).toEqual('maxDatetime'); + + done(); + }); + + test('Datetime validator: check max datetime AND min datetime', done => { + const component = { + label: 'datetime', + key: 'datetime', + type: 'datetime', + datePicker: { + minDate: '2023-09-01T10:00:00+01:00', + maxDate: '2023-09-08T10:00:00+01:00', + }, + customOptions: { + allowInvalidPreload: true, + }, + validate: {datetimeMinMax: true}, + }; + + const componentInstance = new FormioComponent(component, {}, {}); + + const isValid1 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2024-01-01T10:00:00+01:00' + ); + + expect(isValid1).toBeFalsy(); + expect( + componentInstance.openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys + ).toContain('maxDatetime'); + + const isValid2 = MinMaxDatetimeValidator.check( + componentInstance, + {}, + '2020-01-01T10:00:00+01:00' + ); + + expect(isValid2).toBeFalsy(); + expect( + componentInstance.openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys + ).toContain('minDatetime'); + + done(); + }); +}); From eb4def74651bc2df932d74a639b3f3df2adb4310 Mon Sep 17 00:00:00 2001 From: SilviaAmAm <silvia@maykinmedia.nl> Date: Mon, 22 Jan 2024 16:09:23 +0100 Subject: [PATCH 3/3] :ok_hand: [open-formulieren/open-forms#3755] PR Feedback --- src/formio/validators/utils.js | 2 +- .../formio/components/datetime.spec.js | 24 +++++-------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/formio/validators/utils.js b/src/formio/validators/utils.js index 48a289762..d4eecbf80 100644 --- a/src/formio/validators/utils.js +++ b/src/formio/validators/utils.js @@ -8,7 +8,7 @@ export const validateBoundaries = (componentType, minBoundary, maxBoundary, valu return {isValid: true}; } - const parsedValue = parseISO(value, 'yyyy-MM-dd', new Date()); + const parsedValue = parseISO(value); let errorKeyMinValue, errorKeyMaxValue; if (componentType === 'date') { diff --git a/src/jstests/formio/components/datetime.spec.js b/src/jstests/formio/components/datetime.spec.js index 75261974b..43a31b01c 100644 --- a/src/jstests/formio/components/datetime.spec.js +++ b/src/jstests/formio/components/datetime.spec.js @@ -6,7 +6,7 @@ import MinMaxDatetimeValidator from 'formio/validators/minMaxDatetimeValidator'; const FormioComponent = Formio.Components.components.component; describe('Datetime Component', () => { - test('Datetime validator: no min/max datetime', done => { + test('Datetime validator: no min/max datetime', () => { const component = { label: 'datetime', key: 'datetime', @@ -30,11 +30,9 @@ describe('Datetime Component', () => { ); expect(isValid).toBeTruthy(); - - done(); }); - test('Datetime validator: check min datetime', done => { + test('Datetime validator: check min datetime', () => { const component = { label: 'datetime', key: 'datetime', @@ -69,11 +67,9 @@ describe('Datetime Component', () => { ); expect(isValid2).toBeTruthy(); - - done(); }); - test('Datetime validator: check max datetime', done => { + test('Datetime validator: check max datetime', () => { const component = { label: 'datetime', key: 'datetime', @@ -108,11 +104,9 @@ describe('Datetime Component', () => { ); expect(isValid2).toBeTruthy(); - - done(); }); - test('Datetime validator: check max datetime including the current one', done => { + test('Datetime validator: check max datetime including the current one', () => { const component = { label: 'datetime', key: 'datetime', @@ -136,11 +130,9 @@ describe('Datetime Component', () => { ); expect(isValid1).toBeTruthy(); - - done(); }); - test('Datetime validator: error message', done => { + test('Datetime validator: error message', () => { const component = { label: 'datetime', key: 'datetime', @@ -179,11 +171,9 @@ describe('Datetime Component', () => { MinMaxDatetimeValidator.message(componentInstance); expect(mockTranslation.mock.calls[2][0]).toEqual('maxDatetime'); - - done(); }); - test('Datetime validator: check max datetime AND min datetime', done => { + test('Datetime validator: check max datetime AND min datetime', () => { const component = { label: 'datetime', key: 'datetime', @@ -221,7 +211,5 @@ describe('Datetime Component', () => { expect( componentInstance.openForms.validationErrorContext.minMaxDatetimeValidatorErrorKeys ).toContain('minDatetime'); - - done(); }); });