@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.
Files changed (127) 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 +15 -2
  29. package/.server/server/plugins/engine/plugin.js.map +1 -1
  30. package/.server/server/plugins/engine/routes/index.d.ts +2 -2
  31. package/.server/server/plugins/engine/routes/index.js +49 -9
  32. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  33. package/.server/server/plugins/engine/routes/questions.d.ts +4 -4
  34. package/.server/server/plugins/engine/routes/questions.js +10 -10
  35. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  36. package/.server/server/plugins/engine/routes/repeaters/item-delete.d.ts +2 -1
  37. package/.server/server/plugins/engine/routes/repeaters/item-delete.js +31 -27
  38. package/.server/server/plugins/engine/routes/repeaters/item-delete.js.map +1 -1
  39. package/.server/server/plugins/engine/routes/repeaters/summary.d.ts +2 -1
  40. package/.server/server/plugins/engine/routes/repeaters/summary.js +31 -27
  41. package/.server/server/plugins/engine/routes/repeaters/summary.js.map +1 -1
  42. package/.server/server/plugins/engine/types.d.ts +21 -3
  43. package/.server/server/plugins/engine/types.js.map +1 -1
  44. package/.server/server/plugins/engine/validationHelpers.d.ts +15 -0
  45. package/.server/server/plugins/engine/validationHelpers.js +29 -0
  46. package/.server/server/plugins/engine/validationHelpers.js.map +1 -0
  47. package/.server/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  48. package/.server/server/plugins/engine/vision.js +3 -1
  49. package/.server/server/plugins/engine/vision.js.map +1 -1
  50. package/.server/server/plugins/postcode-lookup/index.d.ts +8 -0
  51. package/.server/server/plugins/postcode-lookup/index.js +21 -0
  52. package/.server/server/plugins/postcode-lookup/index.js.map +1 -0
  53. package/.server/server/plugins/postcode-lookup/models/index.d.ts +255 -0
  54. package/.server/server/plugins/postcode-lookup/models/index.js +517 -0
  55. package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -0
  56. package/.server/server/plugins/postcode-lookup/routes/index.d.ts +19 -0
  57. package/.server/server/plugins/postcode-lookup/routes/index.js +267 -0
  58. package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -0
  59. package/.server/server/plugins/postcode-lookup/service.d.ts +26 -0
  60. package/.server/server/plugins/postcode-lookup/service.js +148 -0
  61. package/.server/server/plugins/postcode-lookup/service.js.map +1 -0
  62. package/.server/server/plugins/postcode-lookup/service.test.js +144 -0
  63. package/.server/server/plugins/postcode-lookup/service.test.js.map +1 -0
  64. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.d.ts +282 -0
  65. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js +370 -0
  66. package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js.map +1 -0
  67. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.d.ts +131 -0
  68. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js +195 -0
  69. package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js.map +1 -0
  70. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.d.ts +51 -0
  71. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js +52 -0
  72. package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js.map +1 -0
  73. package/.server/server/plugins/postcode-lookup/types.d.ts +204 -0
  74. package/.server/server/plugins/postcode-lookup/types.js +144 -0
  75. package/.server/server/plugins/postcode-lookup/types.js.map +1 -0
  76. package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  77. package/.server/server/routes/types.d.ts +7 -2
  78. package/.server/server/routes/types.js +6 -0
  79. package/.server/server/routes/types.js.map +1 -1
  80. package/.server/server/schemas/index.js +1 -1
  81. package/.server/server/schemas/index.js.map +1 -1
  82. package/.server/server/types.d.ts +1 -0
  83. package/.server/server/types.js.map +1 -1
  84. package/package.json +2 -2
  85. package/src/client/stylesheets/application.scss +14 -0
  86. package/src/config/index.ts +9 -1
  87. package/src/index.ts +5 -4
  88. package/src/server/constants.js +2 -0
  89. package/src/server/forms/components.json +7 -0
  90. package/src/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
  91. package/src/server/plugins/engine/components/UkAddressField.test.ts +50 -27
  92. package/src/server/plugins/engine/components/UkAddressField.ts +91 -8
  93. package/src/server/plugins/engine/configureEnginePlugin.ts +5 -3
  94. package/src/server/plugins/engine/helpers.test.ts +2 -1
  95. package/src/server/plugins/engine/models/FormModel.ts +10 -2
  96. package/src/server/plugins/engine/options.js +2 -1
  97. package/src/server/plugins/engine/pageControllers/PageController.test.ts +2 -1
  98. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +9 -4
  99. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +69 -1
  100. package/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts +2 -1
  101. package/src/server/plugins/engine/plugin.ts +22 -4
  102. package/src/server/plugins/engine/routes/index.test.ts +317 -0
  103. package/src/server/plugins/engine/routes/index.ts +81 -8
  104. package/src/server/plugins/engine/routes/questions.test.ts +126 -15
  105. package/src/server/plugins/engine/routes/questions.ts +71 -57
  106. package/src/server/plugins/engine/routes/repeaters/item-delete.test.ts +83 -0
  107. package/src/server/plugins/engine/routes/repeaters/item-delete.ts +39 -33
  108. package/src/server/plugins/engine/routes/repeaters/summary.test.ts +75 -0
  109. package/src/server/plugins/engine/routes/repeaters/summary.ts +28 -22
  110. package/src/server/plugins/engine/types.ts +27 -8
  111. package/src/server/plugins/engine/validationHelpers.ts +48 -0
  112. package/src/server/plugins/engine/views/components/ukaddressfield.html +50 -6
  113. package/src/server/plugins/engine/vision.ts +6 -0
  114. package/src/server/plugins/postcode-lookup/index.js +21 -0
  115. package/src/server/plugins/postcode-lookup/models/index.js +549 -0
  116. package/src/server/plugins/postcode-lookup/routes/index.js +258 -0
  117. package/src/server/plugins/postcode-lookup/service.js +188 -0
  118. package/src/server/plugins/postcode-lookup/service.test.js +177 -0
  119. package/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js +382 -0
  120. package/src/server/plugins/postcode-lookup/test/__stubs__/query.js +200 -0
  121. package/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js +53 -0
  122. package/src/server/plugins/postcode-lookup/types.js +143 -0
  123. package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
  124. package/src/server/postcode-lookup.test.ts +64 -0
  125. package/src/server/routes/types.ts +11 -2
  126. package/src/server/schemas/index.ts +5 -7
  127. 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
  }
