hof 22.11.0 → 22.11.6-unit-of-measure-beta.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 (36) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +78 -0
  3. package/components/amount-with-unit-select/amount-with-unit-select.js +107 -0
  4. package/components/amount-with-unit-select/fields.js +15 -0
  5. package/components/amount-with-unit-select/hooks.js +168 -0
  6. package/components/amount-with-unit-select/templates/amount-with-unit-select.html +20 -0
  7. package/components/amount-with-unit-select/utils.js +194 -0
  8. package/components/amount-with-unit-select/validation.js +175 -0
  9. package/components/index.js +1 -0
  10. package/config/hof-defaults.js +1 -1
  11. package/controller/controller.js +5 -3
  12. package/controller/validation/index.js +1 -1
  13. package/controller/validation/validators.js +0 -1
  14. package/frontend/template-mixins/mixins/template-mixins.js +55 -5
  15. package/frontend/template-mixins/partials/forms/grouped-inputs-select.html +13 -0
  16. package/frontend/template-mixins/partials/forms/grouped-inputs-text.html +37 -0
  17. package/frontend/themes/gov-uk/styles/_grouped-input.scss +5 -0
  18. package/frontend/themes/gov-uk/styles/govuk.scss +1 -0
  19. package/frontend/toolkit/assets/javascript/form-focus.js +4 -0
  20. package/lib/encryption.js +43 -17
  21. package/lib/sessions.js +5 -2
  22. package/lib/settings.js +1 -1
  23. package/package.json +2 -3
  24. package/sandbox/apps/sandbox/fields.js +18 -1
  25. package/sandbox/apps/sandbox/index.js +4 -0
  26. package/sandbox/apps/sandbox/sections/summary-data-sections.js +7 -1
  27. package/sandbox/apps/sandbox/translations/en/default.json +33 -0
  28. package/sandbox/apps/sandbox/translations/src/en/fields.json +10 -0
  29. package/sandbox/apps/sandbox/translations/src/en/pages.json +3 -0
  30. package/sandbox/apps/sandbox/translations/src/en/validation.json +12 -0
  31. package/sandbox/package.json +1 -1
  32. package/sandbox/public/css/app.css +10042 -0
  33. package/sandbox/public/images/icons/icon-caret-left.png +0 -0
  34. package/sandbox/public/images/icons/icon-complete.png +0 -0
  35. package/sandbox/public/images/icons/icon-cross-remove-sign.png +0 -0
  36. package/sandbox/public/js/bundle.js +4 -0
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 2025-11-20, Version 22.12.0 (Stable), @dk4g @jamiecarterHO
2
+
3
+ ### Infrastructure
4
+ - Updated CI/CD pipeline to test against Node.js 20.x, 22.x, and 24.x
5
+ - Updated Redis testing versions to 7 and 8
6
+ - Added `NODE_VERSION` environment variable for consistent Node.js version across jobs
7
+ - Updated release process to use Node.js 24 for tagging and publishing operations
8
+
9
+ ### Security
10
+ - Replaced deprecated `crypto.createCipher`/`crypto.createDecipher` with `crypto.createCipheriv`/`crypto.createDecipheriv`
11
+ - Added proper initialisation vector (IV) handling for enhanced security
12
+ - Enforced 32-byte session secret requirement for AES-256 encryption compatibility
13
+ - Removed insecure default session secret ('changethis') - now requires explicit configuration
14
+
15
+ ### Migration Notes
16
+ - **Session Reset Required**: Due to enhanced encryption security, existing user sessions will be invalidated and users will need to re-authenticate after this update
17
+ - **Session Secret**: You must now set a unique `SESSION_SECRET` environment variable of exactly 32 bytes for encryption compatibility.
18
+ For testing purposes, you can use the following command to generate a random value. For production environments, consult a security expert or refer to official cryptographic guidelines to generate a secure secret
19
+ `node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"`
20
+
1
21
  ## 2025-11-15, Version 22.11.0 (Stable), @Rhodine-orleans-lindsay
2
22
 
3
23
  ### Changed
package/README.md CHANGED
@@ -1012,6 +1012,83 @@ Using the translation key `fields.field-name.label` will return different values
1012
1012
 
1013
1013
  # HOF Components
1014
1014
 
