@defra/forms-engine-plugin 4.0.0 → 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.
Files changed (108) hide show
  1. package/.public/stylesheets/application.min.css +3 -3
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/application.scss +14 -0
  4. package/.server/config/index.d.ts +1 -0
  5. package/.server/config/index.js +7 -0
  6. package/.server/config/index.js.map +1 -1
  7. package/.server/index.js +6 -2
  8. package/.server/index.js.map +1 -1
  9. package/.server/server/constants.d.ts +2 -0
  10. package/.server/server/constants.js +2 -0
  11. package/.server/server/constants.js.map +1 -1
  12. package/.server/server/forms/components.json +7 -0
  13. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
  14. package/.server/server/plugins/engine/components/UkAddressField.d.ts +15 -9
  15. package/.server/server/plugins/engine/components/UkAddressField.js +67 -6
  16. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  17. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  18. package/.server/server/plugins/engine/configureEnginePlugin.js +6 -3
  19. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  20. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
  21. package/.server/server/plugins/engine/models/FormModel.js +3 -1
  22. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  23. package/.server/server/plugins/engine/options.js +2 -1
  24. package/.server/server/plugins/engine/options.js.map +1 -1
  25. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -0
  26. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +46 -3
  27. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  28. package/.server/server/plugins/engine/plugin.js +13 -1
  29. package/.server/server/plugins/engine/plugin.js.map +1 -1
  30. package/.server/server/plugins/engine/routes/index.js +41 -3
  31. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  32. package/.server/server/plugins/engine/types.d.ts +19 -1
  33. package/.server/server/plugins/engine/types.js.map +1 -1
  34. package/.server/server/plugins/engine/validationHelpers.d.ts +15 -0
  35. package/.server/server/plugins/engine/validationHelpers.js +29 -0
  36. package/.server/server/plugins/engine/validationHelpers.js.map +1 -0
  37. package/.server/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  38. package/.server/server/plugins/engine/vision.js +3 -1
  39. package/.server/server/plugins/engine/vision.js.map +1 -1
  40. package/.server/server/plugins/postcode-lookup/index.d.ts +8 -0
  41. package/.server/server/plugins/postcode-lookup/index.js +21 -0
  42. package/.server/server/plugins/postcode-lookup/index.js.map +1 -0
  43. package/.server/server/plugins/postcode-lookup/models/index.d.ts +255 -0
  44. package/.server/server/plugins/postcode-lookup/models/index.js +517 -0
  45. package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -0
  46. package/.server/server/plugins/postcode-lookup/routes/index.d.ts +19 -0
  47. package/.server/server/plugins/postcode-lookup/routes/index.js +267 -0
  48. package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -0
  49. package/.server/server/plugins/postcode-lookup/service.d.ts +26 -0
  50. package/.server/server/plugins/postcode-lookup/service.js +148 -0
  51. package/.server/server/plugins/postcode-lookup/service.js.map +1 -0
  52. package/.server/server/plugins/postcode-lookup/service.test.js +144 -0
  53. package/.server/server/plugins/postcode-lookup/service.test.js.map +1 -0
  54. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.d.ts +282 -0
  55. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js +370 -0
  56. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js.map +1 -0
  57. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.d.ts +131 -0
  58. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js +195 -0
  59. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js.map +1 -0
  60. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.d.ts +51 -0
  61. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js +52 -0
  62. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js.map +1 -0
  63. package/.server/server/plugins/postcode-lookup/types.d.ts +204 -0
  64. package/.server/server/plugins/postcode-lookup/types.js +144 -0
  65. package/.server/server/plugins/postcode-lookup/types.js.map +1 -0
  66. package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  67. package/.server/server/routes/types.d.ts +6 -1
  68. package/.server/server/routes/types.js +6 -0
  69. package/.server/server/routes/types.js.map +1 -1
  70. package/.server/server/schemas/index.js +1 -1
  71. package/.server/server/schemas/index.js.map +1 -1
  72. package/.server/server/types.d.ts +1 -0
  73. package/.server/server/types.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/client/stylesheets/application.scss +14 -0
  76. package/src/config/index.ts +9 -1
  77. package/src/index.ts +5 -4
  78. package/src/server/constants.js +2 -0
  79. package/src/server/forms/components.json +7 -0
  80. package/src/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
  81. package/src/server/plugins/engine/components/UkAddressField.test.ts +50 -27
  82. package/src/server/plugins/engine/components/UkAddressField.ts +91 -8
  83. package/src/server/plugins/engine/configureEnginePlugin.ts +5 -3
  84. package/src/server/plugins/engine/models/FormModel.ts +10 -2
  85. package/src/server/plugins/engine/options.js +2 -1
  86. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1 -0
  87. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +69 -1
  88. package/src/server/plugins/engine/plugin.ts +13 -1
  89. package/src/server/plugins/engine/routes/index.test.ts +1 -0
  90. package/src/server/plugins/engine/routes/index.ts +71 -3
  91. package/src/server/plugins/engine/types.ts +21 -1
  92. package/src/server/plugins/engine/validationHelpers.ts +48 -0
  93. package/src/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  94. package/src/server/plugins/engine/vision.ts +6 -0
  95. package/src/server/plugins/postcode-lookup/index.js +21 -0
  96. package/src/server/plugins/postcode-lookup/models/index.js +549 -0
  97. package/src/server/plugins/postcode-lookup/routes/index.js +258 -0
  98. package/src/server/plugins/postcode-lookup/service.js +188 -0
  99. package/src/server/plugins/postcode-lookup/service.test.js +177 -0
  100. package/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js +382 -0
  101. package/src/server/plugins/postcode-lookup/test/__stubs__/query.js +200 -0
  102. package/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js +53 -0
  103. package/src/server/plugins/postcode-lookup/types.js +143 -0
  104. package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  105. package/src/server/postcode-lookup.test.ts +64 -0
  106. package/src/server/routes/types.ts +7 -1
  107. package/src/server/schemas/index.ts +5 -7
  108. package/src/server/types.ts +1 -0
