hof 21.0.0-instrumentation-beta.0 → 21.0.1-axios-beta

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 (106) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +4 -4
  3. package/.nyc_output/4fc007c9-d6c8-4614-89ce-04c7d6ce9fe5.json +1 -0
  4. package/.nyc_output/processinfo/4fc007c9-d6c8-4614-89ce-04c7d6ce9fe5.json +1 -0
  5. package/.nyc_output/processinfo/index.json +1 -1
  6. package/README.md +340 -256
  7. package/build/tasks/sass/index.js +3 -1
  8. package/build/tasks/watch/index.js +1 -1
  9. package/components/combine-and-loop-fields/Readme.md +42 -0
  10. package/components/combine-and-loop-fields/index.js +156 -0
  11. package/components/date/index.js +3 -1
  12. package/components/date/templates/date.html +15 -12
  13. package/components/homeoffice-countries/index.js +22 -0
  14. package/components/index.js +2 -0
  15. package/components/notify/notify.js +2 -2
  16. package/components/summary/index.js +3 -2
  17. package/config/builder-defaults.js +3 -1
  18. package/config/component-defaults.js +13 -0
  19. package/controller/controller.js +57 -1
  20. package/controller/formatting/formatters.js +12 -0
  21. package/controller/validation/index.js +2 -1
  22. package/controller/validation/validators.js +4 -0
  23. package/frontend/govuk-template/build/config.js +2 -2
  24. package/frontend/govuk-template/build/govuk_template.html +104 -0
  25. package/frontend/govuk-template/build/index.js +2 -2
  26. package/frontend/govuk-template/index.js +4 -4
  27. package/frontend/template-mixins/mixins/template-mixins.js +39 -11
  28. package/frontend/template-mixins/partials/forms/checkbox-group.html +47 -0
  29. package/frontend/template-mixins/partials/forms/checkbox.html +4 -4
  30. package/frontend/template-mixins/partials/forms/input-submit.html +1 -1
  31. package/frontend/template-mixins/partials/forms/input-text-date.html +37 -0
  32. package/frontend/template-mixins/partials/forms/input-text-group.html +15 -10
  33. package/frontend/template-mixins/partials/forms/option-group.html +42 -26
  34. package/frontend/template-mixins/partials/forms/select.html +10 -5
  35. package/frontend/template-mixins/partials/forms/textarea-group.html +37 -23
  36. package/frontend/template-mixins/partials/mixins/panel.html +3 -4
  37. package/frontend/template-partials/views/accessibility.html +4 -4
  38. package/frontend/template-partials/views/cookies.html +1 -1
  39. package/frontend/template-partials/views/layout.html +24 -17
  40. package/frontend/template-partials/views/partials/back.html +1 -1
  41. package/frontend/template-partials/views/partials/bullet-list.html +1 -1
  42. package/frontend/template-partials/views/partials/confirmation-alert.html +4 -3
  43. package/frontend/template-partials/views/partials/continue.html +1 -1
  44. package/frontend/template-partials/views/partials/cookie-banner.html +27 -24
  45. package/frontend/template-partials/views/partials/cookie-settings-radio.html +6 -6
  46. package/frontend/template-partials/views/partials/external-link.html +1 -1
  47. package/frontend/template-partials/views/partials/form.html +2 -1
  48. package/frontend/template-partials/views/partials/maincontent-left.html +4 -4
  49. package/frontend/template-partials/views/partials/navigation.html +7 -6
  50. package/frontend/template-partials/views/partials/session-cookies-table.html +6 -6
  51. package/frontend/template-partials/views/partials/summary-table-row.html +2 -2
  52. package/frontend/template-partials/views/partials/table.html +7 -7
  53. package/frontend/template-partials/views/partials/validation-list.html +2 -2
  54. package/frontend/template-partials/views/partials/validation-summary.html +14 -13
  55. package/frontend/template-partials/views/partials/warn.html +7 -0
  56. package/frontend/template-partials/views/session-timeout.html +3 -2
  57. package/frontend/themes/gov-uk/client-js/cookieSettings.js +1 -1
  58. package/frontend/themes/gov-uk/client-js/govuk-cookies.js +121 -0
  59. package/frontend/themes/gov-uk/client-js/index.js +6 -1
  60. package/frontend/themes/gov-uk/client-js/skip-to-main.js +19 -0
  61. package/frontend/themes/gov-uk/styles/_cookie-banner.scss +51 -1
  62. package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
  63. package/frontend/themes/gov-uk/styles/modules/_validation.scss +5 -5
  64. package/frontend/toolkit/assets/javascript/character-count.js +4 -4
  65. package/frontend/toolkit/assets/javascript/progressive-reveal.js +3 -1
  66. package/frontend/toolkit/assets/javascript/validation.js +5 -1
  67. package/frontend/toolkit/assets/stylesheets/modules/_validation.scss +3 -3
  68. package/index.js +15 -2
  69. package/lib/ga-tag.js +1 -1
  70. package/lib/settings.js +18 -2
  71. package/middleware/errors.js +2 -3
  72. package/middleware/not-found.js +0 -3
  73. package/middleware/rate-limiter.js +1 -0
  74. package/model/apis/html-to-pdf-converter.js +9 -8
  75. package/model/index.js +27 -28
  76. package/package.json +16 -14
  77. package/sandbox/README.md +3 -3
  78. package/sandbox/apps/sandbox/fields.js +33 -11
  79. package/sandbox/apps/sandbox/index.js +4 -0
  80. package/sandbox/apps/sandbox/sections/summary-data-sections.js +3 -0
  81. package/sandbox/apps/sandbox/translations/en/default.json +224 -0
  82. package/sandbox/apps/sandbox/translations/src/en/fields.json +11 -4
  83. package/sandbox/apps/sandbox/translations/src/en/journey.json +4 -1
  84. package/sandbox/apps/sandbox/translations/src/en/pages.json +7 -25
  85. package/sandbox/apps/sandbox/translations/src/en/validation.json +5 -1
  86. package/sandbox/assets/js/index.js +1 -1
  87. package/sandbox/assets/scss/app.scss +16 -16
  88. package/sandbox/package.json +6 -1
  89. package/sandbox/public/css/app.css +2793 -0
  90. package/sandbox/public/images/icons/icon-caret-left.png +0 -0
  91. package/sandbox/public/images/icons/icon-complete.png +0 -0
  92. package/sandbox/public/images/icons/icon-cross-remove-sign.png +0 -0
  93. package/sandbox/public/js/bundle.js +32888 -0
  94. package/sandbox/server.js +2 -1
  95. package/sandbox/yarn.lock +243 -1
  96. package/wizard/index.js +0 -13
  97. package/wizard/middleware/check-progress.js +36 -1
  98. package/.nyc_output/4d5a4574-78fc-4fcb-9412-3658f6ce33ff.json +0 -1
  99. package/.nyc_output/processinfo/4d5a4574-78fc-4fcb-9412-3658f6ce33ff.json +0 -1
  100. package/frontend/govuk-template/govuk_template.html +0 -109
  101. package/frontend/themes/gov-uk/views/partials/form.html +0 -9
  102. package/frontend/themes/gov-uk/views/partials/forms/option-group.html +0 -28
  103. package/frontend/themes/gov-uk/views/partials/mixins/panel.html +0 -3
  104. package/frontend/themes/gov-uk/views/partials/validation-summary.html +0 -24
  105. package/middleware/monitor.js +0 -20
  106. package/sandbox/apps/sandbox/views/confirmation.html +0 -15
