import { ControllerPath, type Events, type FormDefinition, type Page, type Section } from '@defra/forms-model' import Boom from '@hapi/boom' import { type Lifecycle, type RouteOptions, type Server } from '@hapi/hapi' import { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js' import { getSaveAndExitHelpers, getStartPath, normalisePath } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/index.js' import { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js' import { type FormContext, type PageViewModelBase } from '~/src/server/plugins/engine/types.js' import { type FormRequest, type FormRequestPayload, type FormRequestPayloadRefs, type FormRequestRefs, type FormResponseToolkit } from '~/src/server/routes/types.js' export class PageController { /** * The base class for all page controllers. Page controllers are responsible for generating the get and post route handlers when a user navigates to `/{id}/{path*}`. */ def: FormDefinition name?: string model: FormModel pageDef: Page id?: string title: string section?: Section condition?: ExecutableCondition events?: Events collection?: ComponentCollection viewName = 'index' allowSaveAndExit = false constructor(model: FormModel, pageDef: Page) { const { def } = model this.def = def this.name = def.name this.model = model this.pageDef = pageDef this.id = pageDef.id this.title = pageDef.title this.events = pageDef.events // Resolve section if (pageDef.section) { this.section = model.getSection(pageDef.section) } // Resolve condition if (pageDef.condition) { this.condition = model.conditions[pageDef.condition] } // Override view name if (pageDef.view) { this.viewName = pageDef.view } } get path() { return this.pageDef.path } get href() { const { path } = this return this.getHref(`/${normalisePath(path)}`) } get keys() { return this.collection?.keys ?? [] } /** * {@link https://hapi.dev/api/?v=20.1.2#route-options} */ get getRouteOptions(): RouteOptions { return {} } /** * {@link https://hapi.dev/api/?v=20.1.2#route-options} */ get postRouteOptions(): RouteOptions { return {} } get viewModel(): PageViewModelBase { const { name, section, title } = this const showTitle = true const pageTitle = title const sectionTitle = section?.hideTitle !== true ? section?.title : '' return { name, page: this, pageTitle, sectionTitle, showTitle, isStartPage: false, serviceUrl: this.getHref('/'), feedbackLink: this.feedbackLink, phaseTag: this.phaseTag } } get feedbackLink() { return this.def.options?.disableUserFeedback ? undefined : `/form/feedback?formId=${this.model.formId}` } get phaseTag() { const { def } = this return def.phaseBanner?.phase } getHref(path: string): string { const basePath = this.model.basePath if (path === '/') { return `/${basePath}` } // if ever the path is not prefixed with a slash, add it const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path let finalPath = `/${basePath}` if (relativeTargetPath) { finalPath += `/${relativeTargetPath}` } finalPath = finalPath.replace(/\/{2,}/g, '/') return finalPath } getStartPath() { return getStartPath(this.model) } getSummaryPath() { return ControllerPath.Summary.valueOf() } getStatusPath() { return ControllerPath.Status.valueOf() } makeGetRouteHandler(): ( request: FormRequest, context: FormContext, h: FormResponseToolkit ) => ReturnType> { return (request, context, h) => { const { viewModel, viewName } = this return h.view(viewName, viewModel) } } makePostRouteHandler(): ( request: FormRequestPayload, context: FormContext, h: FormResponseToolkit ) => ReturnType> { throw Boom.badRequest('Unsupported POST route handler for this page') } /** * Get supplementary state keys for clearing component state. * * This method returns page controller-level state keys only. The core component's * state key (the component's name) is managed separately by the framework and should * NOT be included in the returned array. * * Returns an empty array by default. Override in subclasses to provide * page-specific supplementary state keys (e.g., upload state, cached data). * @param _component - The component to get supplementary state keys for (optional) * @returns Array of supplementary state keys to clear (excluding the component name itself) */ getStateKeys(_component?: FormComponent): string[] { return [] } shouldShowSaveAndExit(server: Server): boolean { return getSaveAndExitHelpers(server) !== undefined && this.allowSaveAndExit } }