From bc413bca5eb3a9a3bdcdc5b63554503a71e6e6f2 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 10 Jun 2026 14:40:53 +0100 Subject: [PATCH 1/2] Add validation to MonthYearField to support absolute date range options --- .../engine/components/MonthYearField.test.ts | 65 ++++++++++++++++++- .../engine/components/MonthYearField.ts | 30 ++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/server/plugins/engine/components/MonthYearField.test.ts b/src/server/plugins/engine/components/MonthYearField.test.ts index 7781fcdc8..582d1f4a4 100644 --- a/src/server/plugins/engine/components/MonthYearField.test.ts +++ b/src/server/plugins/engine/components/MonthYearField.test.ts @@ -1,5 +1,5 @@ import { ComponentType, type MonthYearFieldComponent } from '@defra/forms-model' -import { startOfDay } from 'date-fns' +import { addMonths, format, startOfDay, startOfMonth } from 'date-fns' import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' import { @@ -401,6 +401,13 @@ describe('MonthYearField', () => { describe('Validation', () => { const date = new Date('2001-01-01') + const today = startOfDay(new Date()) + const thisMonth = startOfMonth(today) + + const OneMonthInPast = addMonths(thisMonth, -1) + const TwoMonthsInPast = addMonths(thisMonth, -2) + const OneMonthInFuture = addMonths(thisMonth, 1) + const TwoMonthsInFuture = addMonths(thisMonth, 2) describe.each([ { @@ -517,6 +524,62 @@ describe('MonthYearField', () => { } ] }, + { + description: 'Earliest month/year option', + component: { + title: 'Example month/year field', + name: 'myComponent', + type: ComponentType.MonthYearField, + options: { + earliestMonthYear: format(OneMonthInPast, 'yyyy-MM') + } + } satisfies MonthYearFieldComponent, + assertions: [ + { + input: getFormData(TwoMonthsInPast), + output: { + value: getFormData(TwoMonthsInPast), + errors: [ + expect.objectContaining({ + text: `Example month/year field must be the same as or after ${format(OneMonthInPast, 'd MMMM yyyy')}` + }) + ] + } + }, + { + input: getFormData(today), + output: { value: getFormData(today) } + } + ] + }, + { + description: 'Latest month/year option', + component: { + title: 'Example month/year field', + name: 'myComponent', + type: ComponentType.MonthYearField, + options: { + latestMonthYear: format(OneMonthInFuture, 'yyyy-MM') + } + } satisfies MonthYearFieldComponent, + assertions: [ + { + input: getFormData(TwoMonthsInFuture), + output: { + value: getFormData(TwoMonthsInFuture), + errors: [ + expect.objectContaining({ + text: `Example month/year field must be the same as or before ${format(OneMonthInFuture, 'd MMMM yyyy')}` + }) + ] + } + }, + { + input: getFormData(today), + output: { value: getFormData(today) } + } + ] + }, { description: 'Optional fields', component: { diff --git a/src/server/plugins/engine/components/MonthYearField.ts b/src/server/plugins/engine/components/MonthYearField.ts index 7fb5452e2..fb274ab1a 100644 --- a/src/server/plugins/engine/components/MonthYearField.ts +++ b/src/server/plugins/engine/components/MonthYearField.ts @@ -1,5 +1,5 @@ import { ComponentType, type MonthYearFieldComponent } from '@defra/forms-model' -import { format, isValid } from 'date-fns' +import { format, isValid, parse } from 'date-fns' import { type Context, type CustomValidator, @@ -259,6 +259,34 @@ export function getValidatorMonthYear(component: MonthYearField) { : payload } + const date = parse( + `${values.year}-${values.month}-01`, + 'yyyy-MM-dd', + new Date() + ) + + if (!isValid(date)) { + return helpers.error('date.format', context) + } + + // Minimum date from today + const earliestDate = options.earliestMonthYear + ? new Date(`${options.earliestMonthYear}-01`) + : undefined + + if (earliestDate && date < earliestDate) { + return helpers.error('date.min', { ...context, limit: earliestDate }) + } + + // Maximum date from today + const latestDate = options.latestMonthYear + ? new Date(`${options.latestMonthYear}-01`) + : undefined + + if (latestDate && date > latestDate) { + return helpers.error('date.max', { ...context, limit: latestDate }) + } + return payload } From bf19c778f0a5abedada4b4a89398adc778202a79 Mon Sep 17 00:00:00 2001 From: David Stone Date: Fri, 12 Jun 2026 17:43:51 +0100 Subject: [PATCH 2/2] Bump @defra/forms-model@3.0.678 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61522b63d..f6aa6c5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.676", + "@defra/forms-model": "^3.0.678", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.22-alpha", "@elastic/ecs-pino-format": "^1.5.0", @@ -3516,9 +3516,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.676", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.676.tgz", - "integrity": "sha512-YH0nYkFVTi6PE6xWKEDYK95aT1dJCSIpxkDx5QfkNSkEQqET6nf4o4QYsYISehJl5XSPwAZVHn36RyXCyYhqFg==", + "version": "3.0.678", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.678.tgz", + "integrity": "sha512-J7FkKLZIMpJk6Y6VihFx9jmODWnoSYONFx+khSdZIAUqWh2dg6S4ikfDszSioW0sq1owndBYolvjuVeaLhg1dw==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", diff --git a/package.json b/package.json index 8a69e4660..6ad1bb1af 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ }, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.676", + "@defra/forms-model": "^3.0.678", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.22-alpha", "@elastic/ecs-pino-format": "^1.5.0",