hof 22.11.9 → 22.12.0-unit-of-measure-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +78 -0
- package/components/amount-with-unit-select/amount-with-unit-select.js +107 -0
- package/components/amount-with-unit-select/fields.js +15 -0
- package/components/amount-with-unit-select/hooks.js +168 -0
- package/components/amount-with-unit-select/templates/amount-with-unit-select.html +20 -0
- package/components/amount-with-unit-select/utils.js +191 -0
- package/components/amount-with-unit-select/validation.js +175 -0
- package/components/index.js +1 -0
- package/config/hof-defaults.js +1 -1
- package/controller/controller.js +5 -3
- package/controller/validation/index.js +1 -1
- package/controller/validation/validators.js +0 -1
- package/frontend/template-mixins/mixins/template-mixins.js +55 -5
- package/frontend/template-mixins/partials/forms/grouped-inputs-select.html +13 -0
- package/frontend/template-mixins/partials/forms/grouped-inputs-text.html +37 -0
- package/frontend/themes/gov-uk/styles/_grouped-input.scss +5 -0
- package/frontend/themes/gov-uk/styles/govuk.scss +1 -0
- package/frontend/toolkit/assets/javascript/form-focus.js +4 -0
- package/lib/encryption.js +43 -17
- package/lib/sessions.js +5 -2
- package/lib/settings.js +1 -1
- package/package.json +2 -7
- package/sandbox/apps/sandbox/fields.js +18 -1
- package/sandbox/apps/sandbox/index.js +4 -0
- package/sandbox/apps/sandbox/sections/summary-data-sections.js +7 -1
- package/sandbox/apps/sandbox/translations/en/default.json +278 -0
- package/sandbox/apps/sandbox/translations/src/en/fields.json +10 -0
- package/sandbox/apps/sandbox/translations/src/en/pages.json +3 -0
- package/sandbox/apps/sandbox/translations/src/en/validation.json +12 -0
- package/sandbox/public/css/app.css +10042 -0
- package/sandbox/public/images/icons/icon-caret-left.png +0 -0
- package/sandbox/public/images/icons/icon-complete.png +0 -0
- package/sandbox/public/images/icons/icon-cross-remove-sign.png +0 -0
- package/sandbox/public/js/bundle.js +36724 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const controller = require('../../controller/controller').prototype;
|
|
5
|
+
const utils = require('./utils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates the value is a string consisting of two hyphen-separated values.
|
|
9
|
+
* Can be passed to the list of field validators to run as a custom validator.
|
|
10
|
+
* @param {string} value - The amountWithUnitSelect value to validate (E.G. '1-Litre').
|
|
11
|
+
* @returns {boolean} Returns true if the value is in the expected format, false otherwise.
|
|
12
|
+
*/
|
|
13
|
+
const isTwoHyphenSeparatedValues = value => {
|
|
14
|
+
if (typeof value !== 'string' || value.indexOf('-') === -1) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const selectValue = [value.split('-').pop()];
|
|
18
|
+
return Array.isArray(selectValue) && selectValue.length;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a custom 'equal' validator for a select component.
|
|
23
|
+
* @param {Object[]} options - The select component options.
|
|
24
|
+
* @returns {Object[]} Returns a custom 'equal' validator object for the select component.
|
|
25
|
+
*/
|
|
26
|
+
const createCustomEqualValidator = options => [{
|
|
27
|
+
type: 'equal',
|
|
28
|
+
arguments: _.map(options, opt =>
|
|
29
|
+
typeof opt === 'string' ? opt : opt.value)
|
|
30
|
+
}];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds a validator to a field's validate array, ensuring no duplicates.
|
|
34
|
+
* @param {Object} field - The field to add the validator to.
|
|
35
|
+
* @param {Object|Object[]|string|string[]|function(string): boolean|(function(string): boolean)[]} newValidator -
|
|
36
|
+
* The validator to add.
|
|
37
|
+
*/
|
|
38
|
+
const addValidator = (field, newValidator) => {
|
|
39
|
+
field.validate = _.uniq(field.validate.concat(newValidator));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds the 'groupedFieldsWithOptions' property to the specified field.
|
|
44
|
+
* This property prevents the 'equal' validator being applied to the parent component by default,
|
|
45
|
+
* and enables it to separately be added to the unit child component instead.
|
|
46
|
+
* @param {Object} field - The field to add the property to.
|
|
47
|
+
*/
|
|
48
|
+
const addGroupedFieldsWithOptionsProperty = field => {
|
|
49
|
+
field.groupedFieldsWithOptions = true;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves configurations related to making the amount and/or unit fields optional (E.G. amountOptional, unitOptional).
|
|
54
|
+
* @param {Object[]} parentField - The parent component's field definition and configuration.
|
|
55
|
+
* @param {Object[]} childFields - The child component's field definitions and configurations.
|
|
56
|
+
* @param {Array} validators - The list of validators assigned to the parent component.
|
|
57
|
+
* @param {string} key - The parent component's key.
|
|
58
|
+
*/
|
|
59
|
+
const resolveOptionalFields = (parentField, childFields, validators, key) => {
|
|
60
|
+
// adds existing required validators from parent component to the child components
|
|
61
|
+
// and resolves configurations that determine if the child components should be optional
|
|
62
|
+
validators?.indexOf('required') !== -1 || parentField[key]?.amountOptional !== 'true' &&
|
|
63
|
+
addValidator(childFields[`${key}-amount`], 'required');
|
|
64
|
+
validators?.indexOf('required') !== -1 || parentField[key]?.unitOptional !== 'true' &&
|
|
65
|
+
addValidator(childFields[`${key}-unit`], 'required');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Propagates the child component's (amount and unit) field data and values into the form request to enable validation.
|
|
70
|
+
* @param {Object} formReq - The form's request object.
|
|
71
|
+
* @param {Object[]} fields - The child components' definitions and configurations.
|
|
72
|
+
* @param {string} key - The parent component's key.
|
|
73
|
+
*/
|
|
74
|
+
const propagateChildFieldValidation = (formReq, fields, key) => {
|
|
75
|
+
// adds child component field definitions to the form request
|
|
76
|
+
Object.assign(formReq.options.fields,
|
|
77
|
+
{ 'amountWithUnitSelect-amount': fields[`${key}-amount`] },
|
|
78
|
+
{ 'amountWithUnitSelect-unit': fields[`${key}-unit`] }
|
|
79
|
+
);
|
|
80
|
+
// splits and assigns the component's values to the form request
|
|
81
|
+
const amountWithUnitSelectValues = utils.getAmountWithUnitSelectValues(formReq.values.amountWithUnitSelect);
|
|
82
|
+
Object.assign(formReq.values,
|
|
83
|
+
{ 'amountWithUnitSelect-amount': amountWithUnitSelectValues[0] },
|
|
84
|
+
{ 'amountWithUnitSelect-unit': amountWithUnitSelectValues[1] }
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Moves validators, that are not the 'required' or 'equal' type, from the parent component to
|
|
90
|
+
* the 'amount' child component,
|
|
91
|
+
* ensuring all other validators are applied to the amount field only.
|
|
92
|
+
* @param {Object} formReqFields - The fields in the form's request object (req.form.options.fields).
|
|
93
|
+
* @param {Object[]} fields - The child components' definitions and configurations.
|
|
94
|
+
* @param {string} key - The parent component's key.
|
|
95
|
+
*/
|
|
96
|
+
const moveExcessValidatorToChildComponent = (formReqFields, fields, key) => {
|
|
97
|
+
_.remove(formReqFields?.amountWithUnitSelect?.validate, validator => {
|
|
98
|
+
if (!((typeof validator === 'object' &&
|
|
99
|
+
(validator.type === 'equal' ||
|
|
100
|
+
validator.type === 'required')) ||
|
|
101
|
+
(typeof validator === 'string' &&
|
|
102
|
+
(validator === 'equal' ||
|
|
103
|
+
validator === 'required')))) {
|
|
104
|
+
if (formReqFields[`${key}-amount`] === null) {
|
|
105
|
+
Object.assign(formReqFields, {
|
|
106
|
+
'amountWithUnitSelect-amount': fields[`${key}-amount`]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!formReqFields[`${key}-amount`]?.validate?.includes(validator)) {
|
|
110
|
+
formReqFields[`${key}-amount`].validate.push(validator);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates and adds a validation error for a child component into the form request's errors list.
|
|
120
|
+
* @param {Object} req - The request object given to the component.
|
|
121
|
+
* @param {Object} res - The response object given to the component.
|
|
122
|
+
* @param {Object[]} errors - The validation errors recorded in the session model.
|
|
123
|
+
* @param {string} pKey - The parent component's key.
|
|
124
|
+
* @param {string} key - The child component's key.
|
|
125
|
+
*/
|
|
126
|
+
const addValidationError = (req, res, errors, pKey, key) => {
|
|
127
|
+
// manually creates and adds an error object
|
|
128
|
+
req.form.errors[`${pKey}-${key}`] = {
|
|
129
|
+
errorLinkId: `${pKey}-${key}`,
|
|
130
|
+
key: errors[`${pKey}-${key}`]?.key || `${key}-${key}`,
|
|
131
|
+
type: errors[`${pKey}-${key}`]?.type || null
|
|
132
|
+
};
|
|
133
|
+
// ensure the error message is processed and translated by the controller
|
|
134
|
+
req.form.errors[`${pKey}-${key}`].message =
|
|
135
|
+
controller.getErrorMessage(req.form.errors[`${pKey}-${key}`], req, res) ||
|
|
136
|
+
controller.getErrorMessage({
|
|
137
|
+
errorLinkId: `${pKey}-${key}`,
|
|
138
|
+
key: `${pKey}`,
|
|
139
|
+
type: errors[`${pKey}-${key}`]?.type || null
|
|
140
|
+
}, req, res);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Inserts child component validation errors into the form request if there is no parent component error.
|
|
145
|
+
* Only one error from the component is added to the request's error list in the order of the parent, amount,
|
|
146
|
+
* and then unit component.
|
|
147
|
+
* @param {Object} req - The request object given to the component.
|
|
148
|
+
* @param {Object} res - The response object given to the component.
|
|
149
|
+
* @param {Object[]} errors - The validation errors recorded in the session model.
|
|
150
|
+
*/
|
|
151
|
+
const insertChildValidationErrors = (req, res, errors) => {
|
|
152
|
+
const pKey = 'amountWithUnitSelect';
|
|
153
|
+
let key;
|
|
154
|
+
if (errors && !errors[pKey] && req?.form?.errors) {
|
|
155
|
+
if (errors[`${pKey}-amount`] && req.form.errors[`${pKey}-amount`]) {
|
|
156
|
+
key = 'amount';
|
|
157
|
+
} else if (errors[`${pKey}-unit`] && req.form.errors[`${pKey}-unit`]) {
|
|
158
|
+
key = 'unit';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// if there are not parent or child errors, no errors are added
|
|
162
|
+
key && addValidationError(req, res, errors, pKey, key);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
isTwoHyphenSeparatedValues,
|
|
167
|
+
createCustomEqualValidator,
|
|
168
|
+
addValidator,
|
|
169
|
+
addGroupedFieldsWithOptionsProperty,
|
|
170
|
+
resolveOptionalFields,
|
|
171
|
+
propagateChildFieldValidation,
|
|
172
|
+
moveExcessValidatorToChildComponent,
|
|
173
|
+
addValidationError,
|
|
174
|
+
insertChildValidationErrors
|
|
175
|
+
};
|
package/components/index.js
CHANGED
|
@@ -5,6 +5,7 @@ module.exports = {
|
|
|
5
5
|
clearSession: require('./clear-session'),
|
|
6
6
|
combineAndLoopFields: require('./combine-and-loop-fields'),
|
|
7
7
|
date: require('./date'),
|
|
8
|
+
amountWithUnitSelect: require('./amount-with-unit-select/amount-with-unit-select'),
|
|
8
9
|
emailer: require('./emailer'),
|
|
9
10
|
homeOfficeCountries: require('./homeoffice-countries'),
|
|
10
11
|
notify: require('./notify'),
|
package/config/hof-defaults.js
CHANGED
package/controller/controller.js
CHANGED
|
@@ -200,10 +200,12 @@ module.exports = class Controller extends BaseController {
|
|
|
200
200
|
req.form.errors[key].errorLinkId = key + '-' + field.options[0];
|
|
201
201
|
}
|
|
202
202
|
// eslint-disable-next-line brace-style
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
else if (field && field.mixin === 'input-date') {
|
|
203
|
+
} else if (field && field.mixin === 'input-date') {
|
|
204
|
+
// get first field for date input control
|
|
206
205
|
req.form.errors[key].errorLinkId = key + '-day';
|
|
206
|
+
} else if (field && field.mixin === 'input-amount-with-unit-select') {
|
|
207
|
+
// get first field for amount-unit input control
|
|
208
|
+
req.form.errors[key].errorLinkId = key + '-amount';
|
|
207
209
|
} else {
|
|
208
210
|
req.form.errors[key].errorLinkId = key;
|
|
209
211
|
}
|
|
@@ -47,7 +47,7 @@ function validate(fields) {
|
|
|
47
47
|
fields[key].validate = [fields[key].validate];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
if (fields[key].options) {
|
|
50
|
+
if (fields[key].options && !fields[key].groupedFieldsWithOptions) {
|
|
51
51
|
fields[key].validate = fields[key].validate || [];
|
|
52
52
|
fields[key].validate.push({
|
|
53
53
|
type: 'equal',
|
|
@@ -177,5 +177,4 @@ module.exports = Validators = {
|
|
|
177
177
|
// eslint-disable-next-line max-len
|
|
178
178
|
return value === '' || Validators.regex(value, /^(([GIR] ?0[A]{2})|((([A-Z][0-9]{1,2})|(([A-Z][A-HJ-Y][0-9]{1,2})|(([A-Z][0-9][A-Z])|([A-Z][A-HJ-Y][0-9]?[A-Z])))) ?[0-9][A-Z]{2}))$/i);
|
|
179
179
|
}
|
|
180
|
-
|
|
181
180
|
};
|
|
@@ -14,6 +14,8 @@ const PARTIALS = [
|
|
|
14
14
|
'partials/forms/input-text-group',
|
|
15
15
|
'partials/forms/input-text-date',
|
|
16
16
|
'partials/forms/input-submit',
|
|
17
|
+
'partials/forms/grouped-inputs-select',
|
|
18
|
+
'partials/forms/grouped-inputs-text',
|
|
17
19
|
'partials/forms/select',
|
|
18
20
|
'partials/forms/checkbox',
|
|
19
21
|
'partials/forms/textarea-group',
|
|
@@ -214,6 +216,7 @@ module.exports = function (options) {
|
|
|
214
216
|
labelClassName: labelClassName ? `govuk-label ${labelClassName}` : 'govuk-label',
|
|
215
217
|
formGroupClassName: classNames(field, 'formGroupClassName') || extension.formGroupClassName || 'govuk-form-group',
|
|
216
218
|
hint: hint,
|
|
219
|
+
amountWithUnitSelectItemClassName: 'grouped-inputs__item',
|
|
217
220
|
hintId: extension.hintId || (hint ? key + '-hint' : null),
|
|
218
221
|
error: this.errors && this.errors[key],
|
|
219
222
|
maxlengthAttribute: field.maxlengthAttribute === true,
|
|
@@ -222,6 +225,7 @@ module.exports = function (options) {
|
|
|
222
225
|
required: required,
|
|
223
226
|
pattern: extension.pattern,
|
|
224
227
|
date: extension.date,
|
|
228
|
+
amountWithUnitSelect: extension.amountWithUnitSelect,
|
|
225
229
|
autocomplete: autocomplete,
|
|
226
230
|
child: field.child,
|
|
227
231
|
isPageHeading: field.isPageHeading,
|
|
@@ -232,7 +236,7 @@ module.exports = function (options) {
|
|
|
232
236
|
});
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
function optionGroup(key, opts) {
|
|
239
|
+
function optionGroup(key, opts, pKey = key) {
|
|
236
240
|
opts = opts || {};
|
|
237
241
|
const field = Object.assign({}, this.options.fields[key] || options.fields[key]);
|
|
238
242
|
const legend = field.legend;
|
|
@@ -248,6 +252,7 @@ module.exports = function (options) {
|
|
|
248
252
|
legendValue = legend.value;
|
|
249
253
|
}
|
|
250
254
|
}
|
|
255
|
+
|
|
251
256
|
return {
|
|
252
257
|
key: key,
|
|
253
258
|
error: this.errors && this.errors[key],
|
|
@@ -270,15 +275,16 @@ module.exports = function (options) {
|
|
|
270
275
|
|
|
271
276
|
if (typeof obj === 'string') {
|
|
272
277
|
value = obj;
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
// pKey - optional param that demotes parent key for group components - set to key param val by default
|
|
279
|
+
label = 'fields.' + pKey + '.options.' + obj + '.label';
|
|
280
|
+
optionHint = 'fields.' + pKey + '.options.' + obj + '.hint';
|
|
275
281
|
} else {
|
|
276
282
|
value = obj.value;
|
|
277
|
-
label = obj.label || 'fields.' +
|
|
283
|
+
label = obj.label || 'fields.' + pKey + '.options.' + obj.value + '.label';
|
|
278
284
|
toggle = obj.toggle;
|
|
279
285
|
child = obj.child;
|
|
280
286
|
useHintText = obj.useHintText;
|
|
281
|
-
optionHint = obj.hint || 'fields.' +
|
|
287
|
+
optionHint = obj.hint || 'fields.' + pKey + '.options.' + obj.value + '.hint';
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
if (this.values && this.values[key] !== undefined) {
|
|
@@ -469,6 +475,50 @@ module.exports = function (options) {
|
|
|
469
475
|
return parts.concat(monthPart, yearPart).join('\n');
|
|
470
476
|
};
|
|
471
477
|
}
|
|
478
|
+
},
|
|
479
|
+
'input-amount-with-unit-select': {
|
|
480
|
+
handler: function () {
|
|
481
|
+
return function (key) {
|
|
482
|
+
key = (key === '{{key}}' || key === '' || key === undefined) ? hoganRender(key, this) : key;
|
|
483
|
+
const field = Object.assign({}, this.options.fields[key] || options.fields[key]);
|
|
484
|
+
|
|
485
|
+
let autocomplete = field.autocomplete || 'off';
|
|
486
|
+
if (autocomplete === 'off') {
|
|
487
|
+
autocomplete = { amount: 'off'};
|
|
488
|
+
} else if (typeof autocomplete === 'string') {
|
|
489
|
+
autocomplete = { amount: autocomplete + '-amount' };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const formGroupClassName = (field.formGroup && field.formGroup.className) ? field.formGroup.className : '';
|
|
493
|
+
const classNameAmount = (field.controlsClass && field.controlsClass.amount) ? field.controlsClass.amount : 'govuk-input--width-3';
|
|
494
|
+
const classNameUnit = (field.controlsClass && field.controlsClass.unit) ? field.controlsClass.unit : 'govuk-input--width-5';
|
|
495
|
+
|
|
496
|
+
const parts = [];
|
|
497
|
+
|
|
498
|
+
// basically does the '_.each(mixins, function (mixin, name)' part manually (which renders the HTML
|
|
499
|
+
// for both child components and looks for a 'renderWith' and optional 'Options' method to use)
|
|
500
|
+
const amountPart = compiled['partials/forms/grouped-inputs-text']
|
|
501
|
+
.render(inputText.call(this,
|
|
502
|
+
key + '-amount', {
|
|
503
|
+
formGroupClassName,
|
|
504
|
+
autocomplete: autocomplete.amount,
|
|
505
|
+
className: classNameAmount,
|
|
506
|
+
amountWithUnitSelect: true }
|
|
507
|
+
));
|
|
508
|
+
|
|
509
|
+
const unitPart = compiled['partials/forms/grouped-inputs-select']
|
|
510
|
+
.render(inputText.call(this, key + '-unit',
|
|
511
|
+
optionGroup.call(this,
|
|
512
|
+
key + '-unit', {
|
|
513
|
+
formGroupClassName,
|
|
514
|
+
className: classNameUnit,
|
|
515
|
+
amountWithUnitSelect: true },
|
|
516
|
+
key
|
|
517
|
+
)));
|
|
518
|
+
|
|
519
|
+
return parts.concat(amountPart, unitPart).join('\n');
|
|
520
|
+
};
|
|
521
|
+
}
|
|
472
522
|
}
|
|
473
523
|
};
|
|
474
524
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="{{amountWithUnitSelectItemClassName}}">
|
|
2
|
+
<div id="{{id}}-group" class="{{#compound}} form-group-compound{{/compound}}{{#formGroupClassName}} {{formGroupClassName}}{{/formGroupClassName}}">
|
|
3
|
+
<label for="{{id}}" class="{{labelClassName}}">
|
|
4
|
+
<span class="label-text">{{{label}}}</span>
|
|
5
|
+
</label>
|
|
6
|
+
{{#hint}}<div {{$hintId}}id="{{hintId}}" {{/hintId}}class="govuk-hint">{{{hint}}}</div>{{/hint}}
|
|
7
|
+
<select id="{{id}}" class="govuk-select{{#className}} {{className}}{{/className}}{{#error}} govuk-select--error{{/error}}" name="{{id}}" aria-required="{{required}}">
|
|
8
|
+
{{#options}}
|
|
9
|
+
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option>
|
|
10
|
+
{{/options}}
|
|
11
|
+
</select>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="{{amountWithUnitSelectItemClassName}}">
|
|
2
|
+
<div id="{{id}}-group" class="{{#formGroupClassName}} {{formGroupClassName}}{{/formGroupClassName}}">
|
|
3
|
+
<label for="{{id}}" class="{{labelClassName}}">
|
|
4
|
+
<span class="label-text">{{{label}}}</span>
|
|
5
|
+
</label>
|
|
6
|
+
{{#hint}}<span {{$hintId}}id="{{hintId}}" {{/hintId}}class="govuk-hint">{{{hint}}}</span>{{/hint}}
|
|
7
|
+
{{#renderChild}}{{/renderChild}}
|
|
8
|
+
{{#attributes}}
|
|
9
|
+
{{#prefix}}
|
|
10
|
+
<div class="govuk-input__prefix" aria-hidden="true">{{prefix}}</div>
|
|
11
|
+
{{/prefix}}
|
|
12
|
+
{{/attributes}}
|
|
13
|
+
<input
|
|
14
|
+
type="{{type}}"
|
|
15
|
+
name="{{id}}"
|
|
16
|
+
id="{{id}}"
|
|
17
|
+
class="govuk-input{{#className}} {{className}}{{/className}}{{#error}} govuk-input--error{{/error}}"
|
|
18
|
+
aria-required="{{required}}"
|
|
19
|
+
{{#value}} value="{{value}}"{{/value}}
|
|
20
|
+
{{#min}} min="{{min}}"{{/min}}
|
|
21
|
+
{{#max}} max="{{max}}"{{/max}}
|
|
22
|
+
{{#maxlength}} maxlength="{{maxlength}}"{{/maxlength}}
|
|
23
|
+
{{#pattern}} pattern="{{pattern}}"{{/pattern}}
|
|
24
|
+
{{#hintId}} aria-describedby="{{hintId}}"{{/hintId}}
|
|
25
|
+
{{#error}} aria-invalid="true"{{/error}}
|
|
26
|
+
{{#autocomplete}} autocomplete="{{autocomplete}}"{{/autocomplete}}
|
|
27
|
+
{{#attributes}}
|
|
28
|
+
{{attribute}}="{{value}}"
|
|
29
|
+
{{/attributes}}
|
|
30
|
+
>
|
|
31
|
+
{{#attributes}}
|
|
32
|
+
{{#suffix}}
|
|
33
|
+
<div class="govuk-input__prefix" aria-hidden="true">{{suffix}}</div>
|
|
34
|
+
{{/suffix}}
|
|
35
|
+
{{/attributes}}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -85,6 +85,10 @@ function formFocus() {
|
|
|
85
85
|
document.getElementById(getElementFromSummaryLink + '-day').focus();
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
if (document.getElementById(getElementFromSummaryLink + '-amount') && forms.length === 1 && editMode) {
|
|
89
|
+
document.getElementById(getElementFromSummaryLink + '-amount').focus();
|
|
90
|
+
}
|
|
91
|
+
|
|
88
92
|
if (forms.length > 0) {
|
|
89
93
|
labels = document.getElementsByTagName('label');
|
|
90
94
|
if (labels) {
|
package/lib/encryption.js
CHANGED
|
@@ -1,23 +1,49 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
1
|
'use strict';
|
|
3
2
|
|
|
4
|
-
const crypto = require('crypto');
|
|
3
|
+
const crypto = require('node:crypto');
|
|
5
4
|
const algorithm = 'aes-256-cbc';
|
|
5
|
+
const ivLength = 16;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return dec;
|
|
7
|
+
/**
|
|
8
|
+
* Creates an encryption utility with AES-256-CBC algorithm.
|
|
9
|
+
* Provides encrypt and decrypt methods that use a random IV for each encryption operation.
|
|
10
|
+
*
|
|
11
|
+
* @module encryption
|
|
12
|
+
* @param {string|Buffer} secret - Must be exactly 32 bytes
|
|
13
|
+
* @returns {Object} Encryption utility object
|
|
14
|
+
* @throws {Error} If secret is not exactly 32 bytes
|
|
15
|
+
*/
|
|
16
|
+
module.exports = secret => {
|
|
17
|
+
const encryptionKey = Buffer.from(secret, 'utf8');
|
|
18
|
+
if (encryptionKey.byteLength !== 32) {
|
|
19
|
+
throw new Error(`Encryption secret must be exactly 32 bytes. Provided: ${encryptionKey.byteLength} bytes.`);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
return {
|
|
23
|
+
encrypt: text => {
|
|
24
|
+
try {
|
|
25
|
+
const iv = crypto.randomBytes(ivLength);
|
|
26
|
+
const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv);
|
|
27
|
+
let encrypted = cipher.update(text, 'utf8');
|
|
28
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
29
|
+
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Encryption failed: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
decrypt: text => {
|
|
36
|
+
try {
|
|
37
|
+
const textParts = text.split(':');
|
|
38
|
+
const iv = Buffer.from(textParts.shift(), 'hex');
|
|
39
|
+
const encryptedText = Buffer.from(textParts.join(':'), 'hex');
|
|
40
|
+
const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv);
|
|
41
|
+
let decrypted = decipher.update(encryptedText);
|
|
42
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
43
|
+
return decrypted.toString('utf8');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new Error(`Decryption failed: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
};
|
package/lib/sessions.js
CHANGED
|
@@ -10,8 +10,11 @@ const secureHttps = config => config.protocol === 'https' || config.env === 'pro
|
|
|
10
10
|
module.exports = (app, config) => {
|
|
11
11
|
const logger = config.logger || console;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const secretBuffer = Buffer.from(config.session.secret, 'utf8');
|
|
14
|
+
if (secretBuffer.byteLength !== 32) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Session secret must be exactly 32 bytes. Current: ${secretBuffer.byteLength} bytes.`
|
|
17
|
+
);
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
app.use(cookieParser(config.session.secret, {
|
package/lib/settings.js
CHANGED
|
@@ -42,7 +42,7 @@ module.exports = async (app, config) => {
|
|
|
42
42
|
viewsArray.slice().reverse().forEach(view => {
|
|
43
43
|
const customViewPath = path.resolve(config.root, view);
|
|
44
44
|
try {
|
|
45
|
-
fs.accessSync(customViewPath, fs.F_OK);
|
|
45
|
+
fs.accessSync(customViewPath, fs.constants.F_OK);
|
|
46
46
|
} catch (err) {
|
|
47
47
|
throw new Error(`Cannot find views at ${customViewPath}`);
|
|
48
48
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hof",
|
|
3
3
|
"description": "A bootstrap for HOF projects",
|
|
4
|
-
"version": "22.
|
|
4
|
+
"version": "22.12.0-unit-of-measure-beta.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"author": "HomeOffice",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=
|
|
10
|
-
"npm": ">=6.14.0"
|
|
9
|
+
"node": ">=14.0.0"
|
|
11
10
|
},
|
|
12
11
|
"bin": {
|
|
13
12
|
"hof-build": "./bin/hof-build",
|
|
@@ -17,10 +16,6 @@
|
|
|
17
16
|
"type": "git",
|
|
18
17
|
"url": "https://github.com/UKHomeOfficeForms/hof.git"
|
|
19
18
|
},
|
|
20
|
-
"publishConfig": {
|
|
21
|
-
"provenance": true,
|
|
22
|
-
"registry": "https://registry.npmjs.org/"
|
|
23
|
-
},
|
|
24
19
|
"bugs": {
|
|
25
20
|
"url": "https://github.com/UKHomeOfficeForms/hof/issues"
|
|
26
21
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const dateComponent = require('../../../').components.date;
|
|
5
5
|
const staticAppealStages = require('./lib/staticAppealStages');
|
|
6
|
+
const amountWithUnitSelectComponent = require('../../../').components.amountWithUnitSelect;
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
'landing-page-radio': {
|
|
@@ -124,5 +125,21 @@ module.exports = {
|
|
|
124
125
|
value: '',
|
|
125
126
|
label: 'fields.appealStages.options.null'
|
|
126
127
|
}].concat(staticAppealStages.getstaticAppealStages())
|
|
127
|
-
}
|
|
128
|
+
},
|
|
129
|
+
'amountWithUnitSelect' : amountWithUnitSelectComponent('amountWithUnitSelect', {
|
|
130
|
+
mixin: 'input-amount-with-unit-select',
|
|
131
|
+
amountLabel: "Amount-",
|
|
132
|
+
unitLabel: "Unit-",
|
|
133
|
+
options: [
|
|
134
|
+
{ "null": "Select" },
|
|
135
|
+
{ "label": "non trans option 1", "value": "1" },
|
|
136
|
+
{ "label": "non trans option 2", "value": "2" }
|
|
137
|
+
],
|
|
138
|
+
hint: "E.G: 5 Kilogram",
|
|
139
|
+
legend: 'Enter An Amount',
|
|
140
|
+
isPageHeading: 'true',
|
|
141
|
+
unitOptional: 'false',
|
|
142
|
+
amountOptional: 'true',
|
|
143
|
+
validate: ['alphanum']
|
|
144
|
+
})
|
|
128
145
|
}
|
|
@@ -39,5 +39,11 @@ module.exports = {
|
|
|
39
39
|
],
|
|
40
40
|
whatHappened: [
|
|
41
41
|
'whatHappened'
|
|
42
|
-
]
|
|
42
|
+
],
|
|
43
|
+
amountWithUnitSelect:
|
|
44
|
+
[{
|
|
45
|
+
field: 'amountWithUnitSelect',
|
|
46
|
+
parse: val => val ?
|
|
47
|
+
(val.substring(0, val.lastIndexOf('-')) || '0') + ' ' + val.substring(val.lastIndexOf('-') + 1) : ''
|
|
48
|
+
}]
|
|
43
49
|
};
|