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..d4eecbf80 --- /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); + + 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]}; +}; diff --git a/src/jstests/formio/components/datetime.spec.js b/src/jstests/formio/components/datetime.spec.js new file mode 100644 index 000000000..43a31b01c --- /dev/null +++ b/src/jstests/formio/components/datetime.spec.js @@ -0,0 +1,215 @@ +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', () => { + 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(); + }); + + test('Datetime validator: check min datetime', () => { + 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(); + }); + + test('Datetime validator: check max datetime', () => { + 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(); + }); + + test('Datetime validator: check max datetime including the current one', () => { + 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(); + }); + + test('Datetime validator: error message', () => { + 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'); + }); + + test('Datetime validator: check max datetime AND min datetime', () => { + 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'); + }); +});