@@ -76,7 +76,8 @@ describe('Helpers', () => {
76
76
 
77
77
  h = {
78
78
  redirect: jest.fn().mockImplementation(() => response),
79
- view: jest.fn()
79
+ view: jest.fn(),
80
+ continue: Symbol('continue')
80
81
  }
81
82
  })
82
83
 
@@ -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
  /**
@@ -160,7 +160,8 @@ describe('PageController', () => {
160
160
 
161
161
  const h: FormResponseToolkit = {
162
162
  redirect: jest.fn(),
163
- view: jest.fn()
163
+ view: jest.fn(),
164
+ continue: Symbol('continue')
164
165
  }
165
166
 
166
167
  it('returns default route options', () => {
@@ -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,
@@ -805,7 +806,8 @@ describe('QuestionPageController', () => {
805
806
 
806
807
  const h: FormResponseToolkit = {
807
808
  redirect: jest.fn().mockReturnValue(response),
808
- view: jest.fn()
809
+ view: jest.fn(),
810
+ continue: Symbol('continue')
809
811
  }
810
812
 
811
813
  it('returns default route options', () => {
@@ -1373,7 +1375,8 @@ describe('QuestionPageController V2', () => {
1373
1375
 
1374
1376
  const h: FormResponseToolkit = {
1375
1377
  redirect: jest.fn().mockReturnValue(response),
1376
- view: jest.fn()
1378
+ view: jest.fn(),
1379
+ continue: Symbol('continue')
1377
1380
  }
1378
1381
 
1379
1382
  it('returns default route options', () => {
@@ -1532,7 +1535,8 @@ describe('Save and Exit functionality', () => {
1532
1535
 
1533
1536
  const h: FormResponseToolkit = {
1534
1537
  redirect: jest.fn().mockReturnValue(response),
1535
- view: jest.fn()
1538
+ view: jest.fn(),
1539
+ continue: Symbol('continue')
1536
1540
  }
1537
1541
 
1538
1542
  beforeEach(() => {
@@ -1663,7 +1667,8 @@ describe('Save and Exit functionality', () => {
1663
1667
 
1664
1668
  const mockH = {
1665
1669
  redirect: jest.fn().mockReturnValue(mockResponse),
1666
- view: jest.fn()
1670
+ view: jest.fn(),
1671
+ continue: Symbol('continue')
1667
1672
  }
1668
1673
 
1669
1674
  const postHandler = controller1.makePostRouteHandler()