Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/server/devserver/dxt-devtool-baselayout.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "govuk/template.njk" %}
{% extends "index.html" %}

{% from "govuk/components/back-link/macro.njk" import govukBackLink -%}
{% from "govuk/components/footer/macro.njk" import govukFooter -%}
Expand Down Expand Up @@ -31,6 +31,10 @@
}) }}
{% endblock %}

{% block buttons %}
I've overridden the buttons
{% endblock %}

{% block header %}
{{ govukHeader({
homepageUrl: currentPath if context.isForceAccess else "https://defra.github.io/forms-engine-plugin/",
Expand All @@ -41,16 +45,6 @@
}) }}
{% endblock %}

{% block beforeContent %}
{% if backLink %}
{{ govukBackLink(backLink) }}
{% endif %}
{% endblock %}

{% block content %}
<h1 class="govuk-heading-l">Default page template</h1>
{% endblock %}

{% block bodyEnd %}
<script type="module" nonce="{{ cspNonce }}" src="{{ getDxtAssetPath("application.js") }}"></script>
{% endblock %}
Expand Down
39 changes: 38 additions & 1 deletion src/server/plugins/engine/configureEnginePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,51 @@ export const configureEnginePlugin = async ({
cacheName: 'session',
nunjucks: {
baseLayoutPath: 'dxt-devtool-baselayout.html',
paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner
paths: [
join(findPackageRoot(), 'src/server/devserver'), // custom layout to make it really clear this is not the same as the runner
join(findPackageRoot(), 'src/server/plugins/engine/views') // add engine views path so dxt-devtool-baselayout.html can find index.html
]
},
viewContext: devtoolContext,
preparePageEventRequestOptions,
onRequest,
baseUrl: 'http://localhost:3009', // always runs locally
saveAndReturn
}

/*
To enable custom buttons, use this config:

```
actionHandlers: {
'withdraw-submission': async (request, _) => {
await getCacheService(request.server).clearState(request)
return '/summary'
}
}
```

/*
To enable save and return for testing purposes, use this config:

```
saveAndReturn: {
keyGenerator: (_) => {
return `save-and-return`
},
sessionHydrator: (_) => {
return Promise.resolve({
applicantFirstName: 'Joe'
})
},
sessionPersister: () => {
console.log('no-op')
}
}
```

Then load http://localhost:3009/page-events-demo and the applicantFirstName should be pre-filled as 'Joe'
*/
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/server/plugins/engine/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Boom from '@hapi/boom'
import { type ResponseToolkit, type Server } from '@hapi/hapi'
import { format, parseISO } from 'date-fns'
import { StatusCodes } from 'http-status-codes'
import { type Schema, type ValidationErrorItem } from 'joi'
import Joi, { type Schema, type ValidationErrorItem } from 'joi'
import { Liquid } from 'liquidjs'

import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
Expand Down Expand Up @@ -376,6 +376,18 @@ export function evaluateTemplate(
})
}

export function getSchemas(server?: Server) {
if (!server) {
// only used in debug UI helper where validation isn't really important
return {
actionSchema: Joi.any().required(),
paramsSchema: Joi.any().required()
}
}

return getPluginOptions(server).schemas
}

export function getCacheService(server: Server) {
return getPluginOptions(server).cacheService
}
Expand Down
3 changes: 2 additions & 1 deletion src/server/plugins/engine/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
keyGenerator: Joi.function(),
sessionHydrator: Joi.function(),
sessionPersister: Joi.function()
}).optional()
}).optional(),
actionHandlers: Joi.object().pattern(Joi.string(), Joi.function())
})

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class PageController {
condition?: ExecutableCondition
events?: Events
collection?: ComponentCollection
viewName = 'index'
viewName = 'dxt-devtool-baselayout.html'
allowSaveAndReturn = false

constructor(model: FormModel, pageDef: Page) {
Expand Down
50 changes: 11 additions & 39 deletions src/server/plugins/engine/pageControllers/QuestionPageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@
type Link,
type Page
} from '@defra/forms-model'
import Boom from '@hapi/boom'
import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
import { type ValidationErrorItem } from 'joi'
import Joi, { type ValidationErrorItem } from 'joi'

import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
import {
getCacheService,
getErrors,
getSaveAndReturnHelpers,
getSchemas,
normalisePath,
proceed
} from '~/src/server/plugins/engine/helpers.js'
Expand All @@ -35,17 +34,12 @@
type FormSubmissionState
} from '~/src/server/plugins/engine/types.js'
import {
FormAction,
type FormRequest,
type FormRequestPayload,
type FormRequestPayloadRefs,
type FormRequestRefs
} from '~/src/server/routes/types.js'
import {
actionSchema,
crumbSchema,
paramsSchema
} from '~/src/server/schemas/index.js'
import { crumbSchema } from '~/src/server/schemas/index.js'
import { merge } from '~/src/server/services/cacheService.js'

