@defra/forms-engine-plugin 4.0.0 → 4.0.2

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 +2 -2
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/shared.scss +15 -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/shared.scss +15 -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
@@ -56,10 +56,26 @@ pages:
56
56
  - name: wZLWPy
57
57
  options:
58
58
  required: true
59
+ usePostcodeLookup: true
59
60
  type: UkAddressField
60
- title: Address
61
+ title: What is your billing address
62
+ shortDescription: Billing address
61
63
  schema: {}
62
- hint: This is a UK address. Users must enter address line 1, town and a postcode
64
+ hint: This is a UK billing address. Users must enter address line 1, town and a postcode
65
+ - name: dfTGhD
66
+ options: {}
67
+ schema: {}
68
+ type: MultilineTextField
69
+ title: Delivery notes
70
+ hint:
71
+ Enter some instructions for the delivery person
72
+ - name: drGHuj
73
+ options:
74
+ required: true
75
+ type: UkAddressField
76
+ title: What is your delivery address
77
+ schema: {}
78
+ hint: This is a UK delivery address. Users must enter address line 1, town and a postcode
63
79
  next:
64
80
  - path: '/do-you-want-your-unicorn-breeder-certificate-sent-to-this-address'
65
81
  section: section
@@ -91,6 +91,7 @@ describe('UkAddressField', () => {
91
91
 
92
92
  expect(field.keys).toEqual([
93
93
  'myComponent',
94
+ 'myComponent__uprn',
94
95
  'myComponent__addressLine1',
95
96
  'myComponent__addressLine2',
96
97
  'myComponent__town',
@@ -194,7 +195,8 @@ describe('UkAddressField', () => {
194
195
  addressLine2: '',
195
196
  town: '',
196
197
  county: '',
197
- postcode: ''
198
+ postcode: '',
199
+ uprn: ''
198
200
  })
199
201
  )
200
202
 
@@ -208,7 +210,8 @@ describe('UkAddressField', () => {
208
210
  addressLine2: 'Knutsford Road',
209
211
  town: 'Warrington',
210
212
  county: 'Cheshire',
211
- postcode: 'WA4 1HT'
213
+ postcode: 'WA4 1HT',
214
+ uprn: ''
212
215
  })
213
216
  )
214
217
 
@@ -218,7 +221,8 @@ describe('UkAddressField', () => {
218
221
  addressLine2: '', // Optional field
219
222
  town: 'Warrington',
220
223
  county: '', // Optional field
221
- postcode: 'WA4 1HT'
224
+ postcode: 'WA4 1HT',
225
+ uprn: ''
222
226
  })
223
227
  )
224
228
 
@@ -233,7 +237,8 @@ describe('UkAddressField', () => {
233
237
  addressLine2: '',
234
238
  town: '',
235
239
  county: '',
236
- postcode: ''
240
+ postcode: '',
241
+ uprn: ''
237
242
  })
238
243
  )
239
244
 