@@ -6,7 +6,15 @@ import {
6
6
  } from '@hapi/hapi'
7
7
  import { isEqual } from 'date-fns'
8
8
 
9
- import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
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,10 @@ 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,
26
37
  type OnRequestCallback,
27
38
  type PluginOptions
28
39
  } from '~/src/server/plugins/engine/types.js'
@@ -66,6 +77,8 @@ export async function redirectOrMakeHandler(
66
77
  })
67
78
  }
68
79
 
80
+ state = await importExternalComponentState(request, page, state)
81
+
69
82
  const flash = cacheService.getFlash(request)
70
83
  const context = model.getFormContext(request, state, flash?.errors)
71
84
  const relevantPath = page.getRelevantPath(request, context)
@@ -95,11 +108,66 @@ export async function redirectOrMakeHandler(
95
108
  return proceed(request, h, page.getHref(relevantPath))
96
109
  }
97
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
+
98
162
  export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
99
163
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong
100
164
  const prefix = server.realm.modifiers.route.prefix ?? ''
101
165
 
102
- const { services = defaultServices, controllers } = options
166
+ const {
167
+ services = defaultServices,
168
+ controllers,
169
+ ordnanceSurveyApiKey
170
+ } = options
103
171
 
104
172
  const { formsService } = services
105
173
 
@@ -166,7 +234,7 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
166
234
  // Construct the form model
167
235
  const model = new FormModel(
168
236
  definition,
169
- { basePath, versionNumber },
237
+ { basePath, versionNumber, ordnanceSurveyApiKey },
170
238
  services,
171
239
  controllers
172
240
  )
@@ -4,7 +4,8 @@ import {
4
4
  type FormVersionMetadata,
5
5
  type Item,
6
6
  type List,
7
- type Page
7
+ type Page,
8
+ type UkAddressFieldComponent
8
9
  } from '@defra/forms-model'
