From 7e0806086bacdeb19b15b4fb73d0c040afa426f5 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 13 Oct 2025 10:06:12 +0100 Subject: [PATCH 1/9] Empty From d4dedf68750955eeb27ddc50080a67990802ed9b Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:40:59 +0100 Subject: [PATCH 2/9] Add ExternalAction types --- src/server/plugins/engine/models/FormModel.ts | 4 +++- src/server/routes/types.ts | 8 +++++++- src/server/schemas/index.ts | 12 +++++------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/server/plugins/engine/models/FormModel.ts b/src/server/plugins/engine/models/FormModel.ts index 103cc8e04..5be037d06 100644 --- a/src/server/plugins/engine/models/FormModel.ts +++ b/src/server/plugins/engine/models/FormModel.ts @@ -553,7 +553,9 @@ function validateFormPayload( // Skip validation GET requests or other actions if ( !request.payload || - (action && ![FormAction.Validate, FormAction.SaveAndExit].includes(action)) + (action && + ![FormAction.Validate, FormAction.SaveAndExit].includes(action) && + !action.startsWith(FormAction.External)) ) { return context } diff --git a/src/server/routes/types.ts b/src/server/routes/types.ts index 5639840fb..506b92f2a 100644 --- a/src/server/routes/types.ts +++ b/src/server/routes/types.ts @@ -45,10 +45,16 @@ export enum FormAction { Delete = 'delete', AddAnother = 'add-another', Send = 'send', - SaveAndExit = 'save-and-exit' + SaveAndExit = 'save-and-exit', + External = 'external' } export enum FormStatus { Draft = 'draft', Live = 'live' } + +export enum ExternalActions { + PostcodeLookup = 'postcode-lookup', + AnotherExternalAction = 'another-external-action' +} diff --git a/src/server/schemas/index.ts b/src/server/schemas/index.ts index f181e7f01..b4e22af31 100644 --- a/src/server/schemas/index.ts +++ b/src/server/schemas/index.ts @@ -8,13 +8,11 @@ export const stateSchema = Joi.string() .required() export const actionSchema = Joi.string() - .valid( - FormAction.Continue, - FormAction.Validate, - FormAction.Delete, - FormAction.AddAnother, - FormAction.Send, - FormAction.SaveAndExit + .pattern(new RegExp(`^${FormAction.External}-[a-zA-Z-:]*$`)) + .allow( + ...Object.values(FormAction).filter( + (value) => value !== FormAction.External + ) ) .default(FormAction.Validate) .optional() From d0d2993ab20c63597aafdf6eee3d0c7d37dd6d24 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:41:28 +0100 Subject: [PATCH 3/9] Add button styled as a link --- src/client/stylesheets/application.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/stylesheets/application.scss b/src/client/stylesheets/application.scss index dd004291e..de7abe1d0 100644 --- a/src/client/stylesheets/application.scss +++ b/src/client/stylesheets/application.scss @@ -17,3 +17,12 @@ display: none; visibility: hidden; } + +.govuk-button--link { + @extend %govuk-link; + @include govuk-font($size: 19); + color: $govuk-link-colour; + border: none; + cursor: pointer; + background-color: transparent; +} From 21915bcfa00c17fb82882c45ea2fd4d15b8e5b54 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:41:53 +0100 Subject: [PATCH 4/9] Remove enginePluginOptions from postcode plugin --- src/server/plugins/engine/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/plugins/engine/plugin.ts b/src/server/plugins/engine/plugin.ts index 15adb0b76..cde8ed853 100644 --- a/src/server/plugins/engine/plugin.ts +++ b/src/server/plugins/engine/plugin.ts @@ -51,8 +51,7 @@ export const plugin = { await server.register({ plugin: postcodeLookupPlugin, options: { - ordnanceSurveyApiKey, - enginePluginOptions: options + ordnanceSurveyApiKey } }) } From 6c35583d1c187446a0eec5e44b4ad392f8a47174 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:42:33 +0100 Subject: [PATCH 5/9] Remove lookup hrefs from address component --- .../engine/components/UkAddressField.ts | 28 ++----------------- .../views/components/ukaddressfield.html | 17 ++++++----- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/server/plugins/engine/components/UkAddressField.ts b/src/server/plugins/engine/components/UkAddressField.ts index fc6d57c75..34153a2ff 100644 --- a/src/server/plugins/engine/components/UkAddressField.ts +++ b/src/server/plugins/engine/components/UkAddressField.ts @@ -8,7 +8,6 @@ import { } from '~/src/server/plugins/engine/components/FormComponent.js' import { TextField } from '~/src/server/plugins/engine/components/TextField.js' import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js' -import { type FormQuery } from '~/src/server/plugins/engine/types/index.js' import { type ErrorMessageTemplateList, type FormPayload, @@ -17,7 +16,6 @@ import { type FormSubmissionError, type FormSubmissionState } from '~/src/server/plugins/engine/types.js' -import { JOURNEY_BASE_URL } from '~/src/server/plugins/postcode-lookup/models/index.js' export class UkAddressField extends FormComponent { declare options: UkAddressFieldComponent['options'] @@ -160,11 +158,7 @@ export class UkAddressField extends FormComponent { ) } - getViewModel( - payload: FormPayload, - errors?: FormSubmissionError[], - query?: FormQuery - ) { + getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) { const { collection, name, options } = this const viewModel = super.getViewModel(payload, errors) @@ -210,30 +204,12 @@ export class UkAddressField extends FormComponent { ? this.getDisplayStringFromState(payload) : undefined - let postcodeLookupBaseUrl - let postcodeLookupQuery - - if (usePostcodeLookup) { - const searchParams = new URLSearchParams([['clear', 'true']]) - - if (query) { - Object.entries(query).forEach(([key, value]) => { - searchParams.append(key, value ?? '') - }) - } - - postcodeLookupBaseUrl = JOURNEY_BASE_URL - postcodeLookupQuery = searchParams.toString() - } - return { ...viewModel, value, fieldset, components, - usePostcodeLookup, - postcodeLookupBaseUrl, - postcodeLookupQuery + usePostcodeLookup } } diff --git a/src/server/plugins/engine/views/components/ukaddressfield.html b/src/server/plugins/engine/views/components/ukaddressfield.html index a805d281f..41f473d75 100644 --- a/src/server/plugins/engine/views/components/ukaddressfield.html +++ b/src/server/plugins/engine/views/components/ukaddressfield.html @@ -32,10 +32,7 @@ }) if fieldset else addressFieldHtml }} {% if usePostcodeLookup %} - {% set postcodeLookupBaseUrl = component.model.postcodeLookupBaseUrl %} - {% set postcodeLookupQuery = component.model.postcodeLookupQuery %} - {% set value = component.model.value %} - {% set postcodeLookupHref = postcodeLookupBaseUrl + "/" + slug + page.path + "/" + component.model.name + "?" + postcodeLookupQuery %} + {% set value = component.model.value %} {% if value %} {% set insetHtml %} @@ -43,7 +40,10 @@

{{ value }}

- Use a different address +

+ +

{% endset %} {{ govukInsetText({ @@ -54,10 +54,13 @@
{{ govukButton({ text: "Find an address", - href: postcodeLookupHref, + attributes: { + name: "action", + value: "external-postcode-lookup--name:" + component.model.name + }, classes: "govuk-button--secondary govuk-!-margin-right-1" }) }} -

or enter address manually

+

or

{% endif %} {% endif %} From d2986aa5fb9665858f1441c1cbb90ffa27150a70 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:42:57 +0100 Subject: [PATCH 6/9] Add ExternalAction handlng --- src/server/plugins/engine/routes/index.ts | 57 ++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/server/plugins/engine/routes/index.ts b/src/server/plugins/engine/routes/index.ts index 8f5290770..ef48ede48 100644 --- a/src/server/plugins/engine/routes/index.ts +++ b/src/server/plugins/engine/routes/index.ts @@ -1,3 +1,4 @@ +import { ComponentType } from '@defra/forms-model' import Boom from '@hapi/boom' import { type ResponseObject, @@ -25,8 +26,13 @@ import { type FormContext, type PluginOptions } from '~/src/server/plugins/engine/types.js' +import { dispatch } from '~/src/server/plugins/postcode-lookup/routes/index.js' +import { type PostcodeLookupDispatchArgs } from '~/src/server/plugins/postcode-lookup/types.js' import { + ExternalActions, + FormAction, type FormRequest, + type FormRequestPayload, type FormResponseToolkit } from '~/src/server/routes/types.js' @@ -38,7 +44,7 @@ export async function redirectOrMakeHandler( context: FormContext ) => ResponseObject | Promise ) { - const { app, params } = request + const { app, params, payload } = request const { model } = app if (!model) { @@ -64,6 +70,55 @@ export async function redirectOrMakeHandler( }) } + // External journey redirect + const { action = '' } = page.getFormParams(request) + if (payload && action.startsWith(FormAction.External)) { + // Find the external action and arguments + // `external-{externalAction}--{argname1}:{argvalue1}--{argname2}:{argvalue2}` + // E.g. external-postcode-lookup--name:wDFtgf--step:manual + const externalActionsWithArgs = action + .slice(`${FormAction.External}-`.length) + .split('--') + const externalAction = externalActionsWithArgs[0] as ExternalActions + const externalActionArgs = externalActionsWithArgs + .slice(1) + .map((arg) => arg.split(':')) + + switch (externalAction) { + case ExternalActions.PostcodeLookup: { + const args = Object.fromEntries( + externalActionArgs + ) as PostcodeLookupDispatchArgs + const componentName = args.name + const component = model.componentDefMap.get(componentName) + + if (!component) { + throw Boom.notFound(`No component found for ${componentName}`) + } + + if (component.type !== ComponentType.UkAddressField) { + throw Boom.internal( + `Invalid component type, expected UkAddressFieldComponent got ${component.type}` + ) + } + + return dispatch(request as FormRequestPayload, h, { + payload, + formName: model.name, + componentName, + componentHint: component.hint, + componentTitle: component.title || page.title, + step: args.step, + sourceUrl: request.url.toString() + }) + } + default: + throw Boom.internal( + `Invalid external action, expected one of '${Object.values(ExternalActions).join('|')}' got '${externalAction}'` + ) + } + } + const flash = cacheService.getFlash(request) const context = model.getFormContext(request, state, flash?.errors) const relevantPath = page.getRelevantPath(request, context) From ae1351bf0ea0f65bcfa91007453d2b824e7c6043 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:43:16 +0100 Subject: [PATCH 7/9] Remove loadFormPreHandler from postcode lookup routes --- src/server/plugins/postcode-lookup/index.js | 22 ++------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/server/plugins/postcode-lookup/index.js b/src/server/plugins/postcode-lookup/index.js index 41a8ae1e6..813ed4df9 100644 --- a/src/server/plugins/postcode-lookup/index.js +++ b/src/server/plugins/postcode-lookup/index.js @@ -1,4 +1,3 @@ -import { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js' import { getRoutes } from '~/src/server/plugins/postcode-lookup/routes/index.js' export const VIEW_PATH = 'src/server/plugins/postcode-lookup/views' @@ -11,25 +10,8 @@ export const postcodeLookupPlugin = { dependencies: ['@hapi/vision'], multiple: false, register(server, options) { - const loadFormPreHandler = makeLoadFormPreHandler( - server, - options.enginePluginOptions - ) - - const getRouteOptions = { - pre: [ - { - method: loadFormPreHandler - } - ] - } - - server.route( - /** @type {ServerRoute[]} */ ( - // @ts-expect-error - Request typing - getRoutes(getRouteOptions, options) - ) - ) + // @ts-expect-error - Request typing + server.route(getRoutes(options)) } } From e8432490ec6e40ba1b9386133dfe8d48158ac620 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:43:46 +0100 Subject: [PATCH 8/9] Update postcode lookup plugin to us external actions --- .../plugins/postcode-lookup/models/index.js | 166 ++++-------- .../plugins/postcode-lookup/routes/index.js | 237 +++++------------- .../views/postcode-lookup-details.html | 4 - 3 files changed, 112 insertions(+), 295 deletions(-) diff --git a/src/server/plugins/postcode-lookup/models/index.js b/src/server/plugins/postcode-lookup/models/index.js index 46a04bdf1..7e36a1cb0 100644 --- a/src/server/plugins/postcode-lookup/models/index.js +++ b/src/server/plugins/postcode-lookup/models/index.js @@ -1,17 +1,7 @@ -import { - hasComponentsEvenIfNoNext, - hasFormField, - slugSchema -} from '@defra/forms-model' import Joi from 'joi' -import { FORM_PREFIX } from '~/src/server/constants.js' import * as service from '~/src/server/plugins/postcode-lookup/service.js' -import { - crumbSchema, - pathSchema, - stateSchema -} from '~/src/server/schemas/index.js' +import { crumbSchema } from '~/src/server/schemas/index.js' // Field names/ids const postcodeQueryFieldName = 'postcodeQuery' @@ -132,25 +122,13 @@ async function getAddresses(postcodeQuery, buildingNameQuery, apiKey) { } } -/** - * @param {string} slug - * @param {FormStatus} [status] - */ -function constructFormUrl(slug, status) { - if (!status) { - return `${FORM_PREFIX}/${slug}` - } - - return `${FORM_PREFIX}/preview/${status}/${slug}` -} - /** * Get the details view fields - * @param {PostcodeLookupDetailsPayload | undefined} payload + * @param {PostcodeLookupDetailsData | undefined} details * @param {OptionalValidationErrorItem} postcodeQueryError * @param {OptionalValidationErrorItem} buildingNameQueryError */ -function getDetailsFields(payload, postcodeQueryError, buildingNameQueryError) { +function getDetailsFields(details, postcodeQueryError, buildingNameQueryError) { return { [postcodeQueryFieldName]: { id: postcodeQueryFieldName, @@ -161,7 +139,7 @@ function getDetailsFields(payload, postcodeQueryError, buildingNameQueryError) { hint: { text: 'For example, AA3 1AB' }, - value: payload?.postcodeQuery, + value: details?.postcodeQuery, errorMessage: postcodeQueryError && { text: postcodeQueryError.message } }, [buildingNameQueryFieldName]: { @@ -173,7 +151,7 @@ function getDetailsFields(payload, postcodeQueryError, buildingNameQueryError) { hint: { text: 'For example, 15 or Prospect Cottage' }, - value: payload?.buildingNameQuery, + value: details?.buildingNameQuery, errorMessage: buildingNameQueryError && { text: buildingNameQueryError.message } @@ -222,7 +200,7 @@ function getSelectFields( value: singleAddress ? singleAddress.uprn : payload?.uprn, errorMessage: uprnError && { text: uprnError.message }, items: hasMultipleAddresses - ? [{ text: selectLabelText }].concat( + ? [{ text: selectLabelText, value: '' }].concat( addresses.map((item) => ({ text: item.formatted, value: item.uprn @@ -302,37 +280,13 @@ function getManualFields( } } -/** - * @param {string} formPath - * @param {string} path - */ -function constructFormPageUrl(formPath, path) { - return `${formPath}${path}` -} - -/** - * Postcode lookup params schema - */ -export const paramsSchema = Joi.object() - .keys({ - slug: slugSchema, - path: pathSchema, - componentName: Joi.string().required(), - state: stateSchema.optional() - }) - .required() - export const stepSchema = Joi.string() .valid(...Object.keys(steps)) .required() const sharedPayloadSchemaKeys = { crumb: crumbSchema, - step: stepSchema, - [postcodeQueryFieldName]: Joi.string().trim().required().messages({ - '*': 'Enter a postcode' - }), - [buildingNameQueryFieldName]: Joi.string().trim().required().allow('').trim() + step: stepSchema } /** @@ -340,7 +294,17 @@ const sharedPayloadSchemaKeys = { * @type {ObjectSchema} */ export const detailsPayloadSchema = Joi.object() - .keys(sharedPayloadSchemaKeys) + .keys({ + ...sharedPayloadSchemaKeys, + [postcodeQueryFieldName]: Joi.string().trim().required().messages({ + '*': 'Enter a postcode' + }), + [buildingNameQueryFieldName]: Joi.string() + .trim() + .required() + .allow('') + .trim() + }) .required() /** @@ -362,8 +326,7 @@ export const selectPayloadSchema = Joi.object() */ export const manualPayloadSchema = Joi.object() .keys({ - crumb: crumbSchema, - step: stepSchema, + ...sharedPayloadSchemaKeys, [line1FieldName]: Joi.string().trim().required().messages({ '*': 'Enter address line 1' }), @@ -378,63 +341,27 @@ export const manualPayloadSchema = Joi.object() }) .required() -/** - * Gets page title - * @param {Page} page - * @param {ComponentDef} component - */ -export function getComponentTitle(page, component) { - if (hasComponentsEvenIfNoNext(page)) { - const formFields = page.components.filter(hasFormField) - - // When there's more than 1 form component on the page, use the component title - if (formFields.length > 1 || formFields[0] !== component) { - return component.title - } - } - - // Otherwise use the page title - return page.title -} - -/** - * Get postcode lookup session key - * @param {string} slug - * @param {FormStatus} [state] - */ -export function getKey(slug, state) { - return `postcode-lookup-${slug}-${state ?? ''}` -} - /** * Get the postcode lookup href - * @param {string} slug - the form slug - * @param {Page} page - the form page - * @param {UkAddressFieldComponent} component - the form component - * @param {FormStatus} [status] - the form status * @param {string} [step] - the postcode lookup step */ -function getHref(slug, page, component, status, step) { +function getHref(step) { const query = step ? `?step=${step}` : '' - const state = status ? `/${status}` : '' - return `${JOURNEY_BASE_URL}/${slug}${page.path}/${component.name}${state}${query}` + return `${JOURNEY_BASE_URL}${query}` } /** * The postcode lookup details form view model - * @param {PostcodeLookupDetailsModelData} data - * @param {PostcodeLookupDetailsPayload} [payload] + * @param {PostcodeLookupSessionData} data + * @param {PostcodeLookupDetailsData} [payload] * @param {Error} [err] */ export function detailsViewModel(data, payload, err) { - const { slug, title, page, component, status } = data - const pageTitle = getComponentTitle(page, component) - const formPath = constructFormUrl(slug, status) - const pagePath = constructFormPageUrl(formPath, page.path) + const { componentTitle: pageTitle, formName, sourceUrl } = data.initial const backLink = { - href: pagePath + href: sourceUrl } const { errors, postcodeQueryError, buildingNameQueryError } = @@ -442,7 +369,7 @@ export function detailsViewModel(data, payload, err) { // Model fields const fields = getDetailsFields( - payload, + payload ?? data.details, postcodeQueryError, buildingNameQueryError ) @@ -454,14 +381,14 @@ export function detailsViewModel(data, payload, err) { } const manualLink = { text: 'enter address manually', - href: getHref(slug, page, component, status, steps.manual) + href: getHref(steps.manual) } return { step: steps.details, showTitle: true, - name: title, - serviceUrl: formPath, + name: formName, + serviceUrl: sourceUrl, pageTitle, backLink, errors, @@ -472,13 +399,13 @@ export function detailsViewModel(data, payload, err) { /** * The postcode lookup select form view model - * @param {PostcodeLookupSelectModelData} data + * @param {{ session: PostcodeLookupSessionData, apiKey: string }} data * @param {PostcodeLookupSelectPayload} [payload] * @param {Error} [err] */ export async function selectViewModel(data, payload, err) { - const { slug, page, component, details, status, apiKey } = data - + const { session, apiKey } = data + const { details, initial } = session const { postcodeQuery, buildingNameQuery } = details // Search for addresses @@ -490,11 +417,9 @@ export async function selectViewModel(data, payload, err) { addressCount } = await getAddresses(postcodeQuery, buildingNameQuery, apiKey) - const title = hasAddresses - ? getComponentTitle(page, component) - : 'No address found' - const formPath = constructFormUrl(slug, status) - const href = getHref(slug, page, component, status) + const title = hasAddresses ? initial.componentTitle : 'No address found' + const formPath = initial.sourceUrl + const href = getHref() const backLink = { href } @@ -547,15 +472,14 @@ export async function selectViewModel(data, payload, err) { /** * The postcode lookup manual form view model - * @param {PostcodeLookupDetailsModelData} data + * @param {PostcodeLookupSessionData} data * @param {PostcodeLookupManualPayload} [payload] * @param {Error} [err] */ export function manualViewModel(data, payload, err) { - const { slug, title, page, component, status } = data - const pageTitle = getComponentTitle(page, component) - const formPath = constructFormUrl(slug, status) - const href = getHref(slug, page, component, status) + const { componentTitle, sourceUrl, componentHint } = data.initial + const formPath = sourceUrl + const href = getHref() const backLink = { href @@ -571,8 +495,8 @@ export function manualViewModel(data, payload, err) { } = buildErrors(err) // Model hint - const hint = component.hint && { - text: component.hint + const hint = componentHint && { + text: componentHint } // Model fields @@ -598,9 +522,9 @@ export function manualViewModel(data, payload, err) { return { step: steps.manual, showTitle: true, - name: title, + name: componentTitle, serviceUrl: formPath, - pageTitle, + pageTitle: componentTitle, backLink, errors, hint, @@ -612,8 +536,6 @@ export function manualViewModel(data, payload, err) { /** @typedef { ValidationErrorItem | undefined } OptionalValidationErrorItem */ /** - * @import { UkAddressFieldComponent, Page, ComponentDef } from '@defra/forms-model' * @import { ObjectSchema, ValidationErrorItem } from 'joi' - * @import { FormStatus } from '~/src/server/routes/types.js' - * @import { Address, PostcodeLookupDetailsData, PostcodeLookupDetailsModelData, PostcodeLookupDetailsPayload, PostcodeLookupManualPayload, PostcodeLookupSelectModelData, PostcodeLookupSelectPayload, PostcodeLookupSessionState } from '~/src/server/plugins/postcode-lookup/types.js' + * @import { Address, PostcodeLookupDetailsData, PostcodeLookupDetailsPayload, PostcodeLookupManualPayload, PostcodeLookupSelectPayload, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js' */ diff --git a/src/server/plugins/postcode-lookup/routes/index.js b/src/server/plugins/postcode-lookup/routes/index.js index 1667f9272..683051763 100644 --- a/src/server/plugins/postcode-lookup/routes/index.js +++ b/src/server/plugins/postcode-lookup/routes/index.js @@ -1,21 +1,14 @@ -import { ComponentType, hasComponentsEvenIfNoNext } from '@defra/forms-model' import Boom from '@hapi/boom' import { StatusCodes } from 'http-status-codes' import Joi from 'joi' -import { FORM_PREFIX } from '~/src/server/constants.js' -import { - checkFormStatus, - getCacheService -} from '~/src/server/plugins/engine/helpers.js' +import { getCacheService } from '~/src/server/plugins/engine/helpers.js' import { JOURNEY_BASE_URL, detailsPayloadSchema, detailsViewModel, - getKey, manualPayloadSchema, manualViewModel, - paramsSchema, selectPayloadSchema, selectViewModel, stepSchema, @@ -26,41 +19,20 @@ import * as service from '~/src/server/plugins/postcode-lookup/service.js' const viewName = 'postcode-lookup-details' /** - * Get the details of the source form elements associated with this journey + * Get the session state associated with this journey * @param {PostcodeLookupRequest} request */ -function getJourneyDetails(request) { - const { app, params } = request - const { model } = app - const { path, componentName } = params - - if (!model) { - throw Boom.notFound(`No model found for ${path}`) - } - - const { isPreview, state: status } = checkFormStatus(params) - const title = model.name - const page = model.pageDefMap.get(`/${path}`) - - if (!page) { - throw Boom.notFound(`No page found for ${path}`) - } - - const component = hasComponentsEvenIfNoNext(page) - ? page.components.find((c) => c.name === componentName) - : undefined - - if (!component) { - throw Boom.notFound(`No component found for name ${componentName}`) - } +function getSessionState(request) { + /** + * @type {PostcodeLookupSessionData | undefined} + */ + const data = request.yar.get(JOURNEY_BASE_URL) - if (component.type !== ComponentType.UkAddressField) { - throw Boom.internal( - `Invalid component type, expected UkAddressFieldComponent got ${component.type}` - ) + if (!data) { + throw Boom.notFound(`No data found for ${JOURNEY_BASE_URL}`) } - return { model, title, page, component, isPreview, status } + return data } /** @@ -94,68 +66,60 @@ async function updateComponentState(request, componentName, address) { }) } +/** + * Initialises and dispatches the request to the postcode lookup journey + * @param {FormRequestPayload} request - the source page + * @param {FormResponseToolkit} h - the source page + * @param {PostcodeLookupDispatchData} initial - the source data + */ +export function dispatch(request, h, initial) { + /** + * @type {PostcodeLookupSessionData} + */ + const data = { + initial, + details: { postcodeQuery: '', buildingNameQuery: '' } + } + + request.yar.set(JOURNEY_BASE_URL, data) + + const query = initial.step ? `?step=${initial.step}` : '' + + return h.redirect(`${JOURNEY_BASE_URL}${query}`).code(StatusCodes.SEE_OTHER) +} + /** * Gets the postcode lookup routes - * @param {RouteOptions} getRouteOptions - hapi route options * @param {PostcodeLookupConfiguration} options - ordnance survey api key */ -export function getRoutes(getRouteOptions, options) { - return [getRoute(getRouteOptions), postRoute(getRouteOptions, options)] +export function getRoutes(options) { + return [getRoute(), postRoute(options)] } /** - * @param {RouteOptions} getRouteOptions * @returns {ServerRoute} */ -function getRoute(getRouteOptions) { +function getRoute() { return { method: 'GET', - path: `${JOURNEY_BASE_URL}/{slug}/{path}/{componentName}/{state?}`, + path: JOURNEY_BASE_URL, handler(request, h) { - const { params, query } = request - const { slug, state: status } = params - const { step, clear } = query - const { title, page, component } = getJourneyDetails(request) - - /** - * Get the previous details from session - * @type {PostcodeLookupSessionState | undefined} - */ - let previous - - if (clear) { - /** - * @type {PostcodeLookupSessionState} - */ - const state = { - query, - details: undefined - } + const { query } = request + const { step } = query + const session = getSessionState(request) - request.yar.set(getKey(slug, status), state) - } else { - previous = request.yar.get(getKey(slug, status)) - } - - const data = { slug, page, title, component, status } const model = step === steps.manual - ? manualViewModel(data) - : detailsViewModel(data, previous?.details) + ? manualViewModel(session) + : detailsViewModel(session) return h.view(viewName, model) }, - // @ts-expect-error - Request typing options: { - ...getRouteOptions, validate: { - params: paramsSchema, query: Joi.object() .keys({ - step: Joi.string().allow(steps.details, steps.manual).optional(), - clear: Joi.boolean().optional(), - returnUrl: Joi.string().optional(), - force: Joi.boolean().optional() + step: Joi.string().allow(steps.details, steps.manual).optional() }) .optional() } @@ -164,14 +128,13 @@ function getRoute(getRouteOptions) { } /** - * @param {RouteOptions} getRouteOptions * @param {PostcodeLookupConfiguration} options * @returns {ServerRoute} */ -function postRoute(getRouteOptions, options) { +function postRoute(options) { return { method: 'POST', - path: `${JOURNEY_BASE_URL}/{slug}/{path}/{componentName}/{state?}`, + path: JOURNEY_BASE_URL, async handler(request, h) { const { payload } = request const { step } = payload @@ -184,17 +147,14 @@ function postRoute(getRouteOptions, options) { return selectPostHandler(request, h, options) } case steps.manual: { - return manualPostHandler(request, h, options) + return manualPostHandler(request, h) } default: throw Boom.badRequest(`Invalid step ${step}`) } }, - // @ts-expect-error - Request typing options: { - ...getRouteOptions, validate: { - params: paramsSchema, payload: Joi.object() .keys({ step: stepSchema @@ -212,33 +172,26 @@ function postRoute(getRouteOptions, options) { * @param {PostcodeLookupConfiguration} options */ async function detailsPostHandler(request, h, options) { - const { params, payload } = request - const { slug, state: status } = params - const { title, page, component } = getJourneyDetails(request) + const { payload } = request + const session = getSessionState(request) const { ordnanceSurveyApiKey: apiKey } = options const { value: details, error } = detailsPayloadSchema.validate(payload) - let data, model + let model if (error) { - data = { slug, title, page, component, status } - model = detailsViewModel(data, details, error) + model = detailsViewModel(session, details, error) return h.view(viewName, model) } - data = { slug, page, component, details, status, apiKey } - model = await selectViewModel(data) - const key = getKey(slug, status) + const { postcodeQuery, buildingNameQuery } = details + session.details = { postcodeQuery, buildingNameQuery } - /** - * Get the previous details from session - * @type {PostcodeLookupSessionState | undefined} - */ - const previous = request.yar.get(key) + // Store the updated session + request.yar.set(JOURNEY_BASE_URL, session) - // Store the new details in session - request.yar.set(key, previous ? { ...previous, details } : { details }) + model = await selectViewModel({ session, apiKey }) return h.view(viewName, model) } @@ -250,17 +203,13 @@ async function detailsPostHandler(request, h, options) { * @param {PostcodeLookupConfiguration} options */ async function selectPostHandler(request, h, options) { - const { params, payload } = request - const { slug, path, componentName, state: status } = params - const { page, component } = getJourneyDetails(request) + const { payload } = request + const session = getSessionState(request) const { ordnanceSurveyApiKey: apiKey } = options const { value: select, error } = selectPayloadSchema.validate(payload) if (error) { - const { postcodeQuery, buildingNameQuery } = select - const details = { postcodeQuery, buildingNameQuery } - const data = { slug, page, component, details, status, apiKey } - const model = await selectViewModel(data, select, error) + const model = await selectViewModel({ session, apiKey }, select, error) return h.view(viewName, model) } @@ -272,91 +221,41 @@ async function selectPostHandler(request, h, options) { throw Boom.internal(`UPRN ${property} not found`) } + const { componentName, sourceUrl } = session.initial await updateComponentState(request, componentName, property) // Redirect back to the source form page - const key = getKey(slug, status) - - /** - * Get the previous details from session - * @type {PostcodeLookupSessionState | undefined} - */ - const previous = request.yar.get(key) - const url = new URL( - `${FORM_PREFIX}/${slug}/${path}`, - options.enginePluginOptions.baseUrl - ) - - if (previous?.query) { - const query = previous.query - - if (query.returnUrl) { - url.searchParams.append('returnUrl', query.returnUrl) - } - - if (query.force !== undefined) { - url.searchParams.append('force', `${query.force}`) - } - } - - // Redirect back to the source form page - return h.redirect(url.toString()).code(StatusCodes.SEE_OTHER) + return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER) } /** * Post handler for the manual step * @param {PostcodeLookupPostRequest} request * @param {ResponseToolkit} h - * @param {PostcodeLookupConfiguration} options */ -async function manualPostHandler(request, h, options) { - const { params, payload } = request - const { slug, path, componentName, state: status } = params - const { title, page, component } = getJourneyDetails(request) +async function manualPostHandler(request, h) { + const { payload } = request + const session = getSessionState(request) const { value: manual, error } = manualPayloadSchema.validate(payload, { abortEarly: false }) if (error) { - const data = { slug, title, page, component, status } - const model = manualViewModel(data, manual, error) + const model = manualViewModel(session, manual, error) return h.view(viewName, model) } + const { componentName, sourceUrl } = session.initial await updateComponentState(request, componentName, manual) // Redirect back to the source form page - const key = getKey(slug, status) - - /** - * Get the previous details from session - * @type {PostcodeLookupSessionState | undefined} - */ - const previous = request.yar.get(key) - const url = new URL( - `${FORM_PREFIX}/${slug}/${path}`, - options.enginePluginOptions.baseUrl - ) - - if (previous?.query) { - const query = previous.query - - if (query.returnUrl) { - url.searchParams.append('returnUrl', query.returnUrl) - } - - if (query.force !== undefined) { - url.searchParams.append('force', `${query.force}`) - } - } - - // Redirect back to the source form page - return h.redirect(url.toString()).code(StatusCodes.SEE_OTHER) + return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER) } /** - * @import { ResponseToolkit, RouteOptions, ServerRoute } from '@hapi/hapi' - * @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupRequestRefs, PostcodeLookupPostRequest, PostcodeLookupSessionState, PostcodeLookupConfiguration } from '~/src/server/plugins/postcode-lookup/types.js' + * @import { ResponseToolkit, ServerRoute } from '@hapi/hapi' + * @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js' + * @import { FormRequestPayload, FormResponseToolkit } from '~/src/server/routes/types.js' */ diff --git a/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html b/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html index 48bbb26aa..6e3d98482 100644 --- a/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +++ b/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html @@ -31,10 +31,6 @@ {{ govukInput(fields.buildingNameQuery) }} {% case "select" %} - - {{ govukInput(fields.postcodeQuery) }} - {{ govukInput(fields.buildingNameQuery) }} - {%- set detailsHtml -%} {{ details.postcodeQuery }}{% if details.buildingNameQuery %} and {{ details.buildingNameQuery }}{% endif %} From 7995625f8e1a8c4c921a004494ed7c32d696d6f5 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 14 Oct 2025 10:44:01 +0100 Subject: [PATCH 9/9] Update postcode lookup types --- src/server/plugins/postcode-lookup/types.js | 75 ++++++++------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/src/server/plugins/postcode-lookup/types.js b/src/server/plugins/postcode-lookup/types.js index 9c74331d3..2a1cb9d12 100644 --- a/src/server/plugins/postcode-lookup/types.js +++ b/src/server/plugins/postcode-lookup/types.js @@ -1,67 +1,54 @@ /** * @typedef {{ * ordnanceSurveyApiKey: string - * enginePluginOptions: PluginOptions * }} PostcodeLookupConfiguration */ -// -// Model types -// - /** - * The postcode lookup details form view model data - * @typedef {object} PostcodeLookupDetailsData - * @property {string} postcodeQuery - postcode query - * @property {string} buildingNameQuery - Building name or number query + * @typedef {{ + * name: string + * step?: string + * }} PostcodeLookupDispatchArgs */ /** - * The postcode lookup details form view model data - * @typedef {object} PostcodeLookupDetailsModelData - * @property {string} slug - the form slug - * @property {string} title - the form title - * @property {Page} page - the form page - * @property {UkAddressFieldComponent} component - the form component - * @property {FormStatus} [status] - the form status + * @typedef {{ + * sourceUrl: string, + * formName: string + * componentName: string + * componentTitle: string, + * componentHint?: string + * step?: string, + * payload: FormPayload + * }} PostcodeLookupDispatchData */ /** - * The postcode lookup select form view model data - * @typedef {object} PostcodeLookupSelectModelData - * @property {string} slug - the form slug - * @property {Page} page - the form page - * @property {UkAddressFieldComponent} component - the form component - * @property {PostcodeLookupDetailsData} details - the lookup details - * @property {string} apiKey - the ordnance survey api key - * @property {FormStatus} [status] - the form status + * @typedef {{ + * initial: PostcodeLookupDispatchData + * details: PostcodeLookupDetailsData + * }} PostcodeLookupSessionData */ +// +// Model types +// + /** - * @typedef {object} PostcodeLookupSessionState - * @property {PostcodeLookupQuery} query - the source form page query - * @property {PostcodeLookupDetailsPayload | undefined} details - the current postcode lookup details + * The postcode lookup details form view model data + * @typedef {object} PostcodeLookupDetailsData + * @property {string} postcodeQuery - postcode query + * @property {string} buildingNameQuery - Building name or number query */ // // Route types // -/** - * @typedef {object} PostcodeLookupParams - * @property {string} slug - the source form slug - * @property {string} path - the source page path - * @property {string} componentName - the source component name - * @property {FormStatus} [state] - the source form status (draft/live) when in preview mode - */ - /** * Postcode lookup query params * @typedef {object} PostcodeLookupQuery * @property {string} [step] - step - * @property {boolean} [clear] - Clear session state flag - * @property {boolean} [force] - Force param (preview mode) - * @property {string} [returnUrl] - Return url (Back to summary page) */ /** @@ -74,26 +61,20 @@ */ /** - * @typedef {object} PostcodeLookupSelectPayloadProperties + * @typedef {object} PostcodeLookupSelectPayload * @property {string} step - step * @property {number} uprn - postcode */ -/** - * @typedef {PostcodeLookupDetailsPayload & PostcodeLookupSelectPayloadProperties} PostcodeLookupSelectPayload - */ - /** * Postcode lookup get request * @typedef {object} PostcodeLookupGetRequestRefs - * @property {PostcodeLookupParams} Params - Request parameters * @property {PostcodeLookupQuery} Query - Request query */ /** * Postcode lookup post request * @typedef {object} PostcodeLookupPostRequestRefs - * @property {PostcodeLookupParams} Params - Request parameters * @property {PostcodeLookupDetailsPayload | PostcodeLookupSelectPayload} Payload - Request payload */ @@ -159,7 +140,5 @@ /** * @import { Request } from '@hapi/hapi' - * @import { UkAddressFieldComponent, Page } from '@defra/forms-model' - * @import { PluginOptions } from '~/src/server/plugins/engine/types.js' - * @import { FormStatus } from '~/src/server/routes/types.js' + * @import { FormPayload } from '~/src/server/plugins/engine/types.js' */