@@ -302,7 +307,7 @@ describe('UkAddressField', () => {
302
307
  'postal-code'
303
308
  ]
304
309
 
305
- ukAddressField.collection.components.forEach((component) => {
310
+ ukAddressField.collection.components.slice(1).forEach((component) => {
306
311
  const addressFieldOptions =
307
312
  component.options as TextFieldComponent['options']
308
313
 
@@ -319,7 +324,8 @@ describe('UkAddressField', () => {
319
324
  addressLine2: 'Knutsford Road',
320
325
  town: 'Warrington',
321
326
  county: 'Cheshire',
322
- postcode: 'WA4 1HT'
327
+ postcode: 'WA4 1HT',
328
+ uprn: '123456789'
323
329
  }
324
330
 
325
331
  it('returns text from state', () => {
@@ -481,7 +487,8 @@ describe('UkAddressField', () => {
481
487
  addressLine2: 'Knutsford Road',
482
488
  town: 'Warrington',
483
489
  county: 'Cheshire',
484
- postcode: 'WA4 1HT'
490
+ postcode: 'WA4 1HT',
491
+ uprn: ''
485
492
  }
486
493
 
487
494
  const addressLine1Invalid =
@@ -514,7 +521,8 @@ describe('UkAddressField', () => {
514
521
  addressLine2: ' Knutsford Road',
515
522
  town: ' Warrington',
516
523
  county: 'Cheshire',
517
- postcode: ' WA4 1HT'
524
+ postcode: ' WA4 1HT',
525
+ uprn: ''
518
526
  }),
519
527
  output: {
520
528
  value: getFormData(address),
@@ -527,7 +535,8 @@ describe('UkAddressField', () => {
527
535
  addressLine2: 'Knutsford Road ',
528
536
  town: 'Warrington ',
529
537
  county: 'Cheshire ',
530
- postcode: 'WA4 1HT '
538
+ postcode: 'WA4 1HT ',
539
+ uprn: ''
531
540
  }),
532
541
  output: {
533
542
  value: getFormData(address),
@@ -540,7 +549,8 @@ describe('UkAddressField', () => {
540
549
  addressLine2: ' Knutsford Road \n\n',
541
550
  town: ' Warrington \n\n',
542
551
  county: ' Cheshire \n\n',
543
- postcode: ' WA4 1HT \n\n'
552
+ postcode: ' WA4 1HT \n\n',
553
+ uprn: ''
544
554
  }),
545
555
  output: {
546
556
  value: getFormData(address),
@@ -564,7 +574,8 @@ describe('UkAddressField', () => {
564
574
  addressLine2: 'Knutsford Road',
565
575
  town: 'Warrington',
566
576
  county: 'Cheshire',
567
- postcode: 'WA4 1HT'
577
+ postcode: 'WA4 1HT',
578
+ uprn: ''
568
579
  }),
569
580
  output: {
570
581
  value: getFormData({
@@ -572,7 +583,8 @@ describe('UkAddressField', () => {
572
583
  addressLine2: 'Knutsford Road',
573
584
  town: 'Warrington',
574
585
  county: 'Cheshire',
575
- postcode: 'WA4 1HT'
586
+ postcode: 'WA4 1HT',
587
+ uprn: ''
576
588
  }),
577
589
  errors: [
578
590
  expect.objectContaining({
@@ -587,7 +599,8 @@ describe('UkAddressField', () => {
587
599
  addressLine2: addressLine2Invalid,
588
600
  town: 'Warrington',
589
601
  county: 'Cheshire',
590
- postcode: 'WA4 1HT'
602
+ postcode: 'WA4 1HT',
603
+ uprn: ''
591
604
  }),
592
605
  output: {
593
606
  value: getFormData({
@@ -595,7 +608,8 @@ describe('UkAddressField', () => {
595
608
  addressLine2: addressLine2Invalid,
596
609
  town: 'Warrington',
597
610
  county: 'Cheshire',
598
- postcode: 'WA4 1HT'
611
+ postcode: 'WA4 1HT',
612
+ uprn: ''
599
613
  }),
600
614
  errors: [
601
615
  expect.objectContaining({
@@ -610,7 +624,8 @@ describe('UkAddressField', () => {
610
624
  addressLine2: 'Knutsford Road',
611
625
  town: townInvalid,
612
626
  county: 'Cheshire',
613
- postcode: 'WA4 1HT'
627
+ postcode: 'WA4 1HT',
628
+ uprn: ''
614
629
  }),
615
630
  output: {
616
631
  value: getFormData({
@@ -618,7 +633,8 @@ describe('UkAddressField', () => {
618
633
  addressLine2: 'Knutsford Road',
619
634
  town: townInvalid,
620
635
  county: 'Cheshire',
621
- postcode: 'WA4 1HT'
636
+ postcode: 'WA4 1HT',
637
+ uprn: ''
622
638
  }),
623
639
  errors: [
624
640
  expect.objectContaining({
@@ -633,7 +649,8 @@ describe('UkAddressField', () => {
633
649
  addressLine2: 'Knutsford Road',
634
650
  town: 'Warrington',
635
651
  county: countyInvalid,
636
- postcode: 'WA4 1HT'
652
+ postcode: 'WA4 1HT',
653
+ uprn: ''
637
654
  }),
638
655
  output: {
639
656
  value: getFormData({
@@ -641,7 +658,8 @@ describe('UkAddressField', () => {
641
658
  addressLine2: 'Knutsford Road',
642
659
  town: 'Warrington',
643
660
  county: countyInvalid,
644
- postcode: 'WA4 1HT'
661
+ postcode: 'WA4 1HT',
662
+ uprn: ''
645
663
  }),
646
664
  errors: [
647
665
  expect.objectContaining({
@@ -656,7 +674,8 @@ describe('UkAddressField', () => {
656
674
  addressLine2: 'Knutsford Road',
657
675
  town: 'Warrington',
658
676
  county: 'Cheshire',
659
- postcode: postcodeInvalid
677
+ postcode: postcodeInvalid,
678
+ uprn: ''
660
679
  }),
661
680
  output: {
662
681
  value: getFormData({
@@ -664,7 +683,8 @@ describe('UkAddressField', () => {
664
683
  addressLine2: 'Knutsford Road',
665
684
  town: 'Warrington',
666
685
  county: 'Cheshire',
667
- postcode: postcodeInvalid
686
+ postcode: postcodeInvalid,
687
+ uprn: ''
668
688
  }),
669
689
  errors: [
670
690
  expect.objectContaining({
@@ -679,7 +699,8 @@ describe('UkAddressField', () => {
679
699
  addressLine2: '',
680
700
  town: '',
681
701
  county: '',
682
- postcode: postcodeInvalid
702
+ postcode: postcodeInvalid,
703
+ uprn: ''
683
704
  }),
684
705
  output: {
685
706
  value: getFormData({
@@ -687,7 +708,8 @@ describe('UkAddressField', () => {
687
708
  addressLine2: '',
688
709
  town: '',
689
710
  county: '',
690
- postcode: postcodeInvalid
711
+ postcode: postcodeInvalid,
712
+ uprn: ''
691
713
  }),
692
714
  errors: [
693
715
  expect.objectContaining({
@@ -761,7 +783,8 @@ function getFormData(address: FormPayload): FormPayload {
761
783
  myComponent__addressLine2: address.addressLine2,
762
784
  myComponent__town: address.town,
763
785
  myComponent__county: address.county,
764
- myComponent__postcode: address.postcode
786
+ myComponent__postcode: address.postcode,
787
+ myComponent__uprn: address.uprn
765
788
  }
766
789
  }
767
790
 
@@ -769,15 +792,15 @@ function getFormData(address: FormPayload): FormPayload {
769
792
  * UK address session state
770
793
  */
771
794
  function getFormState(address: FormPayload): FormState {
772
- const [addressLine1, addressLine2, town, county, postcode] = Object.values(
773
- getFormData(address)
774
- )
795
+ const [addressLine1, addressLine2, town, county, postcode, uprn] =
796
+ Object.values(getFormData(address))
775
797
 
776
798
  return {
777
799
  myComponent__addressLine1: addressLine1 ?? null,
778
800
  myComponent__addressLine2: addressLine2 ?? null,
779
801
  myComponent__town: town ?? null,
780
802
  myComponent__county: county ?? null,
781
- myComponent__postcode: postcode ?? null
803
+ myComponent__postcode: postcode ?? null,
804
+ myComponent__uprn: uprn ?? null
782
805
  }
783
806
  }
@@ -1,5 +1,10 @@
1
- import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'
1
+ import {
2
+ ComponentType,
3
+ type FormComponentsDef,
4
+ type UkAddressFieldComponent
5
+ } from '@defra/forms-model'
2
6
  import { type ObjectSchema } from 'joi'
7
+ import lowerFirst from 'lodash/lowerFirst.js'
3
8
 
4
9
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
5
10
  import {
@@ -8,14 +13,20 @@ import {
8
13
  } from '~/src/server/plugins/engine/components/FormComponent.js'
9
14
  import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
10
15
  import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
16
+ import {
17
+ type FormRequestPayload,
18
+ type FormResponseToolkit
19
+ } from '~/src/server/plugins/engine/types/index.js'
11
20
  import {
12
21
  type ErrorMessageTemplateList,
13
22
  type FormPayload,
14
23
  type FormState,
15
24
  type FormStateValue,
16
25
  type FormSubmissionError,
17
- type FormSubmissionState
26
+ type FormSubmissionState,
27
+ type PostcodeLookupExternalArgs
18
28
  } from '~/src/server/plugins/engine/types.js'
29
+ import { dispatch } from '~/src/server/plugins/postcode-lookup/routes/index.js'
19
30
 
20
31
  export class UkAddressField extends FormComponent {
21
32
  declare options: UkAddressFieldComponent['options']
@@ -23,13 +34,15 @@ export class UkAddressField extends FormComponent {
23
34
  declare stateSchema: ObjectSchema<FormState>
24
35
  declare collection: ComponentCollection
25
36
 
37
+ shortDescription: FormComponentsDef['shortDescription']
38
+
26
39
  constructor(
27
40
  def: UkAddressFieldComponent,
28
41
  props: ConstructorParameters<typeof FormComponent>[1]
29
42
  ) {
30
43
  super(def, props)
31
44
 
32
- const { name, options } = def
45
+ const { name, options, shortDescription } = def
33
46
 
34
47
  const isRequired = options.required !== false
35
48
  const hideOptional = !!options.optionalText
@@ -37,6 +50,16 @@ export class UkAddressField extends FormComponent {
37
50
 
38
51
  this.collection = new ComponentCollection(
39
52
  [
53
+ {
54
+ type: ComponentType.TextField,
55
+ name: `${name}__uprn`,
56
+ title: 'UPRN',
57
+ schema: {},
58
+ options: {
59
+ required: false,
60
+ classes: 'hidden'
61
+ }
62
+ },
40
63
  {
41
64
  type: ComponentType.TextField,
42
65
  name: `${name}__addressLine1`,
@@ -103,6 +126,7 @@ export class UkAddressField extends FormComponent {
103
126
  this.options = options
104
127
  this.formSchema = this.collection.formSchema
105
128
  this.stateSchema = this.collection.stateSchema
129
+ this.shortDescription = shortDescription
106
130
  }
107
131
 
108
132
  getFormValueFromState(state: FormSubmissionState) {
@@ -115,7 +139,9 @@ export class UkAddressField extends FormComponent {
115
139
  return null
116
140
  }
117
141
 
118
- return Object.values(value).filter(Boolean)
142
+ return Object.entries(value)
143
+ .filter(([key, value]) => key !== 'uprn' && Boolean(value))
144
+ .map(([, value]) => value)
119
145
  }
120
146
 
121
147
  getContextValueFromState(state: FormSubmissionState) {
@@ -140,17 +166,34 @@ export class UkAddressField extends FormComponent {
140
166
  getViewErrors(
141
167
  errors?: FormSubmissionError[]
142
168
  ): FormSubmissionError[] | undefined {
143
- return this.getErrors(errors)?.filter(
169
+ const uniqueErrors = this.getErrors(errors)?.filter(
144
170
  (error, index, self) =>
145
171
  index === self.findIndex((err) => err.name === error.name)
146
172
  )
173
+
174
+ // When using postcode lookup, the address fields are hidden
175
+ // so we replace any individual validation messages with a single one
176
+ if (this.shouldUsePostcodeLookup() && uniqueErrors?.length) {
177
+ const { name, shortDescription } = this
178
+
179
+ return [
180
+ {
181
+ name,
182
+ path: [name],
183
+ href: `#${name}`,
184
+ text: `Enter ${lowerFirst(shortDescription)}`
185
+ }
186
+ ]
187
+ }
188
+
189
+ return uniqueErrors
147
190
  }
148
191
 
149
192
  getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
150
193
  const { collection, name, options } = this
151
194
 
152
195
  const viewModel = super.getViewModel(payload, errors)
153
- let { components, fieldset, hint, label } = viewModel
196
+ let { fieldset, hint, label } = viewModel
154
197
 
155
198
  fieldset ??= {
156
199
  legend: {
@@ -173,12 +216,30 @@ export class UkAddressField extends FormComponent {
173
216
  }
174
217
  }
175
218
 
176
- components = collection.getViewModel(payload, errors)
219
+ const components = collection.getViewModel(payload, errors)
220
+
221
+ // Hide UPRN
222
+ const uprn = components.at(0)
223
+
224
+ if (!uprn) {
225
+ throw new Error('No UPRN')
226
+ }
227
+
228
+ uprn.model.formGroup = { classes: 'app-hidden' }
229
+
230
+ // Postcode lookup
231
+ const usePostcodeLookup = this.shouldUsePostcodeLookup()
232
+
233
+ const value = usePostcodeLookup
234
+ ? this.getDisplayStringFromState(payload)
235
+ : undefined
177
236
 
178
237
  return {
179
238
  ...viewModel,
239
+ value,
180
240
  fieldset,
181
- components
241
+ components,
242
+ usePostcodeLookup
182
243
  }
183
244
  }
184
245
 
@@ -193,6 +254,10 @@ export class UkAddressField extends FormComponent {
193
254
  return UkAddressField.getAllPossibleErrors()
194
255
  }
195
256
 
257
+ private shouldUsePostcodeLookup() {
258
+ return !!(this.options.usePostcodeLookup && this.model.ordnanceSurveyApiKey)
259
+ }
260
+
196
261
  /**
197
262
  * Static version of getAllPossibleErrors that doesn't require a component instance.
198
263
  */
@@ -218,9 +283,27 @@ export class UkAddressField extends FormComponent {
218
283
  TextField.isText(value.postcode)
219
284
  )
220
285
  }
286
+
287
+ static dispatcher(
288
+ request: FormRequestPayload,
289
+ h: FormResponseToolkit,
290
+ args: PostcodeLookupExternalArgs
291
+ ) {
292
+ const { controller, component } = args
293
+
294
+ return dispatch(request, h, {
295
+ formName: controller.model.name,
296
+ componentName: component.name,
297
+ componentHint: component.hint,
298
+ componentTitle: component.title || controller.title,
299
+ step: args.actionArgs.step,
300
+ sourceUrl: args.sourceUrl
301
+ })
302
+ }
221
303
  }
222
304
 
223
305
  export interface UkAddressState extends Record<string, string> {
306
+ uprn: string
224
307
  addressLine1: string
225
308
  addressLine2: string
226
309
  town: string
@@ -21,7 +21,8 @@ export const configureEnginePlugin = async (
21
21
  controllers,
22
22
  preparePageEventRequestOptions,
23
23
  onRequest,
24
- saveAndExit
24
+ saveAndExit,
25
+ ordnanceSurveyApiKey
25
26
  }: RouteConfig = {},
26
27
  cache?: CacheService
27
28
  ): Promise<{
@@ -38,7 +39,7 @@ export const configureEnginePlugin = async (
38
39
 
39
40
  model = new FormModel(
40
41
  definition,
41
- { basePath: initialBasePath },
42
+ { basePath: initialBasePath, ordnanceSurveyApiKey },
42
43
  services,
43
44
  controllers
44
45
  )
@@ -63,7 +64,8 @@ export const configureEnginePlugin = async (
63
64
  preparePageEventRequestOptions,
64
65
  onRequest,
65
66
  baseUrl: 'http://localhost:3009', // always runs locally
66
- saveAndExit
67
+ saveAndExit,
68
+ ordnanceSurveyApiKey
67
69
  }
68
70
  }
69
71
  }
@@ -77,6 +77,7 @@ export class FormModel {
77
77
  values: FormDefinition
78
78
  basePath: string
79
79
  versionNumber?: number
80
+ ordnanceSurveyApiKey?: string
80
81
  conditions: Partial<Record<string, ExecutableCondition>>
81
82
  pages: PageControllerClass[]
82
83
  services: Services
@@ -95,7 +96,11 @@ export class FormModel {
95
96
 
96
97
  constructor(
97
98
  def: typeof this.def,
98
- options: { basePath: string; versionNumber?: number },
99
+ options: {
100
+ basePath: string
101
+ versionNumber?: number
102
+ ordnanceSurveyApiKey?: string
103
+ },
99
104
  services: Services = defaultServices,
100
105
  controllers?: Record<string, typeof PageController>
101
106
  ) {
@@ -150,6 +155,7 @@ export class FormModel {
150
155
  this.values = result.value
151
156
  this.basePath = options.basePath
152
157
  this.versionNumber = options.versionNumber
158
+ this.ordnanceSurveyApiKey = options.ordnanceSurveyApiKey
153
159
  this.conditions = {}
154
160
  this.services = services
155
161
  this.controllers = controllers
@@ -551,7 +557,9 @@ function validateFormPayload(
551
557
  // Skip validation GET requests or other actions
552
558
  if (
553
559
  !request.payload ||
554
- (action && ![FormAction.Validate, FormAction.SaveAndExit].includes(action))
560
+ (action &&
561
+ ![FormAction.Validate, FormAction.SaveAndExit].includes(action) &&
562
+ !action.startsWith(FormAction.External))
555
563
  ) {
556
564
  return context
557
565
  }
@@ -25,7 +25,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
25
25
  preparePageEventRequestOptions: Joi.function().optional(),
26
26
  onRequest: Joi.function().optional(),
27
27
  baseUrl: Joi.string().uri().required(),
28
- saveAndExit: Joi.function().optional()
28
+ saveAndExit: Joi.function().optional(),
29
+ ordnanceSurveyApiKey: Joi.string().optional()
29
30
  })
30
31
 
31
32
  /**
@@ -602,6 +602,7 @@ describe('QuestionPageController', () => {
602
602
  addressField__town: 'Town or city',
603
603
  addressField__county: 'Cheshire',
604
604
  addressField__postcode: 'CW1 1AB',
605
+ addressField__uprn: '',
605
606
  radiosField: 'privateLimitedCompany',
606
607
  selectField: 910400000,
607
608
  autocompleteField: 910400044,
@@ -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,