1015
+ ## AmountWithUnitSelect Component
1016
+
1017
+ A component for handling the rendering, processing, and validation of a text input (amount) field and a select-dropdown input field (unit) used in HOF applications.
1018
+
1019
+ ### Usage
1020
+
1021
+ In your fields config:
1022
+
1023
+ ```js
1024
+ const amountWithUnitSelectComponent = require("hof").components.amountWithUnitSelect;
1025
+
1026
+ module.exports = {
1027
+ "amountWithUnitSelect-field": amountWithUnitSelectComponent("amountWithUnitSelect", {
1028
+ mixin: 'input-amount-with-unit-select',
1029
+ amountLabel: "Amount:", // If not specified, defaults to 'Amount'
1030
+ unitLabel: "Unit:", // If not specified, defaults to 'Unit'
1031
+ options: [
1032
+ { "null": "Select..." }, // If a null option is not specified, a default null option with the label 'Select...' is included in the options
1033
+ { "label": "untranslated option label 1", "value": "1" },
1034
+ { "label": "untranslated option label 2", "value": "2" }
1035
+ ],
1036
+ hint: "E.g: 5 Kilogram",
1037
+ legend: 'Enter An Amount',
1038
+ isPageHeading: 'true',
1039
+ amountOptional: 'false', // If not specified, defaults to false if the required validator is not applied, otherwise true
1040
+ unitOptional: 'true', // If not specified, defaults to false if the required validator is not applied, otherwise true
1041
+ validate: ['alphanum']
1042
+ }),
1043
+ };
1044
+ ```
1045
+
1046
+ The above example will create a new AmountWithUnitSelect component with the key `'amountWithUnitSelect-field'`.
1047
+ It will set the AmountWithUnitSelect component's text input label to `'Amount:'` (instead of the default `'Amount'`) and the select input label to `'Unit:'` (instead of the default `'Unit'`).
1048
+ The component's Select input will have the dropdown options `'Select...'` (mapping to the value `'null'`), `'untranslated option label 1'` (mapping to the value `'1'`), and `'untranslated option label 2'` (mapping to the value `'2'`).
1049
+ The component's hint text will be set to `'E.G: 5 Kilogram'`, the field title/legend will be set to `'Enter An Amount'`, and the page heading will be the field's title.
1050
+ In terms of validation, the text input (amount) will have the `'required'` validator applied, but the select input (unit) will not (a value will not be required to be selected), and the text field will have the `'alphanum'` validator applied.
1051
+
1052
+ ### Configuration
1053
+
1054
+ The following optional configuration options are supported:
1055
+
1056
+ - `validate {String|Array}` – Validators to use on the text input field. The `'alphanum'` and `'required'` validators are likely to be used.
1057
+ - `template` – An absolute path to an alternate template.
1058
+ - `amountLabel {String}` – A custom label for the text input field (amount). Defaults to `'Amount'` if omitted. This can also be specified and defined in the field translations.
1059
+ - `unitLabel {String}` – A custom label for the select input field (unit). Defaults to `'Unit'` if omitted. This can also be specified and defined in the field translations.
1060
+ - `options {Array}` – A list of labels and corresponding values (options) for the select input field to present. Each option has the format `{ "label": "", "value": ""}`. The default/null option is defined with the format `{ "null": "Select" }` (where the text `'Select'` is the label and can be modified). If the null option is not defined, a default null option will be included in the options with the label `'Select...'`. This can also be specified and defined in the field translations.
1061
+ - `hint {String}` – Hint text displayed for both fields. This can also be specified and defined in the field translations.
1062
+ - `legend {String}` – Legend text displayed for both fields. This can also be specified and defined in the field translations.
1063
+ - `isPageHeading {Boolean}` – Sets the legend as the page heading on single-page questions.
1064
+ - `amountOptional {Boolean}` – The text input (amount) defaults to `''` (empty string) if omitted. Defaults to `false`. If the `'required'` validator is defined in the `validate` configuration option, then this configuration is ignored (both fields are made mandatory).
1065
+ - `unitOptional {Boolean}` – The select input (unit) defaults to `null` if omitted. Defaults to `false`. If the `'required'` validator is defined in the `validate` configuration option, then this configuration is ignored (both fields are made mandatory).
1066
+
1067
+ ### Validation Error Messages
1068
+
1069
+ In Validation.json (within the translations):
1070
+
1071
+ ```js
1072
+ "amountWithUnitSelect": {
1073
+ "default": "Enter the amount in the correct format; for example, 10 Litres",
1074
+ "alphanum": "The amount must not contain any special characters",
1075
+ "required": "Enter an amount and a unit value"
1076
+ },
1077
+ "amountWithUnitSelect-unit": {
1078
+ "default": "A valid value must be selected as the amount unit",
1079
+ "required": "A unit must be selected for the amount"
1080
+ },
1081
+ "amountWithUnitSelect-amount": {
1082
+ "alphanum": "The amount must not be an alphanum"
1083
+ }
1084
+ ```
1085
+
1086
+ Validation error messages can be defined for a specific child component that errored by adding a JSON object with a key that has the component's name (I.E. `'amountWithUnitSelect'`) followed by a hyphen and the child component's name (I.E. '-amount' or '-unit'). So to define an error message, for the `'required'` validation error, for specifically the unit component, an `'amountWithUnitSelect-unit'` object can be created (like in the example above) with a validator's name and the error message to show for it respectively, set as a key:value pair in the object (E.G. `"required": "A unit must be selected for the amount" within "amountWithUnitSelect-unit"` - like in the example).
1087
+ Validation error messages defined specifically for child components, for a given type of validation error, (such as `'required'` for `'amountWithUnitSelect-unit'`) will take precedence over error messages defined for the same validation error in the parent. So in this case, the `"required": "Enter an amount and a unit value"` validation message defined in the `"amountWithUnitSelect"` JSON object in the above example will not show when the `"amountWithUnitSelect-unit"` `'required'` validation error message is defined. If there was no `'required'` error message defined for, say, the `"amountWithUnitSelect-amount"` object (like in the example above), the `"amountWithUnitSelect"` object's `'required'` error message would show if the `'required'` validation error is triggered on the 'amount' child component (I.E. When an amount value is not provided).
1088
+ The `'default'` catch all validation error message can also be defined in a similar manner for the child components to override the parent component's defined `'default'` error message.
1089
+
1090
+ So the example above will create a scenario where `'required'` validation errors triggered on the 'unit' field will display the error message `"A unit must be selected for the amount"` (specified in the `"amountWithUnitSelect-unit"` JSON object). Any `'required'` validation errors triggered on the 'amount' field will display the error message `"Enter an amount and a unit value"` (specified in the `"amountWithUnitSelect"` JSON object - as there is no `'required'` error message defined specifically for the 'amount' field (withing `"amountWithUnitSelect-amount"`)).
1091
+
1015
1092
  ## Date Component