export class QuestionPageController extends PageController {
Expand All @@ -64,7 +58,7 @@

this.collection.formSchema = this.collection.formSchema.keys({
crumb: crumbSchema,
action: actionSchema
action: Joi.string()
})
}

Expand Down Expand Up @@ -276,7 +270,7 @@
getFormParams(request?: FormContextRequest): FormPayloadParams {
const { payload } = request ?? {}

const result = paramsSchema.validate(payload, {
const result = getSchemas(request?.server).paramsSchema.validate(payload, {

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Route handlers › supports GET route handler

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:636:15)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Form journey › Summary › returns the summary path

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:544:23)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Form journey › Next › returns the next page path

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:544:23)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Form journey › Next getter › returns the next page links

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:544:23)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Condition evaluation context › filters on condition B

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:494:40)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Condition evaluation context › filters on condition A

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:438:40)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Condition evaluation context › combines state values for date fields

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:395:40)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Condition evaluation context › filters state by journey pages

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:310:38)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Component view models › returns the component view models for the page

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:189:15)

Check failure on line 273 in src/server/plugins/engine/pageControllers/QuestionPageController.ts

View workflow job for this annotation

GitHub Actions / Unit tests

QuestionPageController › Component view models › hides the page title for single form component pages

TypeError: Cannot read properties of undefined (reading 'paramsSchema') at QuestionPageController.getFormParams (src/server/plugins/engine/pageControllers/QuestionPageController.ts:273:47) at QuestionPageController.getFormParams [as getFormDataFromState] (src/server/plugins/engine/pageControllers/QuestionPageController.ts:256:25) at FormModel.getFormDataFromState [as getFormContext] (src/server/plugins/engine/models/FormModel.ts:336:21) at Object.getFormContext (src/server/plugins/engine/pageControllers/QuestionPageController.test.ts:189:15)
abortEarly: false,
stripUnknown: true
})
Expand Down Expand Up @@ -515,10 +509,13 @@
return h.view(viewName, viewModel)
}

// Check if this is a save-and-return action
// Check if this is a custom action that needs handling
const { action } = request.payload
if (action === FormAction.SaveAndReturn) {
return this.handleSaveAndReturn(request, context, h)
const { actionHandlers } = request.server.plugins['forms-engine-plugin']

if (action && action in actionHandlers) {
const exitPath = await actionHandlers[action](request, context)
return h.redirect(this.getHref(exitPath))
}

// Save and proceed
Expand All @@ -539,31 +536,6 @@
return proceed(request, h, nextUrl)
}

/**
* Handle save-and-return action by processing form data and redirecting to exit page
*/
async handleSaveAndReturn(
request: FormRequestPayload,
context: FormContext,
h: Pick<ResponseToolkit, 'redirect' | 'view'>
) {
const { state } = context

// Save the current state and redirect to exit page
const saveAndReturn = getSaveAndReturnHelpers(request.server)

if (!saveAndReturn?.sessionPersister) {
throw Boom.internal('Server misconfigured for save and return')
}

await saveAndReturn.sessionPersister(state, request)

const cacheService = getCacheService(request.server)
await cacheService.clearState(request)

return h.redirect(this.getHref('/exit'))
}

/**
* {@link https://hapi.dev/api/?v=20.1.2#route-options}
*/
Expand Down
32 changes: 32 additions & 0 deletions src/server/plugins/engine/pageControllers/buttonHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Boom from '@hapi/boom'

import {
getCacheService,
getSaveAndReturnHelpers
} from '~/src/server/plugins/engine/helpers.js'
import { type FormContext } from '~/src/server/plugins/engine/types.js'
import { type FormRequestPayload } from '~/src/server/routes/types.js'