@@ -26,7 +26,9 @@ module.exports = config => {
26
26
  sass.render({
27
27
  file: config.sass.src,
28
28
  importer: importer({ aliases }),
29
- aliases
29
+ aliases,
30
+ outputStyle: config.sass.outputStyle,
31
+ quietDeps: config.sass.quietDeps
30
32
  }, (err, result) => err ? reject(err) : resolve(result.css));
31
33
  }))
32
34
  .then(css => new Promise((resolve, reject) => {
@@ -116,7 +116,7 @@ module.exports = config => {
116
116
 
117
117
  if (process.env.HOF_SANDBOX === 'true') {
118
118
  const rootDir = require('path').resolve(__dirname, '../../../');
119
- ignored.push(`${rootDir}/frontend/govuk-template/govuk_template.html`);
119
+ ignored.push(`${rootDir}/frontend/govuk-template/govuk_template_generated.html`);
120
120
  watchLocation = [rootDir, '.'];
121
121
  }
122
122
 
@@ -0,0 +1,42 @@
1
+ # Combine & Loop Fields Behaviour
2
+
3
+ ## What this does
4
+ This allows you to specify fields to loop over and add as objects to a parent array. You can use this for adding multiple addresses, criminal offences, names etc. You can see here in this example, we ask for a set of details prefixed with the word `storage-` for getting address details for multiple storage addresses (see the Home Office's firearms repository). We then can aggregate these addresses as objects into an array called `all-storage-addresses` and get redirected back to `/add-address` whenever we selected `yes` to adding another address,
5
+ ```
6
+ '/add-address': {
7
+ fields: [
8
+ 'storage-building',
9
+ 'storage-street',
10
+ 'storage-townOrCity',
11
+ 'storage-postcodeOrZIPCode'
12
+ ],
13
+ next: '/add-another-address-with-list',
14
+ continueOnEdit: true
15
+ },
16
+ '/add-another-address-with-list': {
17
+ template: 'add-another-address-loop.html',
18
+ behaviours: CombineAndLoopFields({
19
+ groupName: 'all-storage-addresses',
20
+ fieldsToGroup: [
21
+ 'storage-building',
22
+ 'storage-street',
23
+ 'storage-townOrCity',
24
+ 'storage-postcodeOrZIPCode'
25
+ ],
26
+ removePrefix: 'storage-',
27
+ combineValuesToSingleField: 'address',
28
+ groupOptional: true,
29
+ returnTo: '/add-address'
30
+ }),
31
+ next: '/confirm'
32
+ ```
33
+ Here are the fields you call this behaviour first to set config for it:
34
+ ```
35
+ `groupName`: (Required) a parent array for storing details for each object you are collecting information for,
36
+ `fieldsToGroup`: (Required) the fields being specified for an object, e.g. house number, street, postcode, that are grouped together,
37
+ `removePrefix`: (Optional) a string which is used to remove consistent prefixes from a collection of fields that are grouped together,
38
+ `combineValuesToSingleField`: (Optional) a new field that is created with its value being the concatenation of values of the fields specified in `fieldsToGroup`,
39
+ `groupOptional`: (Optional) set this to true if you want to land on the radio button question if all records in the group are deleted after creation,
40
+ `returnTo`: (Required) the next step if you want to add another object to this group
41
+ ```
42
+ N.B. in the above example we use `continueOnEdit: true` on the individual record step (i.e. `/add-address`) to ensure we revisit the grouped fields page otherwise it will not be added to the group and the user will be returned to the 'Check Your Answers' page upon a field edit.
@@ -0,0 +1,156 @@
1
+
2
+ const _ = require('lodash');
3
+ const uuid = require('uuid').v1;
4
+ const path = require('path');
5
+ const express = require('express');
6
+
7
+ module.exports = config => {
8
+ const { returnTo, groupName, fieldsToGroup, combineValuesToSingleField, removePrefix, groupOptional } = config;
9
+
10
+ if (removePrefix && typeof removePrefix !== 'string') {
11
+ throw new Error('removePrefix is a string and is optional for loops');
12
+ }
13
+
14
+ if (combineValuesToSingleField && typeof combineValuesToSingleField !== 'string') {
15
+ throw new Error('combineValuesToSingleField is a string and is optional for loops');
16
+ }
17
+
18
+ if (!returnTo || typeof returnTo !== 'string') {
19
+ throw new Error('returnTo is a string and is required for loops');
20
+ }
21
+
22
+ if (!groupName || typeof groupName !== 'string') {
23
+ throw new Error('groupName is a string and is required for loops');
24
+ }
25
+
26
+ if (!fieldsToGroup ||
27
+ !fieldsToGroup.length ||
28
+ !Array.isArray(fieldsToGroup) ||
29
+ _.some(fieldsToGroup, field => typeof field !== 'string')) {
30
+ throw new Error('fieldsToGroup is an array of strings and is required for loops');
31
+ }
32
+
33
+ return superclass => class extends superclass {
34
+ get(req, res, next) {
35
+ if (req.query.delete) {
36
+ const router = express.Router({ mergeParams: true });
37
+ router.use([
38
+ // eslint-disable-next-line no-underscore-dangle
39
+ this._configure.bind(this),
40
+ this.removeItem.bind(this),
41
+ this.reload.bind(this)
42
+ ]);
43
+ return router.handle(req, res, next);
44
+ }
45
+ return super.get(req, res, next);
46
+ }
47
+
48
+ getLoopFields(req) {
49
+ let loopedFields = _.pick(req.sessionModel.toJSON(), fieldsToGroup);
50
+
51
+ if (removePrefix) {
52
+ loopedFields = _.mapKeys(loopedFields, (value, key) => key.replace(removePrefix, ''));
53
+ }
54
+ return loopedFields;
55
+ }
56
+
57
+ removeItem(req, res, next) {
58
+ const id = req.query.delete;
59
+ const items = req.sessionModel.get(groupName).filter(item => item.id !== id);
60
+ req.sessionModel.set(groupName, items);
61
+ next();
62
+ }
63
+
64
+ // eslint-disable-next-line no-unused-vars
65
+ reload(req, res, next) {
66
+ const items = req.sessionModel.get(groupName);
67
+ if (!items.length) {
68
+ req.sessionModel.set(`${groupName}-saved`, false);
69
+ fieldsToGroup.forEach(field => {
70
+ req.sessionModel.unset(field);
71
+ });
72
+ }
73
+
74
+ const target = (items.length || groupOptional) ? req.form.options.route : returnTo;
75
+ const action = req.params.action || '';
76
+ res.redirect(path.join(req.baseUrl, target, action));
77
+ }
78
+
79
+ configure(req, res, next) {
80
+ const field = `${groupName}-add-another`;
81
+ // add yes/no field
82
+ req.form.options.fields[field] = Object.assign({
83
+ mixin: 'radio-group',
84
+ validate: ['required'],
85
+ options: [
86
+ 'yes', 'no'
87
+ ],
88
+ legend: {
89
+ className: 'visuallyhidden'
90
+ }
91
+ }, req.form.options.fieldSettings);
92
+
93
+ // add conditonal fork
94
+ req.form.options.forks = req.form.options.forks || [];
95
+ req.form.options.forks.push({
96
+ target: returnTo,
97
+ continueOnEdit: true,
98
+ condition: {
99
+ field: field,
100
+ value: 'yes'
101
+ }
102
+ });
103
+ next();
104
+ }
105
+
106
+ getValues(req, res, next) {
107
+ const fieldsGroup = req.sessionModel.get(groupName) || [];
108
+ const added = req.sessionModel.get(`${groupName}-saved`);
109
+ return super.getValues(req, res, (err, values) => {
110
+ if (err) {
111
+ return next(err);
112
+ }
113
+ if (!added) {
114
+ const fields = this.getLoopFields(req);
115
+ if (!_.isEmpty(fields)) {
116
+ const newField = Object.assign({id: uuid()}, fields);
117
+
118
+ if (combineValuesToSingleField) {
119
+ const combinedValues = _.filter(fieldsToGroup.map(field => req.sessionModel.get(field))).join(', ');
120
+ newField[combineValuesToSingleField] = combinedValues;
121
+ }
122
+
123
+ fieldsGroup.push(newField);
124
+ values[groupName] = fieldsGroup;
125
+ fieldsToGroup.forEach(field => req.sessionModel.unset(field));
126
+
127
+ req.sessionModel.set(groupName, fieldsGroup);
128
+ req.sessionModel.set(`${groupName}-saved`, true);
129
+ }
130
+ }
131
+ return next(null, values);
132
+ });
133
+ }
134
+
135
+ locals(req, res) {
136
+ const items = req.form.values[groupName] || [];
137
+ return Object.assign({}, super.locals(req, res), {
138
+ items,
139
+ hasItems: items.length > 0,
140
+ field: groupName
141
+ });
142
+ }
143
+
144
+ saveValues(req, res, next) {
145
+ // remove "yes" value from session so it is no pre-populated next time around
146
+ super.saveValues(req, res, err => {
147
+ const field = `${groupName}-add-another`;
148
+ if (req.form.values[field] === 'yes') {
149
+ req.sessionModel.unset(field);
150
+ req.sessionModel.set(`${groupName}-saved`, false);
151
+ }
152
+ next(err);
153
+ });
154
+ }
155
+ };
156
+ };
@@ -40,6 +40,7 @@ const conditionalTranslate = (key, translate) => {
40
40
  };
41
41
 
42
42
  const getLegendClassName = field => field && field.legend && field.legend.className || '';
43
+ const getIsPageHeading = field => field && field.isPageHeading || '';
43
44
 
44
45
  module.exports = (key, opts) => {
45
46
  if (!key) {
@@ -144,8 +145,9 @@ module.exports = (key, opts) => {
144
145
  const legend = conditionalTranslate(`fields.${key}.legend`, req.translate);
145
146
  const hint = conditionalTranslate(`fields.${key}.hint`, req.translate);
146
147
  const legendClassName = getLegendClassName(options);
148
+ const isPageHeading = getIsPageHeading(options);
147
149
  const error = req.form.errors && req.form.errors[key];
148
- res.render(template, { key, legend, legendClassName, hint, error }, (err, html) => {
150
+ res.render(template, { key, legend, legendClassName, isPageHeading, hint, error }, (err, html) => {
149
151
  if (err) {
150
152
  next(err);
151
153
  } else {
@@ -1,17 +1,20 @@
1
- <fieldset id="{{key}}-group" class="{{#className}}{{className}}{{/className}} {{#error}}validation-error{{/error}}">
2
- <legend>
3
- {{#error}}
4
- <span id="{{key}}-error" class="error-message">{{error.message}}</span>
5
- {{/error}}
6
- <span{{#legendClassName}} class="{{legendClassName}}"{{/legendClassName}}>{{legend}}</span>
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>
7
8
  {{#hint}}
8
- <span id="{{key}}-hint" class="form-hint">{{hint}}</span>
9
+ <span id="{{key}}-hint" class="govuk-hint">{{hint}}</span>
9
10
  {{/hint}}
10
- </legend>
11
-
12
- <input type="hidden" name="{{key}}" />
13
-
14
- <div class="form-group form-date">
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-date-input">
15
17
  {{#input-date}}{{key}}{{/input-date}}
16
18
  </div>
17
19
  </fieldset>
20
+ </div>
@@ -0,0 +1,22 @@
1
+
2
+ const _ = require('lodash');
3
+ const componentDefaults = require('../../config/component-defaults');
4
+
5
+ module.exports = superclass => class extends superclass {
6
+ configure(req, res, next) {
7
+ const homeOfficeCountries = [''].concat(require('homeoffice-countries').allCountries);
8
+
9
+ const nationalityFields = componentDefaults.homeOfficeCountries;
10
+
11
+ nationalityFields.forEach(field => {
12
+ if (_.get(req, `form.options.fields[${field}].options`)) {
13
+ req.form.options.fields[field].options = homeOfficeCountries.map(country => {
14
+ const labelString = country !== '' ? country : 'Please select a country';
15
+ return { label: labelString, value: country };
16
+ });
17
+ }
18
+ });
19
+
20
+ next();
21
+ }
22
+ };
@@ -3,8 +3,10 @@
3
3
  module.exports = {
4
4
  addressLookup: require('./address-lookup'),
5
5
  clearSession: require('./clear-session'),
6
+ combineAndLoopFields: require('./combine-and-loop-fields'),
6
7
  date: require('./date'),
7
8
  emailer: require('./emailer'),
9
+ homeOfficeCountries: require('./homeoffice-countries'),
8
10
  notify: require('./notify'),
9
11
  summary: require('./summary')
10
12
  };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  const NotifyClient = require('notifications-node-client').NotifyClient;
3
- const uuid = require('uuid');
3
+ const { v4: uuidv4 } = require('uuid');
4
4
 
5
5
  module.exports = class Notify {
6
6
  constructor(opts) {
@@ -11,7 +11,7 @@ module.exports = class Notify {
11
11
  }
12
12
 
13
13
  send(email) {
14
- const reference = uuid.v1();
14
+ const reference = uuidv4();
15
15
 
16
16
  return this.notifyClient.sendEmail(this.notifyTemplate, email.recipient, {
17
17
  personalisation: {
@@ -59,7 +59,7 @@ module.exports = SuperClass => class extends SuperClass {
59
59
  fieldData.value = fieldSpec.derivation ?
60
60
  this.runCombinerForDerivedField(fieldSpec, req) : fieldData.value;
61
61
  fieldData.value = (typeof fieldSpec.parse === 'function') ?
62
- fieldSpec.parse(fieldData.value) : fieldData.value;
62
+ fieldSpec.parse(fieldData.value, req) : fieldData.value;
63
63
  }
64
64
 
65
65
  return fieldData;
@@ -121,7 +121,8 @@ module.exports = SuperClass => class extends SuperClass {
121
121
  }
122
122
 
123
123
  getStepForField(key, steps) {
124
- return Object.keys(steps).filter(step => steps[step].fields && steps[step].fields.indexOf(key) > -1)[0];
124
+ const keyName = Array.isArray(key) ? key[0] : key;
125
+ return Object.keys(steps).filter(step => steps[step].fields && steps[step].fields.indexOf(keyName) > -1)[0];
125
126
  }
126
127
 
127
128
 
@@ -12,7 +12,9 @@ module.exports = {
12
12
  src: 'assets/scss/app.scss',
13
13
  out: 'public/css/app.css',
14
14
  match: 'assets/scss/**/*.scss',
15
- restart: false
15
+ restart: false,
16
+ quietDeps: false,
17
+ outputStyle: 'expanded'
16
18
  },
17
19
  translate: {
18
20
  src: 'apps/**/translations/src',
@@ -0,0 +1,13 @@
1
+
2
+ module.exports = {
3
+ homeOfficeCountries: [
4
+ 'nationality',
5
+ 'former-nationality',
6
+ 'dual-nationality',
7
+ 'nominated-nationality',
8
+ 'nationality-error',
9
+ 'country',
10
+ 'country-of-birth',
11
+ 'someone-else-nationality'
12
+ ]
13
+ };
@@ -103,15 +103,23 @@ module.exports = class Controller extends BaseController {
103
103
  const locals = super.locals(req, res);
104
104
  const stepLocals = req.form.options.locals || {};
105
105
 
106
- const fields = _.map(req.form.options.fields, (field, key) =>
106
+ let fields = _.map(req.form.options.fields, (field, key) =>
107
107
  Object.assign({}, field, { key })
108
108
  );
109
+ // only include fields that aren't dependents to mitigate duplicate fields on the page
110
+ fields = fields.filter(field => !req.form.options.fields[field.key].dependent);
109
111
 
110
112
  return _.extend({}, locals, {
111
113
  fields,
112
114
  route,
113
115
  baseUrl: req.baseUrl,
116
+ skipToMain: this.getFirstFormItem(req.form.options.fields),
114
117
  title: this.getTitle(route, lookup, req.form.options.fields, res.locals),
118
+ journeyHeaderURL: this.getJourneyHeaderURL(req.baseUrl),
119
+ header: this.getHeader(route, lookup, res.locals),
120
+ captionHeading: this.getCaptionHeading(route, lookup, res.locals),
121
+ warning: this.getWarning(route, lookup, res.locals),
122
+ subHeading: this.getSubHeading(route, lookup, res.locals),
115
123
  intro: this.getIntro(route, lookup, res.locals),
116
124
  backLink: this.getBackLink(req, res),
117
125
  nextPage: this.getNextStep(req, res),
@@ -119,6 +127,34 @@ module.exports = class Controller extends BaseController {
119
127
  }, stepLocals);
120
128
  }
121
129
 
130
+ getJourneyHeaderURL(url) {
131
+ return url === '' ? '/' : url;
132
+ }
133
+
134
+ getFirstFormItem(fields) {
135
+ let firstFieldKey;
136
+ if (_.size(fields)) {
137
+ firstFieldKey = Object.keys(fields)[0];
138
+ }
139
+ return firstFieldKey | 'main-content';
140
+ }
141
+
142
+ getHeader(route, lookup, locals) {
143
+ return lookup(`pages.${route}.header`, locals);
144
+ }
145
+
146
+ getCaptionHeading(route, lookup, locals) {
147
+ return lookup(`pages.${route}.captionHeading`, locals);
148
+ }
149
+
150
+ getSubHeading(route, lookup, locals) {
151
+ return lookup(`pages.${route}.subHeading`, locals);
152
+ }
153
+
154
+ getWarning(route, lookup, locals) {
155
+ return lookup(`pages.${route}.warning`, locals);
156
+ }
157
+
122
158
  getTitle(route, lookup, fields, locals) {
123
159
  let fieldName = '';
124
160
  if (_.size(fields)) {
@@ -140,6 +176,26 @@ module.exports = class Controller extends BaseController {
140
176
  _getErrors(req, res, callback) {
141
177
  super._getErrors(req, res, () => {
142
178
  Object.keys(req.form.errors).forEach(key => {
179
+ if (req.form && req.form.options && req.form.options.fields) {
180
+ const field = req.form.options.fields[key];
181
+ // get first option for radios and checkbox
182
+ if (field.mixin === 'radio-group' || field.mixin === 'checkbox-group') {
183
+ // get first option for radios and checkbox where there is a toggle
184
+ if(typeof field.options[0] === 'object') {
185
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0].value;
186
+ } else {
187
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0];
188
+ }
189
+ // eslint-disable-next-line brace-style
190
+ }
191
+ // get first field for date input control
192
+ else if (field && field.mixin === 'input-date') {
193
+ req.form.errors[key].errorLinkId = key + '-day';
194
+ } else {
195
+ req.form.errors[key].errorLinkId = key;
196
+ }
197
+ }
198
+
143
199
  req.form.errors[key].message = this.getErrorMessage(req.form.errors[key], req, res);
144
200
  });
145
201
  callback();
@@ -53,6 +53,18 @@ module.exports = {
53
53
 
54
54
  base64decode(value) {
55
55
  return Buffer.from(value, 'base64').toString();
56
+ },
57
+
58
+ ukPostcode(value) {
59
+ if (typeof value !== 'string' || value === '') {
60
+ return value;
61
+ }
62
+
63
+ const postcode = this.uppercase(this.removespaces(value));
64
+ const firstPart = postcode.slice(0, -3);
65
+ const secondPart = postcode.slice(-3);
66
+
67
+ return `${firstPart} ${secondPart}`;
56
68
  }
57
69
 
58
70
  };
@@ -63,7 +63,8 @@ function validate(fields) {
63
63
  debug(`Validating field: "${key}" with value: "${value}"`);
64
64
 
65
65
  function shouldValidate() {
66
- let dependent = fields[key].dependent;
66
+ // validationLink used to validates multiple dependent fields in checkbox-group
67
+ let dependent = fields[key].dependent || fields[key].validationLink;
67
68
 
68
69
  if (typeof dependent === 'string') {
69
70
  dependent = {
@@ -62,6 +62,10 @@ module.exports = Validators = {
62
62
  return Validators.string(value) && (value === '' || value.length <= length);
63
63
  },
64
64
 
65
+ maxword(value, length) {
66
+ return Validators.string(value) && (value === '' || value.split(/\s+/).length <= length);
67
+ },
68
+
65
69
  exactlength(value, length) {
66
70
  return Validators.string(value) && (value === '' || value.length === length);
67
71
  },
@@ -5,6 +5,7 @@ module.exports = {
5
5
  assetPath: '{{govukAssetPath}}',
6
6
  afterHeader: '{{$afterHeader}}{{/afterHeader}}',
7
7
  bodyClasses: '{{$bodyClasses}}{{/bodyClasses}}',
8
+ bodyStart: '{{$bodyStart}}{{/bodyStart}}',
8
9
  bodyEnd: '{{$bodyEnd}}{{/bodyEnd}}',
9
10
  content: '{{$main}}{{/main}}',
10
11
  cookieMessage: '{{$cookieMessage}}{{/cookieMessage}}',
@@ -17,8 +18,7 @@ module.exports = {
17
18
  insideHeader: '{{$insideHeader}}{{/insideHeader}}',
18
19
  pageTitle: '{{$pageTitle}}{{/pageTitle}}',
19
20
  propositionHeader: '{{$propositionHeader}}{{/propositionHeader}}',
20
- skipLinkMessage: '{{$skipLinkMessage}}Skip to main content{{/skipLinkMessage}}',
21
21
  globalHeaderText: '{{$globalHeaderText}}GOV.UK{{/globalHeaderText}}',
22
- licenceMessage: '{{$licenceMessage}}<p>All content is available under the <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" rel="license">Open Government Licence v3.0</a>, except where otherwise stated</p>{{/licenceMessage}}',
22
+ licenceMessage: '{{$licenceMessage}}All content is available under the <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" id="open-government-licence" class="govuk-footer__link" target="_blank" rel="license">Open Government Licence v3.0</a>, except where otherwise stated{{/licenceMessage}}',
23
23
  crownCopyrightMessage: '{{$crownCopyrightMessage}}© Crown copyright{{/crownCopyrightMessage}}'
24
24
  };
@@ -0,0 +1,104 @@
1
+
2
+ <!DOCTYPE html>
3
+ <!--[if lt IE 9]><html class="lte-ie8" lang="{{ htmlLang }}"><![endif]-->
4
+ <!--[if gt IE 8]><!--><html lang="{{ htmlLang }}" class="govuk-template"><!--<![endif]-->
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <title>{{ pageTitle }}</title>
8
+
9
+ <link rel="shortcut icon" sizes="16x16 32x32 48x48" href="{{assetPath}}images/favicon.ico" type="image/x-icon">
10
+ <link rel="mask-icon" href="{{assetPath}}images/govuk-mask-icon.svg" color="#0b0c0c">
11
+ <link rel="apple-touch-icon" sizes="180x180" href="{{assetPath}}images/govuk-apple-touch-icon-180x180.png">
12
+ <link rel="apple-touch-icon" sizes="167x167" href="{{assetPath}}images/govuk-apple-touch-icon-167x167.png">
13
+ <link rel="apple-touch-icon" sizes="152x152" href="{{assetPath}}images/govuk-apple-touch-icon-152x152.png">
14
+ <link rel="apple-touch-icon" href="{{assetPath}}images/govuk-apple-touch-icon.png">
15
+
16
+
17
+ <meta name="theme-color" content="#0b0c0c" />
18
+
19
+ <meta name="viewport" content="width=device-width, initial-scale=1">
20
+
21
+ {{{ head }}}
22
+
23
+
24
+ <meta property="og:image" content="{{assetPath}}images/opengraph-image.png">
25
+ </head>
26
+
27
+ <body class="{{ bodyClasses }} govuk-template__body js-enabled" >
28
+ <script>document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
29
+
30
+
31
+
32
+ <div id="global-cookie-message" class="gem-c-cookie-banner govuk-clearfix" data-module="cookie-banner" role="region" aria-label="cookie banner" data-nosnippet="">
33
+ {{{ cookieMessage }}}
34
+ </div>
35
+
36
+ {{{ bodyStart }}}
37
+
38
+ <header role="banner" id="govuk-header" class="{{{ headerClass }}}">
39
+ <div class="govuk-header__container govuk-width-container">
40
+
41
+ <div class="govuk-header__logo">
42
+ <a href="{{{ homepageUrl }}}" title="{{ logoLinkTitle }}" id="logo" class="govuk-header__link govuk-header__link--homepage" target="_blank" data-module="track-click" data-track-category="homeLinkClicked" data-track-action="homeHeader">
43
+ <span class="govuk-header__logotype">
44
+ <!--[if gt IE 8]><!-->
45
+ <svg aria-hidden="true" focusable="false" class="govuk-header__logotype-crown" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132 97" height="30" width="36">
46
+ <path fill="currentColor" fill-rule="evenodd" d="M25 30.2c3.5 1.5 7.7-.2 9.1-3.7 1.5-3.6-.2-7.8-3.9-9.2-3.6-1.4-7.6.3-9.1 3.9-1.4 3.5.3 7.5 3.9 9zM9 39.5c3.6 1.5 7.8-.2 9.2-3.7 1.5-3.6-.2-7.8-3.9-9.1-3.6-1.5-7.6.2-9.1 3.8-1.4 3.5.3 7.5 3.8 9zM4.4 57.2c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.5-1.5-7.6.3-9.1 3.8-1.4 3.5.3 7.6 3.9 9.1zm38.3-21.4c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.6-1.5-7.6.3-9.1 3.8-1.3 3.6.4 7.7 3.9 9.1zm64.4-5.6c-3.6 1.5-7.8-.2-9.1-3.7-1.5-3.6.2-7.8 3.8-9.2 3.6-1.4 7.7.3 9.2 3.9 1.3 3.5-.4 7.5-3.9 9zm15.9 9.3c-3.6 1.5-7.7-.2-9.1-3.7-1.5-3.6.2-7.8 3.7-9.1 3.6-1.5 7.7.2 9.2 3.8 1.5 3.5-.3 7.5-3.8 9zm4.7 17.7c-3.6 1.5-7.8-.2-9.2-3.8-1.5-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.3 3.5-.4 7.6-3.9 9.1zM89.3 35.8c-3.6 1.5-7.8-.2-9.2-3.8-1.4-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.4 3.6-.3 7.7-3.9 9.1zM69.7 17.7l8.9 4.7V9.3l-8.9 2.8c-.2-.3-.5-.6-.9-.9L72.4 0H59.6l3.5 11.2c-.3.3-.6.5-.9.9l-8.8-2.8v13.1l8.8-4.7c.3.3.6.7.9.9l-5 15.4v.1c-.2.8-.4 1.6-.4 2.4 0 4.1 3.1 7.5 7 8.1h.2c.3 0 .7.1 1 .1.4 0 .7 0 1-.1h.2c4-.6 7.1-4.1 7.1-8.1 0-.8-.1-1.7-.4-2.4V34l-5.1-15.4c.4-.2.7-.6 1-.9zM66 92.8c16.9 0 32.8 1.1 47.1 3.2 4-16.9 8.9-26.7 14-33.5l-9.6-3.4c1 4.9 1.1 7.2 0 10.2-1.5-1.4-3-4.3-4.2-8.7L108.6 76c2.8-2 5-3.2 7.5-3.3-4.4 9.4-10 11.9-13.6 11.2-4.3-.8-6.3-4.6-5.6-7.9 1-4.7 5.7-5.9 8-.5 4.3-8.7-3-11.4-7.6-8.8 7.1-7.2 7.9-13.5 2.1-21.1-8 6.1-8.1 12.3-4.5 20.8-4.7-5.4-12.1-2.5-9.5 6.2 3.4-5.2 7.9-2 7.2 3.1-.6 4.3-6.4 7.8-13.5 7.2-10.3-.9-10.9-8-11.2-13.8 2.5-.5 7.1 1.8 11 7.3L80.2 60c-4.1 4.4-8 5.3-12.3 5.4 1.4-4.4 8-11.6 8-11.6H55.5s6.4 7.2 7.9 11.6c-4.2-.1-8-1-12.3-5.4l1.4 16.4c3.9-5.5 8.5-7.7 10.9-7.3-.3 5.8-.9 12.8-11.1 13.8-7.2.6-12.9-2.9-13.5-7.2-.7-5 3.8-8.3 7.1-3.1 2.7-8.7-4.6-11.6-9.4-6.2 3.7-8.5 3.6-14.7-4.6-20.8-5.8 7.6-5 13.9 2.2 21.1-4.7-2.6-11.9.1-7.7 8.8 2.3-5.5 7.1-4.2 8.1.5.7 3.3-1.3 7.1-5.7 7.9-3.5.7-9-1.8-13.5-11.2 2.5.1 4.7 1.3 7.5 3.3l-4.7-15.4c-1.2 4.4-2.7 7.2-4.3 8.7-1.1-3-.9-5.3 0-10.2l-9.5 3.4c5 6.9 9.9 16.7 14 33.5 14.8-2.1 30.8-3.2 47.7-3.2z"></path>
47
+ </svg>
48
+ <!--<![endif]-->
49
+ <!--<![endif]-->
50
+ <!--[if IE 8]>
51
+ <img src="{{ assetPath }}images/govuk-logotype-crown.png" class="govuk-header__logotype-crown-fallback-image" width="36" height="32">
52
+ <![endif]-->
53
+ </span>
54
+ <span class="govuk-header__logotype-text">
55
+ {{{ globalHeaderText }}}
56
+ </span>
57
+ </a>
58
+ </div>
59
+ {{{ insideHeader }}}
60
+
61
+ {{{ propositionHeader }}}
62
+ </div>
63
+ </header>
64
+
65
+
66
+ {{{ afterHeader }}}
67
+
68
+
69
+ {{{ content }}}
70
+
71
+ <footer class="govuk-footer" id="footer" role="contentinfo">
72
+
73
+ <div class="govuk-width-container">
74
+ {{{ footerTop }}}
75
+
76
+ <div class="govuk-footer__meta">
77
+ <div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
78
+ <h2 class="govuk-visually-hidden">Support links</h2>
79
+ {{{ footerSupportLinks }}}
80
+
81
+ <svg aria-hidden="true" focusable="false" class="govuk-footer__licence-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 483.2 195.7" height="17" width="41">
82
+ <path fill="currentColor" d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145"></path>
83
+ </svg>
84
+
85
+ <span class="govuk-footer__licence-description">{{{ licenceMessage }}}</span>
86
+ </div>
87
+
88
+ <div class="govuk-footer__meta-item">
89
+ <a class="govuk-footer__link govuk-footer__copyright-logo" id="copyright-logo" target="_blank" href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">{{{ crownCopyrightMessage }}}</a>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </footer>
94
+
95
+ <div id="global-app-error" class="app-error hidden"></div>
96
+
97
+
98
+ {{{ bodyEnd }}}
99
+
100
+
101
+ <script {{#nonce}}nonce="{{nonce}}"{{/nonce}}>if (typeof window.GOVUK === 'undefined') document.body.className = document.body.className.replace('js-enabled', '');</script>
102
+
103
+ </body>
104
+ </html>
@@ -12,12 +12,12 @@ function addNonceValueAttributeToInlineScripts(compiledTemplateString) {
12
12
  }
13
13
 
14
14
  module.exports = () => {
15
- const template = require.resolve('govuk_template_mustache/views/layouts/govuk_template.html');
15
+ const template = require.resolve('./govuk_template.html');
16
16
 
17
17
  const govukTemplate = fs.readFileSync(template, { encoding: 'utf-8' });
18
18
  const compiledTemplate = Hogan.compile(govukTemplate).render(govukConfig);
19
19
  const parsedTemplate = addNonceValueAttributeToInlineScripts(compiledTemplate);
20
- const output = path.resolve(__dirname, '../govuk_template.html');
20
+ const output = path.resolve(__dirname, '../govuk_template_generated.html');
21
21
 
22
22
  fs.writeFileSync(output, parsedTemplate, { encoding: 'utf-8' });
23
23
  };