1016
1093
 
1017
1094
  A component for handling the rendering and processing of 3-input date fields used in HOF Applications.
@@ -1643,6 +1720,7 @@ currency
1643
1720
  select
1644
1721
  input-text
1645
1722
  input-date
1723
+ input-amount-with-unit-select
1646
1724
  input-text-compound
1647
1725
  input-text-code
1648
1726
  input-number
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const hooks = require('./hooks');
4
+ const path = require('path');
5
+ const getFields = require('./fields');
6
+
7
+ const TEMPLATE = path.resolve(__dirname, './templates/amount-with-unit-select.html');
8
+
9
+ module.exports = (key, opts) => {
10
+ if (!key) {
11
+ throw new Error('Key must be passed to amountWithUnitSelect component');
12
+ }
13
+
14
+ const fields = getFields(key); // the child field definitions and configurations
15
+ const options = opts || {}; // the component's configuration options
16
+ const template = options.template ? // the field template path
17
+ path.resolve(__dirname, options.template) :
18
+ TEMPLATE;
19
+
20
+ /**
21
+ * Pre-process hook.
22
+ * @param {Object} req - The form's request object.
23
+ * @param {Object} res - The form's response object.
24
+ * @param {Function} next - The next middleware function in the chain.
25
+ */
26
+ const preProcess = (req, res, next) => {
27
+ hooks.preProcess(req, fields, key);
28
+ next();
29
+ };
30
+
31
+ /**
32
+ * Post-process hook.
33
+ * @param {Object} req - The form's request object.
34
+ * @param {Object} res - The form's response object.
35
+ * @param {Function} next - The next middleware function in the chain.
36
+ */
37
+ const postProcess = (req, res, next) => {
38
+ hooks.postProcess(req, key);
39
+ next();
40
+ };
41
+
42
+ /**
43
+ * Pre-validate hook.
44
+ * @param {Object} req - The form's request object.
45
+ * @param {Object} res - The form's response object.
46
+ * @param {Function} next - The next middleware function in the chain.
47
+ */
48
+ const preValidate = (req, res, next) => {
49
+ hooks.preValidate(req, fields, key, options);
50
+ next();
51
+ };
52
+
53
+ /**
54
+ * Pre-getErrors hook.
55
+ * @param {Object} req - The form's request object.
56
+ * @param {Object} res - The form's response object.
57
+ * @param {Function} next - The next middleware function in the chain.
58
+ */
59
+ const preGetErrors = (req, res, next) => {
60
+ hooks.preGetErrors(req, fields, key);
61
+ next();
62
+ };
63
+
64
+ /**
65
+ * Post-getErrors hook.
66
+ * @param {Object} req - The form's request object.
67
+ * @param {Object} res - The form's response object.
68
+ * @param {Function} next - The next middleware function in the chain.
69
+ */
70
+ const postGetErrors = (req, res, next) => {
71
+ hooks.postGetErrors(req, res, fields, key);
72
+ next();
73
+ };
74
+
75
+ /**
76
+ * Post-getValues hook.
77
+ * @param {Object} req - The form's request object.
78
+ * @param {Object} res - The form's response object.
79
+ * @param {Function} next - The next middleware function in the chain.
80
+ */
81
+ const postGetValues = (req, res, next) => {
82
+ hooks.postGetValues(req, fields, key);
83
+ next();
84
+ };
85
+
86
+ /**
87
+ * Pre-render hook.
88
+ * @param {Object} req - The form's request object.
89
+ * @param {Object} res - The form's response object.
90
+ * @param {Function} next - The next middleware function in the chain.
91
+ */
92
+ const preRender = (req, res, next) => {
93
+ hooks.preRender(req, res, fields, options, template, key, next);
94
+ };
95
+
96
+ return Object.assign({}, options, {
97
+ hooks: {
98
+ 'pre-process': preProcess,
99
+ 'post-process': postProcess,
100
+ 'pre-validate': preValidate,
101
+ 'pre-getErrors': preGetErrors,
102
+ 'post-getErrors': postGetErrors,
103
+ 'post-getValues': postGetValues,
104
+ 'pre-render': preRender
105
+ }
106
+ });
107
+ };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ module.exports = key => ({
4
+ [`${key}-amount`]: {
5
+ label: 'Amount',
6
+ autocomplete: 'off',
7
+ validate: []
8
+ },
9
+ [`${key}-unit`]: {
10
+ label: 'Unit',
11
+ groupedFieldsWithOptions: true,
12
+ options: {},
13
+ validate: []
14
+ }
15
+ });
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const utils = require('./utils');
5
+ const validation = require('./validation');
6
+
7
+ /**
8
+ * Pre-process hook. This function:
9
+ * - Splits the amountWithUnitSelect value into its parts (amount and unit in the format [Amount]-[Unit],
10
+ * E.G. 5-Kilograms) and assigns them to the request body (req.body).
11
+ * @param {Object} req - The form's request object.
12
+ * @param {Object} fields - The component's child field definitions and configurations.
13
+ * @param {string} key - The parent component's key.
14
+ */
15
+ const preProcess = (req, fields, key) => {
16
+ const parts = utils.getParts(req.body, fields, key);
17
+ if (_.some(parts, part => part !== '')) {
18
+ req.body[key] = `${(parts.amount || '')}-${(parts.unit || '')}`;
19
+ }
20
+ };
21
+
22
+ /**
23
+ * Post-process hook. This function:
24
+ * - Copies the field value from the request body (req.body) to the form values (req.form.values)
25
+ * if a reference to the field exists in the form values.
26
+ * @param {Object} req - The form's request object.
27
+ * @param {string} key - The parent component's key.
28
+ */
29
+ const postProcess = (req, key) => {
30
+ if (req.form.values[key]) {
31
+ req.form.values[key] = req.body[key];
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Pre-validate hook. This function:
37
+ * - Prevents default select component assignment of 'equal' validator to the parent component.
38
+ * - Resolves required validators and optional configurations for child components.
39
+ * - Propagates child component field data and values to the request to enable their validation.
40
+ * - Adds a custom 'equal' validator to the unit child component.
41
+ * - Adds a custom 'twoHyphenSeparatedValues' validator to the parent component to validate overall value format.
42
+ * - Moves excess validators that do not apply to the parent component to the 'amount' child component.
43
+ * @param {Object} req - The form's request object.
44
+ * @param {Object} fields - The component's child field definitions and configurations.
45
+ * @param {string} key - The parent component's key.
46
+ * @param {Object} options - The component's configuration options.
47
+ */
48
+ const preValidate = (req, fields, key, options) => {
49
+ // Prevents auto assignment of 'equal' validator to parent component
50
+ validation.addGroupedFieldsWithOptionsProperty(req.form.options.fields[key]);
51
+ // resolves required validators and optional configurations for child components
52
+ validation.resolveOptionalFields(req.form.options.fields, fields, options.validate, key);
53
+ // propagates child component field data and values to the request to enable validation
54
+ validation.propagateChildFieldValidation(req.form, fields, key);
55
+ // adds custom 'equal' validator to the unit child component
56
+ validation.addValidator(fields[`${key}-unit`], validation.createCustomEqualValidator(fields[`${key}-unit`].options));
57
+ // adds custom 'twoHyphenSeparatedValues' validator to the parent component to validate overall value format
58
+ validation.addValidator(options, validation.isTwoHyphenSeparatedValues);
59
+ // moves excess validators that do not apply to the parent component to the 'amount' child component
60
+ validation.moveExcessValidatorToChildComponent(req.form.options.fields, fields, key);
61
+ };
62
+
63
+ /**
64
+ * Pre-getErrors hook. This function:
65
+ * - If the parent component has a flagged error, this extends the session model's error values with
66
+ * the child components' error values.
67
+ * @param {Object} req - The form's request object.
68
+ * @param {Object} fields - The component's child field definitions and configurations.
69
+ * @param {string} key - The parent component's key.
70
+ */
71
+ const preGetErrors = (req, fields, key) => {
72
+ // if the amountWithUnitSelect field is included in errorValues (E.G. if there was a validation error),
73
+ // extend errorValues with the individual components
74
+ // (I.E. add the child components' K:V pair to the request sessionModel's attributes)
75
+ const errorValues = req.sessionModel.get('errorValues');
76
+ if (errorValues && errorValues[key]) {
77
+ req.sessionModel.set('errorValues',
78
+ Object.assign({}, errorValues, utils.getPartsFromAmountWithUnitSelect(errorValues[key], Object.keys(fields)))
79
+ );
80
+ }
81
+ };
82
+
83
+ /**
84
+ * Post-getErrors hook. This function:
85
+ * - Ensures only one error is associated with the components in the request form errors
86
+ * (by setting excess errors' type to null) when either the parent or child components have
87
+ * (jointly) multiple errors in the session model.
88
+ * - If there is no parent component error, one of the child component errors (if any) is inserted.
89
+ * @param {Object} req - The form's request object.
90
+ * @param {Object} res - The form's response object.
91
+ * @param {Object} fields - The component's child field definitions and configurations.
92
+ * @param {string} key - The parent component's key.
93
+ */
94
+ const postGetErrors = (req, res, fields, key) => {
95
+ // if the amountWithUnitSelect field or its child fields have any recorded validation error,
96
+ // the remaining errors are added to req.form.errors
97
+ // and their type is set to null to avoid duplicate error messages
98
+ const errors = req.sessionModel.get('errors');
99
+ if (errors && (errors[key] || errors[`${key}-amount`] || errors[`${key}-unit`])) {
100
+ Object.assign(req.form.errors, Object.keys(fields).reduce((obj, field) =>
101
+ Object.assign({}, obj, { [field]: { type: null } })
102
+ , {}));
103
+ }
104
+
105
+ // inserts child component validation errors into req.form.errors
106
+ validation.insertChildValidationErrors(req, res, key, errors);
107
+ };
108
+
109
+ /**
110
+ * Post-getValues hook. This function:
111
+ * - Splits the component's value into its parts and assigns them to the request object's form values (req.form.values).
112
+ * @param {Object} req - The form's request object.
113
+ * @param {Object} fields - The component's child field definitions and configurations.
114
+ * @param {string} key - The parent component's key.
115
+ */
116
+ const postGetValues = (req, fields, key) => {
117
+ // if amountWithUnitSelect value is set, split it into its parts and assign to req.form.values
118
+ // extends the session model's error values, if any
119
+ const amountWithUnitSelect = req.form.values[key];
120
+ if (amountWithUnitSelect) {
121
+ Object.assign(
122
+ req.form.values,
123
+ utils.getPartsFromAmountWithUnitSelect(amountWithUnitSelect, Object.keys(fields)),
124
+ req.sessionModel.get('errorValues') || {}
125
+ );
126
+ }
127
+ };
128
+
129
+ /**
130
+ * Pre-render hook. This function:
131
+ * - Translates the unit options and child component labels.
132
+ * - Renders the component's template to a string
133
+ * and assigns the HTML output to the component field in res.locals.fields.
134
+ * @param {Object} req - The form's request object.
135
+ * @param {Object} res - The form's response object.
136
+ * @param {Object} fields - The component's child field definitions and configurations.
137
+ * @param {Object} options - The component's configuration options.
138
+ * @param {string} template - The component's template path.
139
+ * @param {string} key - The parent component's key.
140
+ * @param {Function} next - The next middleware function in the chain.
141
+ */
142
+ const preRender = (req, res, fields, options, template, key, next) => {
143
+ // applies translations
144
+ utils.translateUnitOptions(req, fields, options, key);
145
+ utils.translateLabels(req, fields, key, ['amount', 'unit']);
146
+
147
+ // renders the template to a string and assign the html output
148
+ // to the amountWithUnitSelect field (in res.locals.fields)
149
+ res.render(template, utils.constructFieldToRender(req, fields, options, key), (err, html) => {
150
+ if (err) {
151
+ next(err);
152
+ } else {
153
+ const field = res.locals.fields.find(f => f.key === key);
154
+ Object.assign(field, { html });
155
+ next();
156
+ }
157
+ });
158
+ };
159
+
160
+ module.exports = {
161
+ preProcess,
162
+ postProcess,
163
+ preValidate,
164
+ preGetErrors,
165
+ postGetErrors,
166
+ postGetValues,
167
+ preRender
168
+ };
@@ -0,0 +1,20 @@
1
+ <div class="govuk-form-group {{#error}}govuk-form-group--error{{/error}}">
2
+ <fieldset id="{{key}}-group" class="govuk-fieldset{{#className}} {{className}}{{/className}}" role="group">
3
+ <legend class="govuk-fieldset__legend {{#isPageHeading}}govuk-fieldset__legend--l{{/isPageHeading}}{{#legendClassName}} {{legendClassName}}{{/legendClassName}}">
4
+ {{#isPageHeading}}<h1 class="govuk-fieldset__heading">{{/isPageHeading}}
5
+ {{legend}}
6
+ {{#isPageHeading}}</h1>{{/isPageHeading}}
7
+ </legend>
8
+ {{#hint}}
9
+ <span id="{{key}}-hint" class="govuk-hint">{{hint}}</span>
10
+ {{/hint}}
11
+ {{#error}}
12
+ <p id="{{key}}-error" class="govuk-error-message">
13
+ <span class="govuk-visually-hidden">Error:</span> {{error.message}}
14
+ </p>
15
+ {{/error}}
16
+ <div id="{{key}}" class="govuk-amount-with-unit-select-input">
17
+ {{#input-amount-with-unit-select}}{{key}}{{/input-amount-with-unit-select}}
18
+ </div>
19
+ </fieldset>
20
+ </div>
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+
5
+ /**
6
+ * Translates a field property (given its key and a translation function),
7
+ * returning null if there is no translation (enabling conditional statements to branch if there is no translation).
8
+ * @param {string} key - The key of the field property to translate
9
+ * (E.G. 'fields.amountWithUnitSelect-amount.label' - references the amount field label).
10
+ * @param {function(string): string} translate - The translation function to apply.
11
+ * @returns {string|null} Returns the translation or null if there is no translation.
12
+ */
13
+ const conditionalTranslate = (key, translate) => {
14
+ let result = translate(key);
15
+ if (result === key) {
16
+ result = null;
17
+ }
18
+ return result;
19
+ };
20
+
21
+ /**
22
+ * Gets the classname specified for the field's legend.
23
+ * Defaults to an empty string if not specified.
24
+ * @param {object} field - The field with (potentially) the legend classname.
25
+ * @returns {string} The classname specified for the field's legend text.
26
+ */
27
+ const getLegendClassName = field =>
28
+ field?.legend?.className || '';
29
+
30
+ /**
31
+ * Gets a boolean determining if the field's heading has been set to be the page heading.
32
+ * @param {Object} field - The field with the heading to (potentially) make the page title.
33
+ * @returns {string} A boolean that indicates if the field's heading is also the page heading.
34
+ */
35
+ const getIsPageHeading = field =>
36
+ field?.isPageHeading || '';
37
+
38
+ /**
39
+ * Creates a new object with the component's assigned 'amount' and 'unit' values.
40
+ * It does this by looking for the 'amountWithUnitSelect-amount' and 'amountWithUnitSelect-unit' fields in req.body
41
+ * and making a new object that copies those 2 fields and values, and removes the 'amountWithUnitSelect-' suffix.
42
+ * @param {Object} body - The submitted request's body (req.body) containing K:V pairs
43
+ * (E.G. 'amountWithUnitSelect-amount : 12').
44
+ * @param {Object} fields - The set of fields relevant to this component (I.E. fields defined in ./fields.js).
45
+ * @param {string} key - The grouped child fields' parent name/key ('amountWithUnitSelect' in this case).
46
+ * @returns {{ amount: string, unit: string }} Returns a map of values in the format { amount: '1', unit: 'litres' }.
47
+ */
48
+ const getParts = (body, fields, key) =>
49
+ _.mapKeys(_.pick(body, Object.keys(fields)), (value, fieldKey) =>
50
+ fieldKey.replace(`${key}-`, '')
51
+ );
52
+
53
+ /**
54
+ * Splits the AmountWithUnitSelect value (usually in the format '[Amount]-[Unit]') by the last hyphen in the text
55
+ * into an Array with 2 elements (the amount and unit - the value before and after the hyphen respectively).
56
+ * Returns an empty string for each element if in an unexpected format.
57
+ * @param {string} amountWithUnitSelectVal - The amountWithUnitSelect value (E.G. '1-Litre').
58
+ * @returns {string[]} Returns an array in format [amount, unit].
59
+ */
60
+ const getAmountWithUnitSelectValues = amountWithUnitSelectVal => {
61
+ const splitPointIndex = typeof amountWithUnitSelectVal === 'string' ?
62
+ amountWithUnitSelectVal.lastIndexOf('-') :
63
+ -1;
64
+
65
+ return splitPointIndex === -1 ?
66
+ ['', ''] :
67
+ [amountWithUnitSelectVal.substring(0, splitPointIndex), amountWithUnitSelectVal.substring(splitPointIndex + 1)];
68
+ };
69
+
70
+ /**
71
+ * Returns a map of the component's fields' keys and their assigned values.
72
+ * (E.G. If amountWithUnitSelectVal = 1-L,
73
+ * it returns { amountWithUnitSelect-amount: '1', amountWithUnitSelect-unit: 'L' }).
74
+ * @param {string} amountWithUnitSelectVal - AmountWithUnitSelect value in the format '[Amount]-[Unit]'.
75
+ * @param {Object} fields - The component's child field definitions and configurations.
76
+ * @returns {amountWithUnitSelect-amount: string, amountWithUnitSelect-unit: string} Returns a map of K:V pairs
77
+ * for each child field.
78
+ */
79
+ const getPartsFromAmountWithUnitSelect = (amountWithUnitSelectVal, fields) =>
80
+ getAmountWithUnitSelectValues(amountWithUnitSelectVal).reduce(
81
+ (obj, value, index) => Object.assign({},
82
+ obj,
83
+ {[fields[index]]: value }),
84
+ {});
85
+
86
+ /**
87
+ * Translates the labels of the child fields of the AmountWithUnitSelect component.
88
+ * Depending on which exists, labels are set in the following order of precedence:
89
+ * 1. A specific child-component translation (I.E. amountWithUnitSelect-amount.label).
90
+ * 2. A parent-component translation for the child field (I.E. amountWithUnitSelect.amountLabel).
91
+ * 3. A specific child-component (non-translated) label configuration.
92
+ * 4. A parent-component (non-translated) label configuration for the child field.
93
+ * @param {Object} req - The form's request object.
94
+ * @param {Object} fields - The component's child field definitions and configurations.
95
+ * @param {string} pKey - The parent component's key (E.G. 'amountWithUnitSelect').
96
+ * @param {string[]} keys - The list of child components (by keys) to translate (E.G. ['amount', 'unit']).
97
+ */
98
+ const translateLabels = (req, fields, pKey, keys) => {
99
+ keys.forEach(key => {
100
+ fields[`${pKey}-${key}`].label =
101
+ conditionalTranslate(`fields.${pKey}-${key}.label`, req.translate) ||
102
+ conditionalTranslate(`fields.${pKey}.${key}Label`, req.translate) ||
103
+ fields[`${pKey}-${key}`].label ||
104
+ req.form.options.fields[`${pKey}`]?.[`${key}Label`];
105
+ });
106
+ };
107
+
108
+ /**
109
+ * Adds the component's child fields to the request's form options fields (req.form.options.fields).
110
+ * @param {Object} req - The form's request object.
111
+ * @param {Object} fields - The component's child field definitions and configurations.
112
+ * @param {string} key - The parent component's key (E.G. 'amountWithUnitSelect').
113
+ */
114
+ const addChildFieldsToRequestForm = (req, fields, key) => {
115
+ Object.assign(req.form.options.fields, _.mapValues(fields, (v, k) => {
116
+ const rawKey = k.replace(`${key}-`, '');
117
+ const labelKey = `fields.${key}.parts.${rawKey}`;
118
+ const label = req.translate(labelKey);
119
+
120
+ return Object.assign({}, v, {
121
+ label: label === labelKey ? v.label : label
122
+ });
123
+ }));
124
+ };
125
+
126
+ /**
127
+ * Sets a default null option for a select component if undefined.
128
+ * The default null option has a label of 'Select...' and a value of an empty string.
129
+ * @param {Object[]} options - The dropdown menu options.
130
+ * @returns {Object[]} Returns the options with the null/default option resolved.
131
+ */
132
+ const resolveNullOption = options => {
133
+ const nullOptionLabel = options.find(opt => opt.null !== undefined && Object.keys(opt).length === 1);
134
+ const nonNullOptions = options.filter(opt => opt.null === undefined || Object.keys(opt).length !== 1);
135
+
136
+ return [{label: (nullOptionLabel !== undefined ? nullOptionLabel.null : 'Select...'), value: ''}]
137
+ .concat(nonNullOptions);
138
+ };
139
+
140
+ /**
141
+ * Translates the unit field's/component's options (dropdown menu options) for the AmountWithUnitSelect component.
142
+ * Also resolves the null/default select option's label and value.
143
+ * @param {Object} req - The form's request object.
144
+ * @param {Object} fields - The component's child field definitions and configurations.
145
+ * @param {Object} options - The component's configuration options.
146
+ * @param {String} key - The parent component's key (E.G. 'amountWithUnitSelect').
147
+ */
148
+ const translateUnitOptions = (req, fields, options, key) => {
149
+ // sets unit options as either translations (if they exist) or default untranslated options
150
+ const optionsToDisplay = conditionalTranslate(`fields.${key}-unit.options`, req.translate) ||
151
+ conditionalTranslate(`fields.${key}.options`, req.translate) ||
152
+ options.options;
153
+
154
+ // resolves the null/default select option
155
+ fields[`${key}-unit`].options = resolveNullOption(optionsToDisplay);
156
+ };
157
+
158
+ /**
159
+ * Constructs an object with field data required to render the AmountWithUnitSelect component.
160
+ * @param {Object} req - The form's request object.
161
+ * @param {Object} fields - The component's child field definitions and configurations.
162
+ * @param {Object} options - The component's configuration options.
163
+ * @param {string} key - The parent component's key.
164
+ * @returns {Object} Returns an object with field data required to render the component.
165
+ */
166
+ const constructFieldToRender = (req, fields, options, key) => {
167
+ addChildFieldsToRequestForm(req, fields, key);
168
+
169
+ const reqForm = req.form;
170
+ const legend = conditionalTranslate(`fields.${key}.legend`, req.translate) ||
171
+ reqForm.options.fields[`${key}`]?.legend;
172
+ const hint = conditionalTranslate(`fields.${key}.hint`, req.translate) ||
173
+ reqForm.options.fields[`${key}`]?.hint;
174
+ const legendClassName = getLegendClassName(options);
175
+ const isPageHeading = getIsPageHeading(options);
176
+ const error = reqForm.errors &&
177
+ (reqForm.errors[key] || reqForm.errors[`${key}-amount`] || reqForm.errors[`${key}-unit`]);
178
+
179
+ return { key, legend, legendClassName, isPageHeading, hint, error };
180
+ };
181
+
182
+ module.exports = {
183
+ conditionalTranslate,
184
+ getLegendClassName,
185
+ getIsPageHeading,
186
+ getParts,
187
+ getAmountWithUnitSelectValues,
188
+ getPartsFromAmountWithUnitSelect,
189
+ translateLabels,
190
+ addChildFieldsToRequestForm,
191
+ resolveNullOption,
192
+ translateUnitOptions,
193
+ constructFieldToRender
194
+ };