From 9ebb7eebce6235533e1c11219f8f61482d909950 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 16:51:34 +0100 Subject: [PATCH 1/3] feat: update GeospatialFieldComponent to support multiple countries --- model/src/components/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/components/types.ts b/model/src/components/types.ts index 7e149d798..081618246 100644 --- a/model/src/components/types.ts +++ b/model/src/components/types.ts @@ -278,7 +278,7 @@ export interface GeospatialFieldComponent extends FormFieldBase { type: ComponentType.GeospatialField options: FormFieldBase['options'] & { condition?: string - country?: GeospatialFieldOptionsCountry + countries?: GeospatialFieldOptionsCountry[] } } From 5d13a156713369e2dfcc0267e1769652c6f86d86 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 13:44:15 +0100 Subject: [PATCH 2/3] Add country additional option to the GeospatialField component --- .../server/src/common/constants/editor.js | 3 +- .../editor-v2/advanced-settings-config.js | 41 ++++++++++++++++++- .../advanced-settings-fields.test.js | 6 +++ .../editor-v2/advanced-settings-helpers.js | 6 +++ .../advanced-settings-helpers.test.js | 5 +++ .../editor-v2/advanced-settings-schemas.js | 3 +- .../src/models/forms/editor-v2/page-fields.js | 6 +++ .../question-details-advanced-settings.js | 13 +++++- ...question-details-advanced-settings.test.js | 14 +++++++ .../question-details-advanced-settings.njk | 4 ++ model/src/form/form-editor/index.ts | 12 +++++- model/src/form/form-editor/types.ts | 6 +++ 12 files changed, 112 insertions(+), 7 deletions(-) diff --git a/designer/server/src/common/constants/editor.js b/designer/server/src/common/constants/editor.js index ad91c8853..f83627f83 100644 --- a/designer/server/src/common/constants/editor.js +++ b/designer/server/src/common/constants/editor.js @@ -133,7 +133,8 @@ export const QuestionAdvancedSettings = InstructionText: 'instructionText', MinChecks: 'minChecks', MaxChecks: 'maxChecks', - ExactChecks: 'exactChecks' + ExactChecks: 'exactChecks', + Country: 'country' } /** diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-config.js b/designer/server/src/models/forms/editor-v2/advanced-settings-config.js index 5d69d5b1d..2aedfca77 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-config.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-config.js @@ -80,7 +80,8 @@ export const advancedSettingsPerComponentType = QuestionAdvancedSettings.InstructionText, QuestionAdvancedSettings.Classes ], - HiddenField: [] + HiddenField: [], + GeospatialField: [QuestionAdvancedSettings.Country] }) /** @@ -327,6 +328,44 @@ export const allAdvancedSettingsFields = text: 'The maximum number of checkboxes a user can select' }, classes: GOVUK_INPUT_WIDTH_3 + }, + [QuestionAdvancedSettings.Country]: { + name: 'country', + id: 'country', + classes: 'govuk-radios--small', + fieldset: { + legend: { + text: 'Which country must the features be in?', + isPageHeading: false, + classes: 'govuk-fieldset__legend--m' + } + }, + formGroup: { classes: 'app-settings-radios' }, + items: [ + { + value: 'england', + text: 'England' + }, + { + value: 'wales', + text: 'Wales' + }, + { + value: 'northern-ireland', + text: 'Northern Ireland' + }, + { + value: 'scotland', + text: 'Scotland' + }, + { + divider: 'or' + }, + { + value: 'any', + text: 'Any' + } + ] } }) diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js b/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js index ba0aaad6f..736aa6cfc 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js @@ -52,6 +52,12 @@ describe('editor-v2 - advanced settings fields model', () => { ComponentType.TextField ) }) + + test('should return RadiosField for country', () => { + expect(getFieldComponentType({ name: 'country' })).toBe( + ComponentType.RadiosField + ) + }) }) describe('mapQuestionDetails', () => { diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js index f3b1396fb..21567ed83 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js @@ -92,6 +92,12 @@ export function getAdditionalOptions(payload) { key: 'description', getValue: () => payload.paymentDescription, shouldInclude: () => payload.paymentDescription !== undefined + }, + { + key: 'country', + getValue: () => payload.country, + shouldInclude: () => + payload.country !== undefined && payload.country !== 'any' } ] diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js index 3c0948b76..5b3d0378c 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js @@ -34,6 +34,11 @@ describe('advanced-settings-helpers', () => { expect(result).toEqual({ suffix: ' per item' }) }) + it('should include country when provided', () => { + const result = getAdditionalOptions({ country: 'scotland' }) + expect(result).toEqual({ country: 'scotland' }) + }) + it('should map maxFuture to maxDaysInFuture', () => { const result = getAdditionalOptions({ maxFuture: '30' }) expect(result).toEqual({ maxDaysInFuture: '30' }) diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js b/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js index 59d09baa8..3cd3ee049 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js @@ -150,5 +150,6 @@ export const allSpecificSchemas = Joi.object().keys({ '*': 'Enter instructions to help users answer this question' }), otherwise: Joi.string().optional().allow('') - }) + }), + country: questionDetailsFullSchema.countrySchema.valid('any') }) diff --git a/designer/server/src/models/forms/editor-v2/page-fields.js b/designer/server/src/models/forms/editor-v2/page-fields.js index d104bd9aa..b4ad69148 100644 --- a/designer/server/src/models/forms/editor-v2/page-fields.js +++ b/designer/server/src/models/forms/editor-v2/page-fields.js @@ -51,6 +51,8 @@ const checkBoxFieldQuestions = [ const fileUploadFields = [QuestionBaseSettings.FileTypes] +const radiosFieldQuestions = [QuestionAdvancedSettings.Country] + /** * @param {GovukField} field */ @@ -76,6 +78,10 @@ export function getFieldComponentType(field) { return ComponentType.FileUploadField } + if (radiosFieldQuestions.includes(fieldName)) { + return ComponentType.RadiosField + } + throw new Error( `Invalid or not implemented field name setting (${field.name})` ) diff --git a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js index 00f69c4da..47ee2f0a0 100644 --- a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js +++ b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js @@ -144,6 +144,14 @@ export function mapToQuestionOptions(question) { .options.instructionText } : {} + const geospatialExtras = + question.type === ComponentType.GeospatialField + ? { + country: + /** @type {GeospatialFieldComponent} */ (question).options + .country ?? 'any' + } + : {} return { classes: /** @type {FormComponentsDef} */ (question).options.classes, @@ -152,7 +160,8 @@ export function mapToQuestionOptions(question) { ...minMaxExtras, ...multilineExtras, ...regexExtras, - ...locationExtras + ...locationExtras, + ...geospatialExtras } } @@ -244,6 +253,6 @@ export function enhancedFields(options, question, validation) { */ /** - * @import { ComponentDef, DatePartsFieldComponent, EastingNorthingFieldComponent, FileUploadFieldComponent, CheckboxesFieldComponent, FormComponentsDef, FormEditor, GovukField, LatLongFieldComponent, MonthYearFieldComponent, MultilineTextFieldComponent, NationalGridFieldNumberFieldComponent, NumberFieldComponent, OsGridRefFieldComponent, TextFieldComponent } from '@defra/forms-model' + * @import { ComponentDef, DatePartsFieldComponent, EastingNorthingFieldComponent, FileUploadFieldComponent, CheckboxesFieldComponent, FormComponentsDef, FormEditor, GovukField, LatLongFieldComponent, MonthYearFieldComponent, MultilineTextFieldComponent, NationalGridFieldNumberFieldComponent, NumberFieldComponent, OsGridRefFieldComponent, TextFieldComponent, GeospatialFieldComponent } from '@defra/forms-model' * @import { ValidationFailure } from '~/src/common/helpers/types.js' */ diff --git a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js index d8752ea28..42c634c16 100644 --- a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js +++ b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js @@ -342,6 +342,20 @@ describe('editor-v2 - question details advanced settings model', () => { classes: 'month-class' }) }) + + test('should map a geospatial field with country', () => { + const res = mapToQuestionOptions({ + type: ComponentType.GeospatialField, + name: 'features', + title: 'features title', + options: { + country: 'wales' + } + }) + expect(res).toEqual({ + country: 'wales' + }) + }) }) describe('advancedSettingsFields', () => { diff --git a/designer/server/src/views/forms/editor-v2/partials/question-details-advanced-settings.njk b/designer/server/src/views/forms/editor-v2/partials/question-details-advanced-settings.njk index 84ba412d7..86a241bb6 100644 --- a/designer/server/src/views/forms/editor-v2/partials/question-details-advanced-settings.njk +++ b/designer/server/src/views/forms/editor-v2/partials/question-details-advanced-settings.njk @@ -1,6 +1,8 @@ {% from "govuk/components/input/macro.njk" import govukInput %} {% from "govuk/components/textarea/macro.njk" import govukTextarea %} {% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %} +{% from "govuk/components/radios/macro.njk" import govukRadios %} + {% macro renderFieldByType(field) %} {% set fieldType = getFieldType(field) %} {% if fieldType == 'TextField' %} @@ -9,6 +11,8 @@ {{ govukTextarea(field) }} {% elif fieldType == 'CheckboxesField' %} {{ govukCheckboxes(field) }} + {% elif fieldType == 'RadiosField' %} + {{ govukRadios(field) }} {% endif %} {% endmacro %} diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index d840f2836..141fa3950 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -1,7 +1,10 @@ import Joi, { type ArraySchema, type GetRuleOptions } from 'joi' import { rtrimOnly } from '~/src/common/rtrim-only.js' -import { ComponentType } from '~/src/components/enums.js' +import { + ComponentType, + GeospatialFieldOptionsCountryEnum +} from '~/src/components/enums.js' import { MAX_NUMBER_OF_REPEAT_ITEMS, MIN_NUMBER_OF_REPEAT_ITEMS @@ -447,6 +450,10 @@ export const maxChecksSchema = Joi.number() .min(2) .description('Maximum number of items allowed to be selected.') +export const countrySchema = Joi.string() + .valid(...Object.values(GeospatialFieldOptionsCountryEnum)) + .description('The country to be included in a geospatial field') + type GenericRuleOptions = Omit & { args: Record } @@ -638,7 +645,8 @@ export const questionDetailsFullSchema = { usePostcodeLookupSchema, minChecksSchema, maxChecksSchema, - exactChecksSchema + exactChecksSchema, + countrySchema } export const formEditorInputPageKeys = { diff --git a/model/src/form/form-editor/types.ts b/model/src/form/form-editor/types.ts index 33859fbf0..9a27f1862 100644 --- a/model/src/form/form-editor/types.ts +++ b/model/src/form/form-editor/types.ts @@ -349,6 +349,11 @@ export interface FormEditor { * Title that user supplies a section */ sectionTitle: string + + /** + * The country restriction for geospatial questions + */ + country?: string } export type FormEditorInputPage = Pick< @@ -428,6 +433,7 @@ export type FormEditorInputQuestion = Pick< | 'paymentDescription' | 'paymentTestApiKey' | 'paymentLiveApiKey' + | 'country' > export type FormEditorInputPageSettings = Pick< From 7f8f914eb6e5288c450f67539d4f213a99386a6b Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 30 Apr 2026 17:32:31 +0100 Subject: [PATCH 3/3] feat: update GeospatialField to support multiple countries --- designer/server/src/common/constants/editor.js | 2 +- .../forms/editor-v2/advanced-settings-config.js | 8 ++++---- .../forms/editor-v2/advanced-settings-fields.test.js | 4 ++-- .../forms/editor-v2/advanced-settings-helpers.js | 8 +++++--- .../editor-v2/advanced-settings-helpers.test.js | 6 +++--- .../forms/editor-v2/advanced-settings-schemas.js | 2 +- .../server/src/models/forms/editor-v2/page-fields.js | 2 +- .../editor-v2/question-details-advanced-settings.js | 5 ++--- .../question-details-advanced-settings.test.js | 4 ++-- model/src/form/form-editor/index.ts | 12 +++++++++--- model/src/form/form-editor/types.ts | 9 ++++++--- 11 files changed, 36 insertions(+), 26 deletions(-) diff --git a/designer/server/src/common/constants/editor.js b/designer/server/src/common/constants/editor.js index f83627f83..8ef9241cd 100644 --- a/designer/server/src/common/constants/editor.js +++ b/designer/server/src/common/constants/editor.js @@ -134,7 +134,7 @@ export const QuestionAdvancedSettings = MinChecks: 'minChecks', MaxChecks: 'maxChecks', ExactChecks: 'exactChecks', - Country: 'country' + Countries: 'countries' } /** diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-config.js b/designer/server/src/models/forms/editor-v2/advanced-settings-config.js index 2aedfca77..11fc25ccb 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-config.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-config.js @@ -81,7 +81,7 @@ export const advancedSettingsPerComponentType = QuestionAdvancedSettings.Classes ], HiddenField: [], - GeospatialField: [QuestionAdvancedSettings.Country] + GeospatialField: [QuestionAdvancedSettings.Countries] }) /** @@ -329,9 +329,9 @@ export const allAdvancedSettingsFields = }, classes: GOVUK_INPUT_WIDTH_3 }, - [QuestionAdvancedSettings.Country]: { - name: 'country', - id: 'country', + [QuestionAdvancedSettings.Countries]: { + name: 'countries', + id: 'countries', classes: 'govuk-radios--small', fieldset: { legend: { diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js b/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js index 736aa6cfc..0d266d07e 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-fields.test.js @@ -53,8 +53,8 @@ describe('editor-v2 - advanced settings fields model', () => { ) }) - test('should return RadiosField for country', () => { - expect(getFieldComponentType({ name: 'country' })).toBe( + test('should return RadiosField for countries', () => { + expect(getFieldComponentType({ name: 'countries' })).toBe( ComponentType.RadiosField ) }) diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js index 21567ed83..859ecf846 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.js @@ -94,10 +94,12 @@ export function getAdditionalOptions(payload) { shouldInclude: () => payload.paymentDescription !== undefined }, { - key: 'country', - getValue: () => payload.country, + key: 'countries', + getValue: () => payload.countries, shouldInclude: () => - payload.country !== undefined && payload.country !== 'any' + Array.isArray(payload.countries) && + payload.countries.length === 1 && + payload.countries[0] !== 'any' } ] diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js index 5b3d0378c..b844e86d4 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-helpers.test.js @@ -34,9 +34,9 @@ describe('advanced-settings-helpers', () => { expect(result).toEqual({ suffix: ' per item' }) }) - it('should include country when provided', () => { - const result = getAdditionalOptions({ country: 'scotland' }) - expect(result).toEqual({ country: 'scotland' }) + it('should include countries when provided', () => { + const result = getAdditionalOptions({ countries: ['scotland'] }) + expect(result).toEqual({ countries: ['scotland'] }) }) it('should map maxFuture to maxDaysInFuture', () => { diff --git a/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js b/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js index 3cd3ee049..7d53e3833 100644 --- a/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js +++ b/designer/server/src/models/forms/editor-v2/advanced-settings-schemas.js @@ -151,5 +151,5 @@ export const allSpecificSchemas = Joi.object().keys({ }), otherwise: Joi.string().optional().allow('') }), - country: questionDetailsFullSchema.countrySchema.valid('any') + countries: questionDetailsFullSchema.countriesSchema }) diff --git a/designer/server/src/models/forms/editor-v2/page-fields.js b/designer/server/src/models/forms/editor-v2/page-fields.js index b4ad69148..7ce5b1b28 100644 --- a/designer/server/src/models/forms/editor-v2/page-fields.js +++ b/designer/server/src/models/forms/editor-v2/page-fields.js @@ -51,7 +51,7 @@ const checkBoxFieldQuestions = [ const fileUploadFields = [QuestionBaseSettings.FileTypes] -const radiosFieldQuestions = [QuestionAdvancedSettings.Country] +const radiosFieldQuestions = [QuestionAdvancedSettings.Countries] /** * @param {GovukField} field diff --git a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js index 47ee2f0a0..6cc2b043f 100644 --- a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js +++ b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.js @@ -147,9 +147,8 @@ export function mapToQuestionOptions(question) { const geospatialExtras = question.type === ComponentType.GeospatialField ? { - country: - /** @type {GeospatialFieldComponent} */ (question).options - .country ?? 'any' + countries: /** @type {GeospatialFieldComponent} */ (question).options + .countries ?? ['any'] } : {} diff --git a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js index 42c634c16..d7a8a54a3 100644 --- a/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js +++ b/designer/server/src/models/forms/editor-v2/question-details-advanced-settings.test.js @@ -349,11 +349,11 @@ describe('editor-v2 - question details advanced settings model', () => { name: 'features', title: 'features title', options: { - country: 'wales' + countries: ['wales'] } }) expect(res).toEqual({ - country: 'wales' + countries: ['wales'] }) }) }) diff --git a/model/src/form/form-editor/index.ts b/model/src/form/form-editor/index.ts index 141fa3950..08ac18b97 100644 --- a/model/src/form/form-editor/index.ts +++ b/model/src/form/form-editor/index.ts @@ -450,8 +450,14 @@ export const maxChecksSchema = Joi.number() .min(2) .description('Maximum number of items allowed to be selected.') -export const countrySchema = Joi.string() - .valid(...Object.values(GeospatialFieldOptionsCountryEnum)) +export const countriesSchema = Joi.array() + .items( + Joi.string().valid( + ...Object.values(GeospatialFieldOptionsCountryEnum), + 'any' + ) + ) + .single() .description('The country to be included in a geospatial field') type GenericRuleOptions = Omit & { @@ -646,7 +652,7 @@ export const questionDetailsFullSchema = { minChecksSchema, maxChecksSchema, exactChecksSchema, - countrySchema + countriesSchema } export const formEditorInputPageKeys = { diff --git a/model/src/form/form-editor/types.ts b/model/src/form/form-editor/types.ts index 9a27f1862..f6705b008 100644 --- a/model/src/form/form-editor/types.ts +++ b/model/src/form/form-editor/types.ts @@ -1,5 +1,8 @@ import { type ComponentType } from '~/src/components/enums.js' -import { type ComponentDef } from '~/src/components/types.js' +import { + type ComponentDef, + type GeospatialFieldOptionsCountry +} from '~/src/components/types.js' import { type DateDirections, type DateUnits } from '~/src/conditions/enums.js' import { type ConditionWrapperV2, @@ -353,7 +356,7 @@ export interface FormEditor { /** * The country restriction for geospatial questions */ - country?: string + countries?: (GeospatialFieldOptionsCountry | 'any')[] } export type FormEditorInputPage = Pick< @@ -433,7 +436,7 @@ export type FormEditorInputQuestion = Pick< | 'paymentDescription' | 'paymentTestApiKey' | 'paymentLiveApiKey' - | 'country' + | 'countries' > export type FormEditorInputPageSettings = Pick<