9
10
  import {
10
11
  type PluginProperties,
@@ -28,6 +29,7 @@ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
28
29
  import { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'
29
30
  import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
30
31
  import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
32
+ import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/index.js'
31
33
  import {
32
34
  type FileStatus,
33
35
  type FormAdapterSubmissionSchemaVersion,
@@ -378,6 +380,23 @@ export type SaveAndExitHandler = (
378
380
  context: FormContext
379
381
  ) => ResponseObject
380
382
 
383
+ export interface ExternalArgs {
384
+ component: ComponentDef
385
+ controller: QuestionPageController
386
+ sourceUrl: string
387
+ actionArgs: Record<string, string>
388
+ }
389
+
390
+ export interface PostcodeLookupExternalArgs extends ExternalArgs {
391
+ component: UkAddressFieldComponent
392
+ actionArgs: { step: string }
393
+ }
394
+
395
+ export interface ExternalStateAppendage {
396
+ component: string
397
+ data: FormStateValue | FormState
398
+ }
399
+
381
400
  export interface PluginOptions {
382
401
  model?: FormModel
383
402
  services?: Services
@@ -395,6 +414,7 @@ export interface PluginOptions {
395
414
  preparePageEventRequestOptions?: PreparePageEventRequestOptions
396
415
  onRequest?: OnRequestCallback
397
416
  baseUrl: string // base URL of the application, protocol and hostname e.g. "https://myapp.com"
417
+ ordnanceSurveyApiKey?: string
398
418
  }
399
419
 
400
420
  export interface FormAdapterSubmissionMessageMeta {
@@ -0,0 +1,48 @@
1
+ import { type ResponseObject } from '@hapi/hapi'
2
+
3
+ import * as Components from '~/src/server/plugins/engine/components/index.js'
4
+ import {
5
+ type FormRequestPayload,
6
+ type FormResponseToolkit
7
+ } from '~/src/server/plugins/engine/types/index.js'
8
+ import { type ExternalArgs } from '~/src/server/plugins/engine/types.js'
9
+
10
+ // Type guard for ExternalComponent
11
+ export function isExternalComponent(
12
+ component: unknown
13
+ ): component is ExternalComponent {
14
+ return typeof (component as ExternalComponent).dispatcher === 'function'
15
+ }
16
+
17
+ // External components are guaranteed to have a dispatcher method
18
+ export interface ExternalComponent {
19
+ dispatcher(
20
+ request: FormRequestPayload,
21
+ h: FormResponseToolkit,
22
+ args: ExternalArgs
23
+ ): ResponseObject
24
+ }
25
+
26
+ /**
27
+ * Returns internal and external components from a componentMap, regardless of error state.
28
+ * @returns An object containing internalComponents and externalComponents arrays
29
+ */
30
+ export function getComponentsByType(): {
31
+ internalComponents: Map<string, unknown>
32
+ externalComponents: Map<string, ExternalComponent>
33
+ } {
34
+ const internalComponents = new Map<string, unknown>()
35
+ const externalComponents = new Map<string, ExternalComponent>()
36
+
37
+ const componentMap = new Map<string, unknown>(Object.entries(Components))
38
+
39
+ for (const [name, component] of componentMap.entries()) {
40
+ if (isExternalComponent(component)) {
41
+ externalComponents.set(name, component)
42
+ } else {
43
+ internalComponents.set(name, component)
44
+ }
45
+ }
46
+
47
+ return { internalComponents, externalComponents }
48
+ }
@@ -1,10 +1,18 @@
1
1
  {% from "partials/components.html" import componentList %}
2
2
  {% from "govuk/components/fieldset/macro.njk" import govukFieldset %}
3
3
  {% from "govuk/components/hint/macro.njk" import govukHint %}
4
+ {% from "govuk/components/button/macro.njk" import govukButton %}
5
+ {% from "govuk/components/inset-text/macro.njk" import govukInsetText %}
4
6
 
5
7
  {% macro UkAddressField(component) %}
6
8
  {% set fieldset = component.model.fieldset %}
7
- {% set addressFieldHtml = componentList(component.model.components) %}
9
+ {% set usePostcodeLookup = component.model.usePostcodeLookup %}
10
+
11
+ {% set addressFieldHtml %}
12
+ <div {{"hidden" if usePostcodeLookup }}>
13
+ {{ componentList(component.model.components) }}
14
+ </div>
15
+ {% endset %}
8
16
 
9
17
  {% if component.model.hint %}
10
18
  {% set addressHintHtml %}
@@ -17,9 +25,45 @@
17
25
  {% set addressFieldHtml = addressHintHtml + addressFieldHtml %}
18
26
  {% endif %}
19
27
 
20
- {{ govukFieldset({
21
- legend: fieldset.legend,
22
- attributes: fieldset.attributes,
23
- html: addressFieldHtml
24
- }) if fieldset else addressFieldHtml }}
28
+ <div id="{{component.model.name}}" class="govuk-form-group{{ ' govuk-form-group--error' if usePostcodeLookup and component.model.errors | length }}">
29
+ {{ govukFieldset({
30
+ legend: fieldset.legend,
31
+ attributes: fieldset.attributes,
32
+ html: addressFieldHtml
33
+ }) if fieldset else addressFieldHtml }}
34
+
35
+ {% if usePostcodeLookup %}
36
+ {% set value = component.model.value %}
37
+
38
+ {% if value %}
39
+ {% set insetHtml %}
40
+ <strong>Selected address:</strong>
41
+ <br><br>
42
+ {{ value }}
43
+ <br><br>
44
+ <p class="govuk-body">
45
+ <button class="govuk-link govuk-button--link govuk-!-margin-right-1" name="action"
46
+ value="external-{{component.model.name}}">Use a different address</button>
47
+ </p>
48
+ {% endset %}
49
+
50
+ {{ govukInsetText({
51
+ html: insetHtml,
52
+ classes: "govuk-!-margin-top-2"
53
+ }) }}
54
+ {% else %}
55
+ <div class="govuk-button-group govuk-!-margin-bottom-0">
56
+ {{ govukButton({
57
+ text: "Find an address",
58
+ attributes: {
59
+ name: "action",
60
+ value: "external-" + component.model.name
61
+ },
62
+ classes: "govuk-button--secondary govuk-!-margin-right-1 govuk-!-margin-bottom-0"
63
+ }) }}
64
+ <p class="govuk-body govuk-!-margin-bottom-0">or <button class="govuk-link govuk-button--link govuk-!-margin-right-1 govuk-!-margin-bottom-0" name="action" value="external-{{component.model.name}}--step:manual">enter address manually</button></p>
65
+ </div>
66
+ {% endif %}
67
+ {% endif %}
68
+ </div>
25
69
  {% endmacro %}
@@ -13,6 +13,7 @@ import {
13
13
  prepareNunjucksEnvironment
14
14
  } from '~/src/server/plugins/engine/index.js'
15
15
  import { type PluginOptions } from '~/src/server/plugins/engine/types.js'
16
+ import { VIEW_PATH as POSTCODE_LOOKUP_VIEW_PATH } from '~/src/server/plugins/postcode-lookup/index.js'
16
17
 
17
18
  export async function registerVision(
18
19
  server: Server,
@@ -24,10 +25,15 @@ export async function registerVision(
24
25
  )
25
26
 
26
27
  const viewPathResolved = join(packageRoot, VIEW_PATH)
28
+ const postcodeLookupPathResolved = join(
29
+ packageRoot,
30
+ POSTCODE_LOOKUP_VIEW_PATH
31
+ )
27
32
 
28
33
  const paths = [
29
34
  ...pluginOptions.nunjucks.paths,
30
35
  viewPathResolved,
36
+ postcodeLookupPathResolved,
31
37
  join(govukFrontendPath, 'dist')
32
38
  ]
33
39
 
@@ -0,0 +1,21 @@
1
+ import { getRoutes } from '~/src/server/plugins/postcode-lookup/routes/index.js'
2
+
3
+ export const VIEW_PATH = 'src/server/plugins/postcode-lookup/views'
4
+
5
+ /**
6
+ * @satisfies {NamedPlugin<PostcodeLookupConfiguration>}
7
+ */
8
+ export const postcodeLookupPlugin = {
9
+ name: '@defra/forms-engine-plugin/postcode-lookup',
10
+ dependencies: ['@hapi/vision'],
11
+ multiple: false,
12
+ register(server, options) {
13
+ // @ts-expect-error - Request typing
14
+ server.route(getRoutes(options))
15
+ }
16
+ }
17
+
18
+ /**
19
+ * @import { NamedPlugin } from '@hapi/hapi'
20
+ * @import { PostcodeLookupConfiguration } from '~/src/server/plugins/postcode-lookup/types.js'
21
+ */