Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/485768 page conditions #669

Merged
merged 10 commits into from
Jan 21, 2025
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
},
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@defra/forms-model": "^3.0.393",
"@defra/forms-model": "^3.0.395",
"@elastic/ecs-pino-format": "^1.5.0",
"@hapi/boom": "^10.0.1",
"@hapi/catbox": "^12.1.1",
Expand Down
7 changes: 6 additions & 1 deletion src/server/plugins/engine/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ControllerPath } from '@defra/forms-model'
import { ControllerPath, Engine } from '@defra/forms-model'
import Boom from '@hapi/boom'
import { type ResponseToolkit } from '@hapi/hapi'
import { format, parseISO } from 'date-fns'
Expand Down Expand Up @@ -155,6 +155,11 @@ export function findPage(model: FormModel | undefined, path?: string) {
}

export function getStartPath(model?: FormModel) {
if (model?.engine === Engine.V2) {
const startPath = normalisePath(model.def.pages.at(0)?.path)
return startPath ? `/${startPath}` : ControllerPath.Start
}

const startPath = normalisePath(model?.def.startPage)
return startPath ? `/${startPath}` : ControllerPath.Start
}
Expand Down
74 changes: 54 additions & 20 deletions src/server/plugins/engine/models/FormModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ConditionsModel,
ControllerPath,
ControllerType,
Engine,
formDefinitionSchema,
hasRepeater,
type ConditionWrapper,
Expand Down Expand Up @@ -34,10 +35,12 @@ import {
import { FormAction } from '~/src/server/routes/types.js'
import { merge } from '~/src/server/services/cacheService.js'

/**
* Responsible for instantiating the {@link PageControllerClass} and condition context from a form JSON
*/
export class FormModel {
/**
* Responsible for instantiating the {@link PageControllerClass} and condition context from a form JSON
*/
/** The runtime engine that should be used */
engine?: Engine

/** the entire form JSON as an object */
def: FormDefinition
Expand Down Expand Up @@ -78,6 +81,7 @@ export class FormModel {
]
})

this.engine = def.engine
this.def = def
this.lists = def.lists
this.sections = def.sections
Expand Down Expand Up @@ -228,27 +232,16 @@ export class FormModel {
// Find start page
let nextPage = findPage(this, startPath)

this.initialiseContext(context)

// Walk form pages from start
while (nextPage) {
const { collection, pageDef } = nextPage

// Add page to context
context.relevantPages.push(nextPage)

// Skip evaluation state for repeater pages
if (!hasRepeater(pageDef)) {
Object.assign(
context.evaluationState,
collection.getContextValueFromState(context.state)
)
}
this.assignEvaluationState(context, nextPage)

// Copy relevant state by expected keys
for (const key of nextPage.keys) {
if (typeof context.state[key] !== 'undefined') {
context.relevantState[key] = context.state[key]
}
}
this.assignRelevantState(context, nextPage)

// Stop at current page
if (nextPage.path === currentPath) {
Expand All @@ -263,6 +256,49 @@ export class FormModel {
context = validateFormState(request, page, context)

// Add paths for navigation
this.assignPaths(context)

return context
}

private initialiseContext(context: FormContext) {
// For the V2 engine, we need to initialise `evaluationState` to null
// for all keys. This is because the current condition evaluation
// library (eval-expr) will throw if an expression uses a key that is undefined.
if (this.engine === Engine.V2) {
for (const page of this.pages) {
for (const key of page.keys) {
context.evaluationState[key] = null
}
}
}
}

private assignEvaluationState(
context: FormContext,
page: PageControllerClass
) {
const { collection, pageDef } = page
// Skip evaluation state for repeater pages

if (!hasRepeater(pageDef)) {
Object.assign(
context.evaluationState,
collection.getContextValueFromState(context.state)
)
}
}

private assignRelevantState(context: FormContext, page: PageControllerClass) {
// Copy relevant state by expected keys
for (const key of page.keys) {
if (typeof context.state[key] !== 'undefined') {
context.relevantState[key] = context.state[key]
}
}
}

private assignPaths(context: FormContext) {
for (const { keys, path } of context.relevantPages) {
context.paths.push(path)

Expand All @@ -275,8 +311,6 @@ export class FormModel {
break
}
}

return context
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/server/plugins/engine/pageControllers/PageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
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
Expand All @@ -39,6 +40,7 @@ export class PageController {
pageDef: Page
title: string
section?: Section
condition?: ExecutableCondition
collection?: ComponentCollection
viewName = 'index'

Expand All @@ -55,6 +57,11 @@ export class PageController {
this.section = model.sections.find(
(section) => section.name === pageDef.section
)

// Resolve condition
if (pageDef.condition) {
this.condition = model.conditions[pageDef.condition]
}
}

get path() {
Expand Down
Loading
Loading