@defra/forms-engine-plugin 3.0.9 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.public/stylesheets/application.min.css +3 -3
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/stylesheets/application.scss +14 -0
- package/.server/config/index.d.ts +1 -0
- package/.server/config/index.js +7 -0
- package/.server/config/index.js.map +1 -1
- package/.server/index.js +6 -2
- package/.server/index.js.map +1 -1
- package/.server/server/constants.d.ts +2 -0
- package/.server/server/constants.js +2 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/forms/components.json +7 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
- package/.server/server/plugins/engine/components/UkAddressField.d.ts +15 -9
- package/.server/server/plugins/engine/components/UkAddressField.js +67 -6
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +6 -3
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
- package/.server/server/plugins/engine/models/FormModel.js +3 -1
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +2 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +46 -3
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +15 -2
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.d.ts +2 -2
- package/.server/server/plugins/engine/routes/index.js +49 -9
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
- package/.server/server/plugins/engine/routes/questions.js +10 -10
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +2 -1
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js +31 -27
- package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
- package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +2 -1
- package/.server/server/plugins/engine/routes/repeaters/summary.js +31 -27
- package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +21 -3
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/validationHelpers.d.ts +15 -0
- package/.server/server/plugins/engine/validationHelpers.js +29 -0
- package/.server/server/plugins/engine/validationHelpers.js.map +1 -0
- package/.server/server/plugins/engine/views/components/ukaddressfield.html +50 -6
- package/.server/server/plugins/engine/vision.js +3 -1
- package/.server/server/plugins/engine/vision.js.map +1 -1
- package/.server/server/plugins/postcode-lookup/index.d.ts +8 -0
- package/.server/server/plugins/postcode-lookup/index.js +21 -0
- package/.server/server/plugins/postcode-lookup/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/models/index.d.ts +255 -0
- package/.server/server/plugins/postcode-lookup/models/index.js +517 -0
- package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/routes/index.d.ts +19 -0
- package/.server/server/plugins/postcode-lookup/routes/index.js +267 -0
- package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/service.d.ts +26 -0
- package/.server/server/plugins/postcode-lookup/service.js +148 -0
- package/.server/server/plugins/postcode-lookup/service.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/service.test.js +144 -0
- package/.server/server/plugins/postcode-lookup/service.test.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.d.ts +282 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js +370 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.d.ts +131 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js +195 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.d.ts +51 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js +52 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/types.d.ts +204 -0
- package/.server/server/plugins/postcode-lookup/types.js +144 -0
- package/.server/server/plugins/postcode-lookup/types.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
- package/.server/server/routes/types.d.ts +7 -2
- package/.server/server/routes/types.js +6 -0
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/schemas/index.js +1 -1
- package/.server/server/schemas/index.js.map +1 -1
- package/.server/server/types.d.ts +1 -0
- package/.server/server/types.js.map +1 -1
- package/package.json +2 -2
- package/src/client/stylesheets/application.scss +14 -0
- package/src/config/index.ts +9 -1
- package/src/index.ts +5 -4
- package/src/server/constants.js +2 -0
- package/src/server/forms/components.json +7 -0
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
- package/src/server/plugins/engine/components/UkAddressField.test.ts +50 -27
- package/src/server/plugins/engine/components/UkAddressField.ts +91 -8
- package/src/server/plugins/engine/configureEnginePlugin.ts +5 -3
- package/src/server/plugins/engine/helpers.test.ts +2 -1
- package/src/server/plugins/engine/models/FormModel.ts +10 -2
- package/src/server/plugins/engine/options.js +2 -1
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +2 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +9 -4
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +69 -1
- package/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts +2 -1
- package/src/server/plugins/engine/plugin.ts +22 -4
- package/src/server/plugins/engine/routes/index.test.ts +317 -0
- package/src/server/plugins/engine/routes/index.ts +81 -8
- package/src/server/plugins/engine/routes/questions.test.ts +126 -15
- package/src/server/plugins/engine/routes/questions.ts +71 -57
- package/src/server/plugins/engine/routes/repeaters/item-delete.test.ts +83 -0
- package/src/server/plugins/engine/routes/repeaters/item-delete.ts +39 -33
- package/src/server/plugins/engine/routes/repeaters/summary.test.ts +75 -0
- package/src/server/plugins/engine/routes/repeaters/summary.ts +28 -22
- package/src/server/plugins/engine/types.ts +27 -8
- package/src/server/plugins/engine/validationHelpers.ts +48 -0
- package/src/server/plugins/engine/views/components/ukaddressfield.html +50 -6
- package/src/server/plugins/engine/vision.ts +6 -0
- package/src/server/plugins/postcode-lookup/index.js +21 -0
- package/src/server/plugins/postcode-lookup/models/index.js +549 -0
- package/src/server/plugins/postcode-lookup/routes/index.js +258 -0
- package/src/server/plugins/postcode-lookup/service.js +188 -0
- package/src/server/plugins/postcode-lookup/service.test.js +177 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js +382 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/query.js +200 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js +53 -0
- package/src/server/plugins/postcode-lookup/types.js +143 -0
- package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
- package/src/server/postcode-lookup.test.ts +64 -0
- package/src/server/routes/types.ts +11 -2
- package/src/server/schemas/index.ts +5 -7
- package/src/server/types.ts +1 -0
|
@@ -12,6 +12,10 @@ import Boom from '@hapi/boom'
|
|
|
12
12
|
import { type RouteOptions } from '@hapi/hapi'
|
|
13
13
|
import { type ValidationErrorItem } from 'joi'
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
EXTERNAL_STATE_APPENDAGE,
|
|
17
|
+
EXTERNAL_STATE_PAYLOAD
|
|
18
|
+
} from '~/src/server/constants.js'
|
|
15
19
|
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
16
20
|
import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
|
|
17
21
|
import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
|
|
@@ -35,6 +39,7 @@ import {
|
|
|
35
39
|
type FormStateValue,
|
|
36
40
|
type FormSubmissionState
|
|
37
41
|
} from '~/src/server/plugins/engine/types.js'
|
|
42
|
+
import { getComponentsByType } from '~/src/server/plugins/engine/validationHelpers.js'
|
|
38
43
|
import {
|
|
39
44
|
FormAction,
|
|
40
45
|
type FormRequest,
|
|
@@ -492,6 +497,11 @@ export class QuestionPageController extends PageController {
|
|
|
492
497
|
) => {
|
|
493
498
|
const { collection, viewName, model } = this
|
|
494
499
|
const { isForceAccess, state, evaluationState } = context
|
|
500
|
+
const action = request.payload.action
|
|
501
|
+
|
|
502
|
+
if (action?.startsWith(FormAction.External)) {
|
|
503
|
+
return this.dispatchExternal(request, h, context)
|
|
504
|
+
}
|
|
495
505
|
|
|
496
506
|
/**
|
|
497
507
|
* If there are any errors, render the page with the parsed errors
|
|
@@ -515,7 +525,6 @@ export class QuestionPageController extends PageController {
|
|
|
515
525
|
await this.setState(request, state)
|
|
516
526
|
|
|
517
527
|
// Check if this is a save-and-exit action
|
|
518
|
-
const { action } = request.payload
|
|
519
528
|
if (action === FormAction.SaveAndExit) {
|
|
520
529
|
return this.handleSaveAndExit(request, context, h)
|
|
521
530
|
}
|
|
@@ -525,6 +534,65 @@ export class QuestionPageController extends PageController {
|
|
|
525
534
|
}
|
|
526
535
|
}
|
|
527
536
|
|
|
537
|
+
private dispatchExternal(
|
|
538
|
+
request: FormRequestPayload,
|
|
539
|
+
h: FormResponseToolkit,
|
|
540
|
+
context: FormContext
|
|
541
|
+
) {
|
|
542
|
+
const { externalComponents } = getComponentsByType()
|
|
543
|
+
const action = request.payload.action ?? ''
|
|
544
|
+
|
|
545
|
+
// Find the external action and arguments
|
|
546
|
+
// `external-{componentName}--{argname1}:{argvalue1}--{argname2}:{argvalue2}`
|
|
547
|
+
// E.g. external-abcdef--amount:10--step:manual
|
|
548
|
+
const externalActionsWithArgs = action
|
|
549
|
+
.slice(`${FormAction.External}-`.length)
|
|
550
|
+
.split('--')
|
|
551
|
+
|
|
552
|
+
const externalActionArgs = externalActionsWithArgs
|
|
553
|
+
.slice(1)
|
|
554
|
+
.map((arg) => arg.split(':'))
|
|
555
|
+
|
|
556
|
+
const args = Object.fromEntries(externalActionArgs) as Record<
|
|
557
|
+
string,
|
|
558
|
+
string
|
|
559
|
+
>
|
|
560
|
+
|
|
561
|
+
const componentName = externalActionsWithArgs[0]
|
|
562
|
+
const component = this.model.componentDefMap.get(componentName)
|
|
563
|
+
const componentType = component?.type
|
|
564
|
+
|
|
565
|
+
if (!componentType) {
|
|
566
|
+
throw Boom.internal(
|
|
567
|
+
`External component of type ${componentType} not found`
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const selectedComponent = externalComponents.get(componentType)
|
|
572
|
+
|
|
573
|
+
if (!selectedComponent) {
|
|
574
|
+
throw Boom.internal(`External component ${componentName} not found`)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Stash payload without crumb and action
|
|
578
|
+
const stashedPayload = {
|
|
579
|
+
...context.payload,
|
|
580
|
+
crumb: undefined,
|
|
581
|
+
action: undefined
|
|
582
|
+
}
|
|
583
|
+
request.yar.flash(EXTERNAL_STATE_PAYLOAD, stashedPayload, true)
|
|
584
|
+
|
|
585
|
+
// Clear any previous state appendage
|
|
586
|
+
request.yar.clear(EXTERNAL_STATE_APPENDAGE)
|
|
587
|
+
|
|
588
|
+
return selectedComponent.dispatcher(request, h, {
|
|
589
|
+
component,
|
|
590
|
+
controller: this,
|
|
591
|
+
sourceUrl: request.url.toString(),
|
|
592
|
+
actionArgs: args
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
528
596
|
proceed(
|
|
529
597
|
request: FormContextRequest,
|
|
530
598
|
h: FormResponseToolkit,
|
|
@@ -15,6 +15,7 @@ import { getRoutes as getRepeaterItemDeleteRoutes } from '~/src/server/plugins/e
|
|
|
15
15
|
import { getRoutes as getRepeaterSummaryRoutes } from '~/src/server/plugins/engine/routes/repeaters/summary.js'
|
|
16
16
|
import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
|
|
17
17
|
import { registerVision } from '~/src/server/plugins/engine/vision.js'
|
|
18
|
+
import { postcodeLookupPlugin } from '~/src/server/plugins/postcode-lookup/index.js'
|
|
18
19
|
import {
|
|
19
20
|
type FormRequestPayloadRefs,
|
|
20
21
|
type FormRequestRefs
|
|
@@ -34,7 +35,9 @@ export const plugin = {
|
|
|
34
35
|
saveAndExit,
|
|
35
36
|
nunjucks: nunjucksOptions,
|
|
36
37
|
viewContext,
|
|
37
|
-
preparePageEventRequestOptions
|
|
38
|
+
preparePageEventRequestOptions,
|
|
39
|
+
onRequest,
|
|
40
|
+
ordnanceSurveyApiKey
|
|
38
41
|
} = options
|
|
39
42
|
|
|
40
43
|
const cacheService =
|
|
@@ -44,6 +47,16 @@ export const plugin = {
|
|
|
44
47
|
|
|
45
48
|
await registerVision(server, options)
|
|
46
49
|
|
|
50
|
+
// Register the postcode lookup plugin only if we have an OS api key
|
|
51
|
+
if (ordnanceSurveyApiKey) {
|
|
52
|
+
await server.register({
|
|
53
|
+
plugin: postcodeLookupPlugin,
|
|
54
|
+
options: {
|
|
55
|
+
ordnanceSurveyApiKey
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)
|
|
48
61
|
server.expose('viewContext', viewContext)
|
|
49
62
|
server.expose('cacheService', cacheService)
|
|
@@ -83,10 +96,15 @@ export const plugin = {
|
|
|
83
96
|
...getQuestionRoutes(
|
|
84
97
|
getRouteOptions,
|
|
85
98
|
postRouteOptions,
|
|
86
|
-
preparePageEventRequestOptions
|
|
99
|
+
preparePageEventRequestOptions,
|
|
100
|
+
onRequest
|
|
101
|
+
),
|
|
102
|
+
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions, onRequest),
|
|
103
|
+
...getRepeaterItemDeleteRoutes(
|
|
104
|
+
getRouteOptions,
|
|
105
|
+
postRouteOptions,
|
|
106
|
+
onRequest
|
|
87
107
|
),
|
|
88
|
-
...getRepeaterSummaryRoutes(getRouteOptions, postRouteOptions),
|
|
89
|
-
...getRepeaterItemDeleteRoutes(getRouteOptions, postRouteOptions),
|
|
90
108
|
...getFileUploadStatusRoutes()
|
|
91
109
|
]
|
|
92
110
|
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import Boom from '@hapi/boom'
|
|
2
|
+
import { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
findPage,
|
|
6
|
+
getCacheService,
|
|
7
|
+
getPage,
|
|
8
|
+
proceed
|
|
9
|
+
} from '~/src/server/plugins/engine/helpers.js'
|
|
10
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
11
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
12
|
+
import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
|
|
13
|
+
import {
|
|
14
|
+
type AnyFormRequest,
|
|
15
|
+
type OnRequestCallback
|
|
16
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
17
|
+
import { type FormResponseToolkit } from '~/src/server/routes/types.js'
|
|
18
|
+
|
|
19
|
+
jest.mock('~/src/server/plugins/engine/helpers')
|
|
20
|
+
|
|
21
|
+
describe('redirectOrMakeHandler', () => {
|
|
22
|
+
const mockServer = {} as unknown as Parameters<
|
|
23
|
+
typeof redirectOrMakeHandler
|
|
24
|
+
>[0]['server']
|
|
25
|
+
const mockRequest: AnyFormRequest = {
|
|
26
|
+
server: mockServer,
|
|
27
|
+
app: {},
|
|
28
|
+
yar: { flash: () => [] },
|
|
29
|
+
params: { path: 'test-path' },
|
|
30
|
+
query: {}
|
|
31
|
+
} as unknown as AnyFormRequest
|
|
32
|
+
|
|
33
|
+
const mockH: FormResponseToolkit = {
|
|
34
|
+
redirect: jest.fn(),
|
|
35
|
+
view: jest.fn(),
|
|
36
|
+
continue: Symbol('continue')
|
|
37
|
+
} as unknown as FormResponseToolkit
|
|
38
|
+
|
|
39
|
+
let mockPage: PageControllerClass
|
|
40
|
+
|
|
41
|
+
const mockModel: FormModel = {
|
|
42
|
+
def: {
|
|
43
|
+
metadata: {
|
|
44
|
+
submission: { code: 'TEST-CODE' }
|
|
45
|
+
} as { submission: { code: string } }
|
|
46
|
+
},
|
|
47
|
+
getFormContext: jest.fn().mockReturnValue({
|
|
48
|
+
isForceAccess: false,
|
|
49
|
+
data: {}
|
|
50
|
+
})
|
|
51
|
+
} as unknown as FormModel
|
|
52
|
+
|
|
53
|
+
const mockMakeHandler = jest
|
|
54
|
+
.fn()
|
|
55
|
+
.mockResolvedValue({ statusCode: 200 } as ResponseObject)
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.clearAllMocks()
|
|
59
|
+
mockRequest.app = { model: mockModel }
|
|
60
|
+
|
|
61
|
+
// Reset mock page
|
|
62
|
+
mockPage = {
|
|
63
|
+
getState: jest.fn().mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
64
|
+
mergeState: jest
|
|
65
|
+
.fn()
|
|
66
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
67
|
+
getRelevantPath: jest.fn().mockReturnValue('/test-path'),
|
|
68
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
69
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
70
|
+
path: '/test-path'
|
|
71
|
+
} as unknown as PageControllerClass
|
|
72
|
+
|
|
73
|
+
// Reset mock model
|
|
74
|
+
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
75
|
+
isForceAccess: false,
|
|
76
|
+
data: {}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Setup mocks
|
|
80
|
+
;(getCacheService as jest.Mock).mockReturnValue({
|
|
81
|
+
getFlash: jest.fn().mockReturnValue({ errors: [] })
|
|
82
|
+
})
|
|
83
|
+
;(getPage as jest.Mock).mockReturnValue(mockPage)
|
|
84
|
+
;(findPage as jest.Mock).mockReturnValue({ next: [] })
|
|
85
|
+
;(proceed as jest.Mock).mockReturnValue({ statusCode: 302 })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('onRequest callback functionality', () => {
|
|
89
|
+
it('should call onRequest callback when provided', async () => {
|
|
90
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
91
|
+
.fn()
|
|
92
|
+
.mockResolvedValue(undefined)
|
|
93
|
+
|
|
94
|
+
await redirectOrMakeHandler(
|
|
95
|
+
mockRequest,
|
|
96
|
+
mockH,
|
|
97
|
+
onRequestCallback,
|
|
98
|
+
mockMakeHandler
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
expect(onRequestCallback).toHaveBeenCalledWith(
|
|
102
|
+
mockRequest,
|
|
103
|
+
mockH as ResponseToolkit,
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
isForceAccess: false,
|
|
106
|
+
data: {}
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should not call onRequest callback when not provided', async () => {
|
|
112
|
+
const onRequestCallback = jest.fn()
|
|
113
|
+
|
|
114
|
+
await redirectOrMakeHandler(
|
|
115
|
+
mockRequest,
|
|
116
|
+
mockH,
|
|
117
|
+
undefined,
|
|
118
|
+
mockMakeHandler
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
expect(onRequestCallback).not.toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should return takeover response when onRequest returns takeover response', async () => {
|
|
125
|
+
const takeoverResponse = {
|
|
126
|
+
statusCode: 302,
|
|
127
|
+
headers: { location: '/redirect-url' },
|
|
128
|
+
_takeover: true
|
|
129
|
+
} as unknown as ResponseObject
|
|
130
|
+
|
|
131
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
132
|
+
.fn()
|
|
133
|
+
.mockResolvedValue(takeoverResponse)
|
|
134
|
+
|
|
135
|
+
const result = await redirectOrMakeHandler(
|
|
136
|
+
mockRequest,
|
|
137
|
+
mockH,
|
|
138
|
+
onRequestCallback,
|
|
139
|
+
mockMakeHandler
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(takeoverResponse)
|
|
143
|
+
expect(mockMakeHandler).not.toHaveBeenCalled()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should continue processing when onRequest returns h.continue', async () => {
|
|
147
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
148
|
+
.fn()
|
|
149
|
+
.mockResolvedValue(mockH.continue)
|
|
150
|
+
|
|
151
|
+
await redirectOrMakeHandler(
|
|
152
|
+
mockRequest,
|
|
153
|
+
mockH,
|
|
154
|
+
onRequestCallback,
|
|
155
|
+
mockMakeHandler
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(mockPage, expect.any(Object))
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should handle onRequest callback errors', async () => {
|
|
162
|
+
const error = new Error('onRequest callback error')
|
|
163
|
+
const onRequestCallback: OnRequestCallback = jest
|
|
164
|
+
.fn()
|
|
165
|
+
.mockRejectedValue(error)
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
redirectOrMakeHandler(
|
|
169
|
+
mockRequest,
|
|
170
|
+
mockH,
|
|
171
|
+
onRequestCallback,
|
|
172
|
+
mockMakeHandler
|
|
173
|
+
)
|
|
174
|
+
).rejects.toThrow('onRequest callback error')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('existing functionality', () => {
|
|
179
|
+
it('should throw error when model is missing', async () => {
|
|
180
|
+
mockRequest.app = {}
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
redirectOrMakeHandler(mockRequest, mockH, undefined, mockMakeHandler)
|
|
184
|
+
).rejects.toThrow(Boom.notFound('No model found for /test-path'))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should call makeHandler when page is relevant', async () => {
|
|
188
|
+
const testPage = {
|
|
189
|
+
getState: jest
|
|
190
|
+
.fn()
|
|
191
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
192
|
+
mergeState: jest
|
|
193
|
+
.fn()
|
|
194
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
195
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
196
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
197
|
+
getRelevantPath: jest.fn().mockReturnValue('/test-path'),
|
|
198
|
+
path: '/test-path'
|
|
199
|
+
} as unknown as PageControllerClass
|
|
200
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
201
|
+
|
|
202
|
+
await redirectOrMakeHandler(
|
|
203
|
+
mockRequest,
|
|
204
|
+
mockH,
|
|
205
|
+
undefined,
|
|
206
|
+
mockMakeHandler
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(testPage, expect.any(Object))
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should call makeHandler when context has force access', async () => {
|
|
213
|
+
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
214
|
+
isForceAccess: true,
|
|
215
|
+
data: {}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await redirectOrMakeHandler(
|
|
219
|
+
mockRequest,
|
|
220
|
+
mockH,
|
|
221
|
+
undefined,
|
|
222
|
+
mockMakeHandler
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
expect(mockMakeHandler).toHaveBeenCalledWith(mockPage, expect.any(Object))
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should redirect when page is not relevant', async () => {
|
|
229
|
+
const testPage = {
|
|
230
|
+
getState: jest
|
|
231
|
+
.fn()
|
|
232
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
233
|
+
mergeState: jest
|
|
234
|
+
.fn()
|
|
235
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
236
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
237
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
238
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
239
|
+
path: '/test-path'
|
|
240
|
+
} as unknown as PageControllerClass
|
|
241
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
242
|
+
|
|
243
|
+
await redirectOrMakeHandler(
|
|
244
|
+
mockRequest,
|
|
245
|
+
mockH,
|
|
246
|
+
undefined,
|
|
247
|
+
mockMakeHandler
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect(proceed).toHaveBeenCalledWith(mockRequest, mockH, '/test-href')
|
|
251
|
+
expect(mockMakeHandler).not.toHaveBeenCalled()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should set returnUrl when redirecting and next pages exist', async () => {
|
|
255
|
+
const testPage = {
|
|
256
|
+
getState: jest
|
|
257
|
+
.fn()
|
|
258
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
259
|
+
mergeState: jest
|
|
260
|
+
.fn()
|
|
261
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
262
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
263
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
264
|
+
path: '/test-path',
|
|
265
|
+
getHref: jest
|
|
266
|
+
.fn()
|
|
267
|
+
.mockReturnValueOnce('/summary-href') // First call: for summaryPath (returnUrl)
|
|
268
|
+
.mockReturnValueOnce('/relevant-path-href') // Second call: for relevantPath (redirect)
|
|
269
|
+
} as unknown as PageControllerClass
|
|
270
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
271
|
+
;(findPage as jest.Mock).mockReturnValue({ next: ['next-page'] })
|
|
272
|
+
|
|
273
|
+
await redirectOrMakeHandler(
|
|
274
|
+
mockRequest,
|
|
275
|
+
mockH,
|
|
276
|
+
undefined,
|
|
277
|
+
mockMakeHandler
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
expect(mockRequest.query.returnUrl).toBe('/summary-href')
|
|
281
|
+
expect(proceed).toHaveBeenCalledWith(
|
|
282
|
+
mockRequest,
|
|
283
|
+
mockH,
|
|
284
|
+
'/relevant-path-href'
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should not set returnUrl when redirecting and no next pages exist', async () => {
|
|
289
|
+
const testPage = {
|
|
290
|
+
getState: jest
|
|
291
|
+
.fn()
|
|
292
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
293
|
+
mergeState: jest
|
|
294
|
+
.fn()
|
|
295
|
+
.mockResolvedValue({ $$__referenceNumber: 'REF-123' }),
|
|
296
|
+
getSummaryPath: jest.fn().mockReturnValue('/summary'),
|
|
297
|
+
getHref: jest.fn().mockReturnValue('/test-href'),
|
|
298
|
+
getRelevantPath: jest.fn().mockReturnValue('/other-path'),
|
|
299
|
+
path: '/test-path'
|
|
300
|
+
} as unknown as PageControllerClass
|
|
301
|
+
;(getPage as jest.Mock).mockReturnValue(testPage)
|
|
302
|
+
const returnUrlBefore = mockRequest.query.returnUrl
|
|
303
|
+
;(findPage as jest.Mock).mockReturnValue({ next: [] })
|
|
304
|
+
|
|
305
|
+
await redirectOrMakeHandler(
|
|
306
|
+
mockRequest,
|
|
307
|
+
mockH,
|
|
308
|
+
undefined,
|
|
309
|
+
mockMakeHandler
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// returnUrl should not be set if next pages don't exist
|
|
313
|
+
expect(mockRequest.query.returnUrl).toBe(returnUrlBefore)
|
|
314
|
+
expect(proceed).toHaveBeenCalledWith(mockRequest, mockH, '/test-href')
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
} from '@hapi/hapi'
|
|
7
7
|
import { isEqual } from 'date-fns'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
EXTERNAL_STATE_APPENDAGE,
|
|
11
|
+
EXTERNAL_STATE_PAYLOAD,
|
|
12
|
+
PREVIEW_PATH_PREFIX
|
|
13
|
+
} from '~/src/server/constants.js'
|
|
14
|
+
import {
|
|
15
|
+
FormComponent,
|
|
16
|
+
isFormState
|
|
17
|
+
} from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
10
18
|
import {
|
|
11
19
|
checkEmailAddressForLiveFormSubmission,
|
|
12
20
|
checkFormStatus,
|
|
@@ -22,7 +30,11 @@ import { generateUniqueReference } from '~/src/server/plugins/engine/referenceNu
|
|
|
22
30
|
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
|
|
23
31
|
import {
|
|
24
32
|
type AnyFormRequest,
|
|
33
|
+
type ExternalStateAppendage,
|
|
25
34
|
type FormContext,
|
|
35
|
+
type FormPayload,
|
|
36
|
+
type FormSubmissionState,
|
|
37
|
+
type OnRequestCallback,
|
|
26
38
|
type PluginOptions
|
|
27
39
|
} from '~/src/server/plugins/engine/types.js'
|
|
28
40
|
import {
|
|
@@ -33,6 +45,7 @@ import {
|
|
|
33
45
|
export async function redirectOrMakeHandler(
|
|
34
46
|
request: AnyFormRequest,
|
|
35
47
|
h: FormResponseToolkit,
|
|
48
|
+
onRequest: OnRequestCallback | undefined,
|
|
36
49
|
makeHandler: (
|
|
37
50
|
page: PageControllerClass,
|
|
38
51
|
context: FormContext
|
|
@@ -64,11 +77,21 @@ export async function redirectOrMakeHandler(
|
|
|
64
77
|
})
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
state = await importExternalComponentState(request, page, state)
|
|
81
|
+
|
|
67
82
|
const flash = cacheService.getFlash(request)
|
|
68
83
|
const context = model.getFormContext(request, state, flash?.errors)
|
|
69
84
|
const relevantPath = page.getRelevantPath(request, context)
|
|
70
85
|
const summaryPath = page.getSummaryPath()
|
|
71
86
|
|
|
87
|
+
// Call the onRequest callback if it has been supplied
|
|
88
|
+
if (onRequest) {
|
|
89
|
+
const result = await onRequest(request, h, context)
|
|
90
|
+
if (result !== h.continue) {
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
// Return handler for relevant pages or preview URL direct access
|
|
73
96
|
if (relevantPath.startsWith(page.path) || context.isForceAccess) {
|
|
74
97
|
return makeHandler(page, context)
|
|
@@ -85,11 +108,66 @@ export async function redirectOrMakeHandler(
|
|
|
85
108
|
return proceed(request, h, page.getHref(relevantPath))
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
async function importExternalComponentState(
|
|
112
|
+
request: AnyFormRequest,
|
|
113
|
+
page: PageControllerClass,
|
|
114
|
+
state: FormSubmissionState
|
|
115
|
+
): Promise<FormSubmissionState> {
|
|
116
|
+
const externalComponentData = request.yar.flash(EXTERNAL_STATE_APPENDAGE)
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(externalComponentData)) {
|
|
119
|
+
return state
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const typedStateAppendage = externalComponentData as ExternalStateAppendage
|
|
123
|
+
const componentName = typedStateAppendage.component
|
|
124
|
+
const stateAppendage = typedStateAppendage.data
|
|
125
|
+
const component = request.app.model?.componentMap.get(componentName)
|
|
126
|
+
|
|
127
|
+
if (!component) {
|
|
128
|
+
throw new Error(`Component ${componentName} not found in form`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!(component instanceof FormComponent)) {
|
|
132
|
+
throw new TypeError(
|
|
133
|
+
`Component ${componentName} is not a FormComponent and does not support isState`
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isStateValid = component.isState(stateAppendage)
|
|
138
|
+
|
|
139
|
+
if (!isStateValid) {
|
|
140
|
+
throw new Error(`State for component ${componentName} is invalid`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const componentState = isFormState(stateAppendage)
|
|
144
|
+
? Object.fromEntries(
|
|
145
|
+
Object.entries(stateAppendage).map(([key, value]) => [
|
|
146
|
+
`${componentName}__${key}`,
|
|
147
|
+
value
|
|
148
|
+
])
|
|
149
|
+
)
|
|
150
|
+
: { [componentName]: stateAppendage }
|
|
151
|
+
|
|
152
|
+
// Save the external component state immediately
|
|
153
|
+
const updatedState = await page.mergeState(request, state, componentState)
|
|
154
|
+
|
|
155
|
+
// Merge the stashed payload into the local state
|
|
156
|
+
const payload = request.yar.flash(EXTERNAL_STATE_PAYLOAD)
|
|
157
|
+
const stashedPayload = Array.isArray(payload) ? {} : (payload as FormPayload)
|
|
158
|
+
|
|
159
|
+
return { ...stashedPayload, ...updatedState }
|
|
160
|
+
}
|
|
161
|
+
|
|
88
162
|
export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
89
163
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong
|
|
90
164
|
const prefix = server.realm.modifiers.route.prefix ?? ''
|
|
91
165
|
|
|
92
|
-
const {
|
|
166
|
+
const {
|
|
167
|
+
services = defaultServices,
|
|
168
|
+
controllers,
|
|
169
|
+
ordnanceSurveyApiKey
|
|
170
|
+
} = options
|
|
93
171
|
|
|
94
172
|
const { formsService } = services
|
|
95
173
|
|
|
@@ -156,7 +234,7 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
|
156
234
|
// Construct the form model
|
|
157
235
|
const model = new FormModel(
|
|
158
236
|
definition,
|
|
159
|
-
{ basePath, versionNumber },
|
|
237
|
+
{ basePath, versionNumber, ordnanceSurveyApiKey },
|
|
160
238
|
services,
|
|
161
239
|
controllers
|
|
162
240
|
)
|
|
@@ -166,11 +244,6 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
|
166
244
|
server.app.models.set(key, item)
|
|
167
245
|
}
|
|
168
246
|
|
|
169
|
-
// Call the onRequest callback if it has been supplied
|
|
170
|
-
if (onRequest) {
|
|
171
|
-
onRequest(request, params, item.model.def, metadata)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
247
|
// Assign the model to the request data
|
|
175
248
|
// for use in the downstream handler
|
|
176
249
|
request.app.model = item.model
|