@defra/forms-engine-plugin 4.0.9 → 4.0.10
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/.server/server/plugins/engine/components/EastingNorthingField.js +30 -10
- package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
- package/.server/server/plugins/engine/components/LatLongField.js +32 -4
- package/.server/server/plugins/engine/components/LatLongField.js.map +1 -1
- package/.server/server/plugins/engine/components/NumberField.d.ts +18 -0
- package/.server/server/plugins/engine/components/NumberField.js +103 -2
- package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
- package/package.json +2 -2
- package/src/server/plugins/engine/components/EastingNorthingField.test.ts +15 -1
- package/src/server/plugins/engine/components/EastingNorthingField.ts +38 -10
- package/src/server/plugins/engine/components/LatLongField.test.ts +173 -16
- package/src/server/plugins/engine/components/LatLongField.ts +41 -4
- package/src/server/plugins/engine/components/NumberField.test.ts +504 -12
- package/src/server/plugins/engine/components/NumberField.ts +133 -4
|
@@ -28,10 +28,22 @@ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
|
|
|
28
28
|
|
|
29
29
|
// British National Grid coordinate limits
|
|
30
30
|
const DEFAULT_EASTING_MIN = 0
|
|
31
|
-
const DEFAULT_EASTING_MAX =
|
|
31
|
+
const DEFAULT_EASTING_MAX = 700000
|
|
32
32
|
const DEFAULT_NORTHING_MIN = 0
|
|
33
33
|
const DEFAULT_NORTHING_MAX = 1300000
|
|
34
34
|
|
|
35
|
+
// Easting length constraints (integer values only, no decimals)
|
|
36
|
+
// Min: 1 char for values like "0" or single digit values
|
|
37
|
+
// Max: 6 chars for values up to 700000 (British National Grid easting limit)
|
|
38
|
+
const EASTING_MIN_LENGTH = 1
|
|
39
|
+
const EASTING_MAX_LENGTH = 6
|
|
40
|
+
|
|
41
|
+
// Northing length constraints (integer values only, no decimals)
|
|
42
|
+
// Min: 1 char for values like "0" or single digit values
|
|
43
|
+
// Max: 7 chars for values up to 1300000 (British National Grid northing limit)
|
|
44
|
+
const NORTHING_MIN_LENGTH = 1
|
|
45
|
+
const NORTHING_MAX_LENGTH = 7
|
|
46
|
+
|
|
35
47
|
export class EastingNorthingField extends FormComponent {
|
|
36
48
|
declare options: EastingNorthingFieldComponent['options']
|
|
37
49
|
declare formSchema: ObjectSchema<FormPayload>
|
|
@@ -59,9 +71,11 @@ export class EastingNorthingField extends FormComponent {
|
|
|
59
71
|
'number.base': messageTemplate.objectMissing,
|
|
60
72
|
'number.min': `{{#label}} for ${this.title} must be between {{#limit}} and ${eastingMax}`,
|
|
61
73
|
'number.max': `{{#label}} for ${this.title} must be between ${eastingMin} and {{#limit}}`,
|
|
62
|
-
'number.precision': `{{#label}} for ${this.title} must be between 1 and
|
|
63
|
-
'number.integer': `{{#label}} for ${this.title} must be between 1 and
|
|
64
|
-
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and
|
|
74
|
+
'number.precision': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
|
|
75
|
+
'number.integer': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
|
|
76
|
+
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
|
|
77
|
+
'number.minLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
|
|
78
|
+
'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`
|
|
65
79
|
})
|
|
66
80
|
|
|
67
81
|
const northingValidationMessages: LanguageMessages =
|
|
@@ -72,7 +86,9 @@ export class EastingNorthingField extends FormComponent {
|
|
|
72
86
|
'number.max': `{{#label}} for ${this.title} must be between ${northingMin} and {{#limit}}`,
|
|
73
87
|
'number.precision': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
|
|
74
88
|
'number.integer': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
|
|
75
|
-
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits
|
|
89
|
+
'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
|
|
90
|
+
'number.minLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
|
|
91
|
+
'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`
|
|
76
92
|
})
|
|
77
93
|
|
|
78
94
|
this.collection = new ComponentCollection(
|
|
@@ -81,7 +97,13 @@ export class EastingNorthingField extends FormComponent {
|
|
|
81
97
|
type: ComponentType.NumberField,
|
|
82
98
|
name: `${name}__easting`,
|
|
83
99
|
title: 'Easting',
|
|
84
|
-
schema: {
|
|
100
|
+
schema: {
|
|
101
|
+
min: eastingMin,
|
|
102
|
+
max: eastingMax,
|
|
103
|
+
precision: 0,
|
|
104
|
+
minLength: EASTING_MIN_LENGTH,
|
|
105
|
+
maxLength: EASTING_MAX_LENGTH
|
|
106
|
+
},
|
|
85
107
|
options: {
|
|
86
108
|
required: isRequired,
|
|
87
109
|
optionalText: true,
|
|
@@ -93,7 +115,13 @@ export class EastingNorthingField extends FormComponent {
|
|
|
93
115
|
type: ComponentType.NumberField,
|
|
94
116
|
name: `${name}__northing`,
|
|
95
117
|
title: 'Northing',
|
|
96
|
-
schema: {
|
|
118
|
+
schema: {
|
|
119
|
+
min: northingMin,
|
|
120
|
+
max: northingMax,
|
|
121
|
+
precision: 0,
|
|
122
|
+
minLength: NORTHING_MIN_LENGTH,
|
|
123
|
+
maxLength: NORTHING_MAX_LENGTH
|
|
124
|
+
},
|
|
97
125
|
options: {
|
|
98
126
|
required: isRequired,
|
|
99
127
|
optionalText: true,
|
|
@@ -179,7 +207,7 @@ export class EastingNorthingField extends FormComponent {
|
|
|
179
207
|
{
|
|
180
208
|
type: 'eastingFormat',
|
|
181
209
|
template:
|
|
182
|
-
'Easting for [short description] must be between 1 and
|
|
210
|
+
'Easting for [short description] must be between 1 and 6 digits'
|
|
183
211
|
},
|
|
184
212
|
{
|
|
185
213
|
type: 'northingFormat',
|
|
@@ -190,11 +218,11 @@ export class EastingNorthingField extends FormComponent {
|
|
|
190
218
|
advancedSettingsErrors: [
|
|
191
219
|
{
|
|
192
220
|
type: 'eastingMin',
|
|
193
|
-
template: `Easting for [short description] must be between
|
|
221
|
+
template: `Easting for [short description] must be between 0 and 700000`
|
|
194
222
|
},
|
|
195
223
|
{
|
|
196
224
|
type: 'eastingMax',
|
|
197
|
-
template: `Easting for [short description] must be between
|
|
225
|
+
template: `Easting for [short description] must be between 0 and 700000`
|
|
198
226
|
},
|
|
199
227
|
{
|
|
200
228
|
type: 'northingMin',
|
|
@@ -149,8 +149,8 @@ describe('LatLongField', () => {
|
|
|
149
149
|
|
|
150
150
|
const result2 = collection.validate(
|
|
151
151
|
getFormData({
|
|
152
|
-
latitude: '49',
|
|
153
|
-
longitude: '-9'
|
|
152
|
+
latitude: '49.1',
|
|
153
|
+
longitude: '-8.9'
|
|
154
154
|
})
|
|
155
155
|
)
|
|
156
156
|
|
|
@@ -381,13 +381,7 @@ describe('LatLongField', () => {
|
|
|
381
381
|
describe.each([
|
|
382
382
|
{
|
|
383
383
|
description: 'Trim empty spaces',
|
|
384
|
-
component:
|
|
385
|
-
title: 'Example lat long',
|
|
386
|
-
name: 'myComponent',
|
|
387
|
-
type: ComponentType.LatLongField,
|
|
388
|
-
options: {},
|
|
389
|
-
schema: {}
|
|
390
|
-
} satisfies LatLongFieldComponent,
|
|
384
|
+
component: createLatLongComponent(),
|
|
391
385
|
assertions: [
|
|
392
386
|
{
|
|
393
387
|
input: getFormData({
|
|
@@ -571,15 +565,162 @@ describe('LatLongField', () => {
|
|
|
571
565
|
}
|
|
572
566
|
]
|
|
573
567
|
},
|
|
568
|
+
{
|
|
569
|
+
description: 'Minimum precision validation',
|
|
570
|
+
component: createLatLongComponent(),
|
|
571
|
+
assertions: [
|
|
572
|
+
{
|
|
573
|
+
input: getFormData({
|
|
574
|
+
latitude: '52',
|
|
575
|
+
longitude: '-1'
|
|
576
|
+
}),
|
|
577
|
+
output: {
|
|
578
|
+
value: getFormData({
|
|
579
|
+
latitude: 52,
|
|
580
|
+
longitude: -1
|
|
581
|
+
}),
|
|
582
|
+
errors: [
|
|
583
|
+
expect.objectContaining({
|
|
584
|
+
text: 'Latitude must have at least 1 decimal place'
|
|
585
|
+
}),
|
|
586
|
+
expect.objectContaining({
|
|
587
|
+
text: 'Longitude must have at least 1 decimal place'
|
|
588
|
+
})
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
input: getFormData({
|
|
594
|
+
latitude: '52.1',
|
|
595
|
+
longitude: '-1.5'
|
|
596
|
+
}),
|
|
597
|
+
output: {
|
|
598
|
+
value: getFormData({
|
|
599
|
+
latitude: 52.1,
|
|
600
|
+
longitude: -1.5
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
input: getFormData({
|
|
606
|
+
latitude: '52.123456',
|
|
607
|
+
longitude: '-1.123456'
|
|
608
|
+
}),
|
|
609
|
+
output: {
|
|
610
|
+
value: getFormData({
|
|
611
|
+
latitude: 52.123456,
|
|
612
|
+
longitude: -1.123456
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
description: 'Length and precision validation',
|
|
620
|
+
component: createLatLongComponent(),
|
|
621
|
+
assertions: [
|
|
622
|
+
// Latitude too short
|
|
623
|
+
{
|
|
624
|
+
input: getFormData({
|
|
625
|
+
latitude: '52',
|
|
626
|
+
longitude: '-1.5'
|
|
627
|
+
}),
|
|
628
|
+
output: {
|
|
629
|
+
value: getFormData({
|
|
630
|
+
latitude: 52,
|
|
631
|
+
longitude: -1.5
|
|
632
|
+
}),
|
|
633
|
+
errors: [
|
|
634
|
+
expect.objectContaining({
|
|
635
|
+
text: 'Latitude must have at least 1 decimal place'
|
|
636
|
+
})
|
|
637
|
+
]
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
// Latitude too long
|
|
641
|
+
{
|
|
642
|
+
input: getFormData({
|
|
643
|
+
latitude: '52.12345678',
|
|
644
|
+
longitude: '-1.5'
|
|
645
|
+
}),
|
|
646
|
+
output: {
|
|
647
|
+
value: getFormData({
|
|
648
|
+
latitude: 52.12345678,
|
|
649
|
+
longitude: -1.5
|
|
650
|
+
}),
|
|
651
|
+
errors: [
|
|
652
|
+
expect.objectContaining({
|
|
653
|
+
text: 'Latitude must have no more than 7 decimal places'
|
|
654
|
+
})
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
// Longitude too short
|
|
659
|
+
{
|
|
660
|
+
input: getFormData({
|
|
661
|
+
latitude: '52.1',
|
|
662
|
+
longitude: '-1'
|
|
663
|
+
}),
|
|
664
|
+
output: {
|
|
665
|
+
value: getFormData({
|
|
666
|
+
latitude: 52.1,
|
|
667
|
+
longitude: -1
|
|
668
|
+
}),
|
|
669
|
+
errors: [
|
|
670
|
+
expect.objectContaining({
|
|
671
|
+
text: 'Longitude must have at least 1 decimal place'
|
|
672
|
+
})
|
|
673
|
+
]
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
// Longitude too long
|
|
677
|
+
{
|
|
678
|
+
input: getFormData({
|
|
679
|
+
latitude: '52.1',
|
|
680
|
+
longitude: '-1.12345678'
|
|
681
|
+
}),
|
|
682
|
+
output: {
|
|
683
|
+
value: getFormData({
|
|
684
|
+
latitude: 52.1,
|
|
685
|
+
longitude: -1.12345678
|
|
686
|
+
}),
|
|
687
|
+
errors: [
|
|
688
|
+
expect.objectContaining({
|
|
689
|
+
text: 'Longitude must have no more than 7 decimal places'
|
|
690
|
+
})
|
|
691
|
+
]
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
// Valid values
|
|
695
|
+
{
|
|
696
|
+
input: getFormData({
|
|
697
|
+
latitude: '52.1',
|
|
698
|
+
longitude: '-1.5'
|
|
699
|
+
}),
|
|
700
|
+
output: {
|
|
701
|
+
value: getFormData({
|
|
702
|
+
latitude: 52.1,
|
|
703
|
+
longitude: -1.5
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
input: getFormData({
|
|
709
|
+
latitude: '52.1234',
|
|
710
|
+
longitude: '-1.123'
|
|
711
|
+
}),
|
|
712
|
+
output: {
|
|
713
|
+
value: getFormData({
|
|
714
|
+
latitude: 52.1234,
|
|
715
|
+
longitude: -1.123
|
|
716
|
+
})
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
]
|
|
720
|
+
},
|
|
574
721
|
{
|
|
575
722
|
description: 'Invalid format',
|
|
576
|
-
component:
|
|
577
|
-
title: 'Example lat long',
|
|
578
|
-
name: 'myComponent',
|
|
579
|
-
type: ComponentType.LatLongField,
|
|
580
|
-
options: {},
|
|
581
|
-
schema: {}
|
|
582
|
-
} satisfies LatLongFieldComponent,
|
|
723
|
+
component: createLatLongComponent(),
|
|
583
724
|
assertions: [
|
|
584
725
|
{
|
|
585
726
|
input: getFormData({
|
|
@@ -665,6 +806,22 @@ describe('LatLongField', () => {
|
|
|
665
806
|
})
|
|
666
807
|
})
|
|
667
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Factory function to create a default LatLongField component with optional overrides
|
|
811
|
+
*/
|
|
812
|
+
function createLatLongComponent(
|
|
813
|
+
overrides: Partial<LatLongFieldComponent> = {}
|
|
814
|
+
): LatLongFieldComponent {
|
|
815
|
+
return {
|
|
816
|
+
title: 'Example lat long',
|
|
817
|
+
name: 'myComponent',
|
|
818
|
+
type: ComponentType.LatLongField,
|
|
819
|
+
options: {},
|
|
820
|
+
schema: {},
|
|
821
|
+
...overrides
|
|
822
|
+
} satisfies LatLongFieldComponent
|
|
823
|
+
}
|
|
824
|
+
|
|
668
825
|
function getFormData(
|
|
669
826
|
value:
|
|
670
827
|
| { latitude?: string | number; longitude?: string | number }
|
|
@@ -23,6 +23,23 @@ import {
|
|
|
23
23
|
} from '~/src/server/plugins/engine/types.js'
|
|
24
24
|
import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
|
|
25
25
|
|
|
26
|
+
// Precision constants
|
|
27
|
+
// UK latitude/longitude requires high precision for accurate location (within ~11mm)
|
|
28
|
+
const DECIMAL_PRECISION = 7 // 7 decimal places
|
|
29
|
+
const MIN_DECIMAL_PLACES = 1 // At least 1 decimal place required
|
|
30
|
+
|
|
31
|
+
// Latitude length constraints
|
|
32
|
+
// Min: 3 chars for values like "52.1" (2 digits + decimal + 1 decimal place)
|
|
33
|
+
// Max: 10 chars for values like "59.1234567" (2 digits + decimal + 7 decimal places)
|
|
34
|
+
const LATITUDE_MIN_LENGTH = 3
|
|
35
|
+
const LATITUDE_MAX_LENGTH = 10
|
|
36
|
+
|
|
37
|
+
// Longitude length constraints
|
|
38
|
+
// Min: 2 chars for values like "-1" or single digit with decimal (needs min decimal places)
|
|
39
|
+
// Max: 10 chars for values like "-1.1234567" (minus + 1 digit + decimal + 7 decimal places)
|
|
40
|
+
const LONGITUDE_MIN_LENGTH = 2
|
|
41
|
+
const LONGITUDE_MAX_LENGTH = 10
|
|
42
|
+
|
|
26
43
|
export class LatLongField extends FormComponent {
|
|
27
44
|
declare options: LatLongFieldComponent['options']
|
|
28
45
|
declare formSchema: ObjectSchema<FormPayload>
|
|
@@ -51,6 +68,8 @@ export class LatLongField extends FormComponent {
|
|
|
51
68
|
'number.base': messageTemplate.objectMissing,
|
|
52
69
|
'number.precision':
|
|
53
70
|
'{{#label}} must have no more than 7 decimal places',
|
|
71
|
+
'number.minPrecision':
|
|
72
|
+
'{{#label}} must have at least {{#minPrecision}} decimal place',
|
|
54
73
|
'number.unsafe': '{{#label}} must be a valid number'
|
|
55
74
|
})
|
|
56
75
|
|
|
@@ -58,14 +77,18 @@ export class LatLongField extends FormComponent {
|
|
|
58
77
|
...customValidationMessages,
|
|
59
78
|
'number.base': `Enter a valid latitude for ${this.title} like 51.519450`,
|
|
60
79
|
'number.min': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
|
|
61
|
-
'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}
|
|
80
|
+
'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
|
|
81
|
+
'number.minLength': `Latitude for ${this.title} must be between 3 and 10 characters`,
|
|
82
|
+
'number.maxLength': `Latitude for ${this.title} must be between 3 and 10 characters`
|
|
62
83
|
})
|
|
63
84
|
|
|
64
85
|
const longitudeMessages: LanguageMessages = convertToLanguageMessages({
|
|
65
86
|
...customValidationMessages,
|
|
66
87
|
'number.base': `Enter a valid longitude for ${this.title} like -0.127758`,
|
|
67
88
|
'number.min': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
|
|
68
|
-
'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}
|
|
89
|
+
'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
|
|
90
|
+
'number.minLength': `Longitude for ${this.title} must be between 2 and 10 characters`,
|
|
91
|
+
'number.maxLength': `Longitude for ${this.title} must be between 2 and 10 characters`
|
|
69
92
|
})
|
|
70
93
|
|
|
71
94
|
this.collection = new ComponentCollection(
|
|
@@ -74,7 +97,14 @@ export class LatLongField extends FormComponent {
|
|
|
74
97
|
type: ComponentType.NumberField,
|
|
75
98
|
name: `${name}__latitude`,
|
|
76
99
|
title: 'Latitude',
|
|
77
|
-
schema: {
|
|
100
|
+
schema: {
|
|
101
|
+
min: latitudeMin,
|
|
102
|
+
max: latitudeMax,
|
|
103
|
+
precision: DECIMAL_PRECISION,
|
|
104
|
+
minPrecision: MIN_DECIMAL_PLACES,
|
|
105
|
+
minLength: LATITUDE_MIN_LENGTH,
|
|
106
|
+
maxLength: LATITUDE_MAX_LENGTH
|
|
107
|
+
},
|
|
78
108
|
options: {
|
|
79
109
|
required: isRequired,
|
|
80
110
|
optionalText: true,
|
|
@@ -87,7 +117,14 @@ export class LatLongField extends FormComponent {
|
|
|
87
117
|
type: ComponentType.NumberField,
|
|
88
118
|
name: `${name}__longitude`,
|
|
89
119
|
title: 'Longitude',
|
|
90
|
-
schema: {
|
|
120
|
+
schema: {
|
|
121
|
+
min: longitudeMin,
|
|
122
|
+
max: longitudeMax,
|
|
123
|
+
precision: DECIMAL_PRECISION,
|
|
124
|
+
minPrecision: MIN_DECIMAL_PLACES,
|
|
125
|
+
minLength: LONGITUDE_MIN_LENGTH,
|
|
126
|
+
maxLength: LONGITUDE_MAX_LENGTH
|
|
127
|
+
},
|
|
91
128
|
options: {
|
|
92
129
|
required: isRequired,
|
|
93
130
|
optionalText: true,
|