/**
* Handle save-and-return action by processing form data and return exit path
*/
export async function handleSaveAndReturn(
request: FormRequestPayload,
context: FormContext
): Promise<string> {
const { state } = context

// Save the current state and return the exit path
const saveAndReturn = getSaveAndReturnHelpers(request.server)

if (!saveAndReturn?.sessionPersister) {
throw Boom.internal('Server misconfigured for save and return')
}

await saveAndReturn.sessionPersister(state, request)

const cacheService = getCacheService(request.server)
await cacheService.clearState(request)

return '/exit'
}
31 changes: 29 additions & 2 deletions src/server/plugins/engine/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {

import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
import { validatePluginOptions } from '~/src/server/plugins/engine/options.js'
import { handleSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/buttonHandlers.js'
import { getRoutes as getSaveAndReturnExitRoutes } from '~/src/server/plugins/engine/routes/exit.js'
import { getRoutes as getFileUploadStatusRoutes } from '~/src/server/plugins/engine/routes/file-upload.js'
import { makeLoadFormPreHandler } from '~/src/server/plugins/engine/routes/index.js'
Expand All @@ -17,9 +18,14 @@ import { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engi
import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
import { registerVision } from '~/src/server/plugins/engine/vision.js'
import {
FormAction,
type FormRequestPayloadRefs,
type FormRequestRefs
} from '~/src/server/routes/types.js'
import {
buildActionSchema,
buildParamsSchema
} from '~/src/server/schemas/index.js'
import { CacheService } from '~/src/server/services/index.js'

export const plugin = {
Expand Down Expand Up @@ -49,10 +55,18 @@ export const plugin = {

await registerVision(server, options)

const actionHandlers = getActionHandlers(options)
const customActions = Object.keys(actionHandlers)

server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)
server.expose('viewContext', viewContext)
server.expose('cacheService', cacheService)
server.expose('saveAndReturn', saveAndReturn)
server.expose('actionHandlers', actionHandlers)
server.expose('schemas', {
actionSchema: buildActionSchema(customActions),
paramsSchema: buildParamsSchema(customActions)
})

server.app.model = model

Expand Down Expand Up @@ -86,16 +100,29 @@ export const plugin = {

const routes = [
...getQuestionRoutes(
server,
getRouteOptions,
postRouteOptions,
preparePageEventRequestOptions
),
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),
...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),
...getRepeaterSummaryRoutes(server, getRouteOptions, postRouteOptions),
...getRepeaterItemDeleteRoutes(server, getRouteOptions, postRouteOptions),
...getSaveAndReturnExitRoutes(getRouteOptions),
...getFileUploadStatusRoutes()
]

server.route(routes as unknown as ServerRoute[]) // TODO
}
} satisfies Plugin<PluginOptions>

function getActionHandlers(pluginOptions: PluginOptions) {
let actionHandlers: PluginOptions['actionHandlers'] = {
[FormAction.SaveAndReturn]: handleSaveAndReturn
}

if (pluginOptions.actionHandlers) {
actionHandlers = pluginOptions.actionHandlers
}

return actionHandlers
}
6 changes: 5 additions & 1 deletion src/server/plugins/engine/routes/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
type ResponseObject,
type ResponseToolkit,
type RouteOptions,
type Server,
type ServerRoute
} from '@hapi/hapi'
import Joi from 'joi'

import {
getSchemas,
normalisePath,
proceed,
redirectPath
Expand All @@ -35,7 +37,6 @@ import {
type FormRequestRefs
} from '~/src/server/routes/types.js'
import {
actionSchema,
crumbSchema,
itemIdSchema,
pathSchema,
Expand Down Expand Up @@ -164,10 +165,13 @@ function isSuccessful(response: ResponseObject): boolean {
}

export function getRoutes(
server: Server,
getRouteOptions: RouteOptions<FormRequestRefs>,
postRouteOptions: RouteOptions<FormRequestPayloadRefs>,
preparePageEventRequestOptions?: PreparePageEventRequestOptions
): (ServerRoute<FormRequestRefs> | ServerRoute<FormRequestPayloadRefs>)[] {
const { actionSchema } = getSchemas(server)

return [
{
method: 'get',
Expand Down
Loading
Loading