hof 20.0.0-beta.9 → 20.0.0

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 (44) hide show
  1. package/.nyc_output/7c548a7f-5c40-44b2-b9fb-648341a23d6f.json +1 -0
  2. package/.nyc_output/processinfo/7c548a7f-5c40-44b2-b9fb-648341a23d6f.json +1 -0
  3. package/.nyc_output/processinfo/index.json +1 -1
  4. package/README.md +335 -256
  5. package/components/index.js +2 -1
  6. package/components/notify/index.js +60 -0
  7. package/components/notify/notify.js +25 -0
  8. package/config/sanitisation-rules.js +20 -17
  9. package/controller/base-controller.js +5 -3
  10. package/controller/controller.js +19 -4
  11. package/frontend/template-mixins/mixins/template-mixins.js +7 -3
  12. package/frontend/template-mixins/partials/forms/checkbox-group.html +10 -1
  13. package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
  14. package/frontend/template-mixins/partials/forms/input-text-group.html +5 -3
  15. package/frontend/template-mixins/partials/forms/option-group.html +9 -0
  16. package/frontend/template-mixins/partials/forms/select.html +1 -1
  17. package/frontend/template-mixins/partials/forms/textarea-group.html +2 -2
  18. package/frontend/template-partials/views/layout.html +10 -3
  19. package/frontend/template-partials/views/partials/cookie-banner.html +1 -1
  20. package/frontend/template-partials/views/partials/form.html +2 -1
  21. package/frontend/template-partials/views/partials/maincontent-left.html +2 -2
  22. package/frontend/template-partials/views/partials/navigation.html +2 -2
  23. package/frontend/template-partials/views/partials/summary-table-row.html +2 -2
  24. package/frontend/template-partials/views/partials/warn.html +7 -0
  25. package/frontend/template-partials/views/session-timeout.html +2 -1
  26. package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
  27. package/frontend/themes/gov-uk/styles/modules/_validation.scss +2 -2
  28. package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
  29. package/frontend/toolkit/assets/javascript/progressive-reveal.js +3 -1
  30. package/frontend/toolkit/assets/javascript/validation.js +6 -1
  31. package/index.js +1 -1
  32. package/middleware/errors.js +2 -0
  33. package/package.json +4 -2
  34. package/sandbox/apps/sandbox/fields.js +1 -0
  35. package/sandbox/apps/sandbox/index.js +1 -5
  36. package/sandbox/assets/scss/app.scss +0 -52
  37. package/sandbox/package.json +2 -0
  38. package/sandbox/public/css/app.css +4938 -4990
  39. package/sandbox/public/js/bundle.js +79 -65
  40. package/sandbox/server.js +2 -1
  41. package/sandbox/yarn.lock +39 -564
  42. package/wizard/middleware/check-progress.js +36 -1
  43. package/.nyc_output/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +0 -1
  44. package/.nyc_output/processinfo/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +0 -1
@@ -5,5 +5,6 @@ module.exports = {
5
5
  clearSession: require('./clear-session'),
6
6
  date: require('./date'),
7
7
  emailer: require('./emailer'),
8
- summary: require('./summary')
8
+ summary: require('./summary'),
9
+ notify: require('./notify')
9
10
  };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const Notify = require('./notify');
4
+ const Hogan = require('hogan.js');
5
+ const fs = require('fs');
6
+
7
+ module.exports = config => {
8
+ const notify = new Notify(config);
9
+ config.parse = config.parse || (data => data);
10
+
11
+ if (!config.recipient) {
12
+ throw new Error('Email recipient must be defined');
13
+ }
14
+ if (typeof config.template !== 'string') {
15
+ throw new Error('Email template must be defined');
16
+ }
17
+
18
+ return superclass => class NotifyBehaviour extends superclass {
19
+ successHandler(req, res, next) {
20
+ Promise.resolve()
21
+ .then(() => {
22
+ return new Promise((resolve, reject) => {
23
+ fs.readFile(config.template, (err, template) => err ? reject(err) : resolve(template.toString('utf8')));
24
+ });
25
+ })
26
+ .then(template => {
27
+ const data = config.parse(req.sessionModel.toJSON(), req.translate);
28
+ return Hogan.compile(template).render(data);
29
+ })
30
+ .then(body => {
31
+ const settings = { body };
32
+
33
+ if (typeof config.recipient === 'function') {
34
+ settings.recipient = config.recipient(req.sessionModel.toJSON());
35
+ } else {
36
+ settings.recipient = req.sessionModel.get(config.recipient) || config.recipient;
37
+ }
38
+ if (typeof settings.recipient !== 'string' || !settings.recipient.includes('@')) {
39
+ throw new Error('hof-behaviour-emailer: invalid recipient');
40
+ }
41
+
42
+ if (typeof config.subject === 'function') {
43
+ settings.subject = config.subject(req.sessionModel.toJSON(), req.translate);
44
+ } else {
45
+ settings.subject = config.subject;
46
+ }
47
+
48
+ return settings;
49
+ })
50
+ .then(settings => {
51
+ return notify.send(settings);
52
+ })
53
+ .then(() => {
54
+ super.successHandler(req, res, next);
55
+ }, next);
56
+ }
57
+ };
58
+ };
59
+
60
+ module.exports.Notify = Notify;
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+ const NotifyClient = require('notifications-node-client').NotifyClient;
3
+ const { v4: uuidv4 } = require('uuid');
4
+
5
+ module.exports = class Notify {
6
+ constructor(opts) {
7
+ const options = opts || {};
8
+ this.options = options;
9
+ this.notifyClient = new NotifyClient(options.notifyApiKey);
10
+ this.notifyTemplate = options.notifyTemplate;
11
+ }
12
+
13
+ send(email) {
14
+ const reference = uuidv4();
15
+
16
+ return this.notifyClient.sendEmail(this.notifyTemplate, email.recipient, {
17
+ personalisation: {
18
+ 'email-subject': email.subject,
19
+ 'email-body': email.body
20
+ },
21
+ reference });
22
+ }
23
+ };
24
+
25
+ module.exports.NotifyClient = NotifyClient;
@@ -3,27 +3,30 @@
3
3
 
4
4
  const sanitisationBlacklistArray = {
5
5
  // Input will be sanitised using the below rules
6
+ // Each one is an array which will run sequentially
7
+ // Some have multiple steps which allow us to remove dups then append the suffix
8
+ // Useful for ensuring we're not re-sanitising the same input multiple times (and appending extra hypens each time)
6
9
  // The key is what we're sanitising out
7
10
  // The regex is the rule we used to find them (note some dictate repeating characters)
8
11
  // And the replace is what we're replacing that pattern with. Usually nothing sometimes a
9
12
  // single character or sometimes a single character followed by a "-"
10
- '/*': { regex: '\/\\*', replace: '-' },
11
- '*/': { regex: '\\*\\/', replace: '-' },
12
- '|': { regex: '\\|', replace: '-' },
13
- '&&': { regex: '&&+', replace: '&' },
14
- '@@': { regex: '@@+', replace: '@' },
15
- '/..;/': { regex: '/\\.\\.;/', replace: '-' }, // Purposely input before ".." as they conflict
16
- // '..': { regex: '\\.\\.+', replace: '.' }, // Agreed to disable this rule for now unless its specifically required
17
- '/etc/passwd': { regex: '\/etc\/passwd', replace: '-' },
18
- 'c:\\': { regex: 'c:\\\\', replace: '-' },
19
- 'cmd.exe': { regex: 'cmd\\.exe', replace: '-' },
20
- '<': { regex: '<', replace: '<-' },
21
- '>': { regex: '>', replace: '>-' },
22
- '[': { regex: '\\[+', replace: '[-' },
23
- ']': { regex: '\\]+', replace: ']-' },
24
- '~': { regex: '~', replace: '~-' },
25
- '&#': { regex: '&#', replace: '-' },
26
- '%U': { regex: '%U', replace: '-' }
13
+ '/*': [{ regex: '\/\\*', replace: '-' }],
14
+ '*/': [{ regex: '\\*\\/', replace: '-' }],
15
+ '|': [{ regex: '\\|', replace: '-' }],
16
+ '&&': [{ regex: '&&+', replace: '&' }],
17
+ '@@': [{ regex: '@@+', replace: '@' }],
18
+ '/..;/': [{ regex: '/\\.\\.;/', replace: '-' }], // Purposely input before ".." as they conflict
19
+ // '..': [{ regex: '\\.\\.+', replace: '.' }], // Agreed to disable this rule for now unless its specifically required
20
+ '/etc/passwd': [{ regex: '\/etc\/passwd', replace: '-' }],
21
+ 'c:\\': [{ regex: 'c:\\\\', replace: '-' }],
22
+ 'cmd.exe': [{ regex: 'cmd\\.exe', replace: '-' }],
23
+ '<': [{ regex: '<+', replace: '<' }, { regex: '<(?!-)', replace: '<-' }],
24
+ '>': [{ regex: '>+', replace: '>' }, { regex: '>(?!-)', replace: '>-' }],
25
+ '[': [{ regex: '\\[+', replace: '[' }, { regex: '\\[(?!-)', replace: '[-' }],
26
+ ']': [{ regex: '\\]+', replace: ']-' }, { regex: '\\](?!-)', replace: ']-' }],
27
+ '~': [{ regex: '~+', replace: '~' }, { regex: '~(?!-)', replace: '~-' }],
28
+ '&#': [{ regex: '&#', replace: '-' }],
29
+ '%U': [{ regex: '%U', replace: '-' }]
27
30
  };
28
31
 
29
32
  module.exports = sanitisationBlacklistArray;
@@ -177,9 +177,11 @@ module.exports = class BaseController extends EventEmitter {
177
177
  // For each property in our form data
178
178
  Object.keys(sanitisationBlacklistArray).forEach(function (blacklisted, blacklistedIndex) {
179
179
  const blacklistedDetail = sanitisationBlacklistArray[blacklisted];
180
- const regexQuery = new RegExp(blacklistedDetail.regex, 'gi');
181
- // Will perform the required replace based on our passed in regex and the replace string
182
- req.form.values[property] = req.form.values[property].replace(regexQuery, blacklistedDetail.replace);
180
+ blacklistedDetail.forEach(step => {
181
+ const regexQuery = new RegExp(step.regex, 'gi');
182
+ // Will perform the required replace based on our passed in regex and the replace string
183
+ req.form.values[property] = req.form.values[property].replace(regexQuery, step.replace);
184
+ });
183
185
  });
184
186
  }
185
187
  });
@@ -113,8 +113,10 @@ module.exports = class Controller extends BaseController {
113
113
  baseUrl: req.baseUrl,
114
114
  skipToMain: this.getFirstFormItem(req.form.options.fields),
115
115
  title: this.getTitle(route, lookup, req.form.options.fields, res.locals),
116
+ journeyHeaderURL: this.getJourneyHeaderURL(req.baseUrl),
116
117
  header: this.getHeader(route, lookup, res.locals),
117
118
  captionHeading: this.getCaptionHeading(route, lookup, res.locals),
119
+ warning: this.getWarning(route, lookup, res.locals),
118
120
  subHeading: this.getSubHeading(route, lookup, res.locals),
119
121
  intro: this.getIntro(route, lookup, res.locals),
120
122
  backLink: this.getBackLink(req, res),
@@ -123,6 +125,10 @@ module.exports = class Controller extends BaseController {
123
125
  }, stepLocals);
124
126
  }
125
127
 
128
+ getJourneyHeaderURL(url) {
129
+ return url === '' ? '/' : url;
130
+ }
131
+
126
132
  getFirstFormItem(fields) {
127
133
  let firstFieldKey;
128
134
  if (_.size(fields)) {
@@ -143,6 +149,10 @@ module.exports = class Controller extends BaseController {
143
149
  return lookup(`pages.${route}.subHeading`, locals);
144
150
  }
145
151
 
152
+ getWarning(route, lookup, locals) {
153
+ return lookup(`pages.${route}.warning`, locals);
154
+ }
155
+
146
156
  getTitle(route, lookup, fields, locals) {
147
157
  let fieldName = '';
148
158
  if (_.size(fields)) {
@@ -166,13 +176,18 @@ module.exports = class Controller extends BaseController {
166
176
  Object.keys(req.form.errors).forEach(key => {
167
177
  if (req.form && req.form.options && req.form.options.fields) {
168
178
  const field = req.form.options.fields[key];
169
- // get first option for radios
170
- if (field.mixin === 'radio-group') {
171
- req.form.errors[key].errorLinkId = key + '-' + field.options[0];
179
+ // get first option for radios and checkbox
180
+ if (field.mixin === 'radio-group' || field.mixin === 'checkbox-group') {
181
+ // get first option for radios and checkbox where there is a toggle
182
+ if(typeof field.options[0] === 'object') {
183
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0].value;
184
+ } else {
185
+ req.form.errors[key].errorLinkId = key + '-' + field.options[0];
186
+ }
172
187
  // eslint-disable-next-line brace-style
173
188
  }
174
189
  // get first field for date input control
175
- else if (field && field.controlType === 'date-input') {
190
+ else if (field && field.mixin === 'input-date') {
176
191
  req.form.errors[key].errorLinkId = key + '-day';
177
192
  } else {
178
193
  req.form.errors[key].errorLinkId = key;
@@ -215,6 +215,7 @@ module.exports = function (options) {
215
215
  child: field.child,
216
216
  isPageHeading: field.isPageHeading,
217
217
  attributes: field.attributes,
218
+ isPrefixOrSuffix: _.map(field.attributes, item => {if (item.prefix || item.suffix !== undefined) return true;}),
218
219
  renderChild: renderChild.bind(this)
219
220
  });
220
221
  }
@@ -224,6 +225,7 @@ module.exports = function (options) {
224
225
  const field = Object.assign({}, this.options.fields[key] || options.fields[key]);
225
226
  const legend = field.legend;
226
227
  const detail = field.detail;
228
+ const warningValue = 'fields.' + key + '.warning';
227
229
  let legendClassName;
228
230
  let legendValue = 'fields.' + key + '.legend';
229
231
  if (legend) {
@@ -241,6 +243,8 @@ module.exports = function (options) {
241
243
  legendClassName: legendClassName,
242
244
  role: opts.type === 'radio' ? 'radiogroup' : 'group',
243
245
  isPageHeading: field.isPageHeading,
246
+ isWarning: field.isWarning,
247
+ warning: t(warningValue),
244
248
  detail: detail ? detail : '',
245
249
  hint: conditionalTranslate(getTranslationKey(field, key, 'hint')),
246
250
  options: _.map(field.options, function (obj) {
@@ -303,7 +307,7 @@ module.exports = function (options) {
303
307
  invalid: this.errors && this.errors[key] && opts.required,
304
308
  label: t(fieldLabel || 'fields.' + key + '.label'),
305
309
  selected: selected,
306
- className: classNames(field) || 'block-label',
310
+ className: classNames(field) || 'govuk-label govuk-checkboxes__label',
307
311
  child: field.child,
308
312
  renderChild: renderChild.bind(this)
309
313
  });
@@ -444,8 +448,8 @@ module.exports = function (options) {
444
448
  parts.push(dayPart);
445
449
  }
446
450
 
447
- const monthPart = compiled['partials/forms/input-text-date'].render(inputText.call(this, key + '-month', { pattern: '[0-9]*', min: 1, max: 12, maxlength: 2, hintId: key + '-hint', date: true, autocomplete: autocomplete.month, formGroupClassName, className: classNameMonth, isRequired }));
448
- const yearPart = compiled['partials/forms/input-text-date'].render(inputText.call(this, key + '-year', { pattern: '[0-9]*', maxlength: 4, hintId: key + '-hint', date: true, autocomplete: autocomplete.year, formGroupClassName, className: classNameYear, isRequired }));
451
+ const monthPart = compiled['partials/forms/input-text-date'].render(inputText.call(this, key + '-month', { pattern: '[0-9]*', min: 1, max: 12, maxlength: 2, hintId: key + '-hint', date: true, autocomplete: autocomplete.month, formGroupClassName, className: classNameMonth, isThisRequired }));
452
+ const yearPart = compiled['partials/forms/input-text-date'].render(inputText.call(this, key + '-year', { pattern: '[0-9]*', maxlength: 4, hintId: key + '-hint', date: true, autocomplete: autocomplete.year, formGroupClassName, className: classNameYear, isThisRequired }));
449
453
 
450
454
  return parts.concat(monthPart, yearPart).join('\n');
451
455
  };
@@ -5,6 +5,15 @@
5
5
  {{legend}}
6
6
  {{#isPageHeading}}</h1>{{/isPageHeading}}
7
7
  </legend>
8
+ {{#isWarning}}
9
+ <div class="govuk-warning-text">
10
+ <span class="govuk-warning-text__icon" aria-hidden="true">!</span>
11
+ <strong class="govuk-warning-text__text">
12
+ <span class="govuk-warning-text__assistive">Warning</span>
13
+ {{warning}}
14
+ </strong>
15
+ </div>
16
+ {{/isWarning}}
8
17
  {{#hint}}<div id="{{key}}-hint" class="govuk-hint">{{hint}}</div>{{/hint}}
9
18
  {{#error}}
10
19
  <p id="{{key}}-error" class="govuk-error-message">
@@ -28,7 +37,7 @@
28
37
  >
29
38
  <label class="govuk-label govuk-checkboxes__label" for="{{key}}-{{value}}">
30
39
  {{{label}}}
31
- {{#optionHint}}<div id="{{key}}-{{value}}-item-hint" class="govuk-hint govuk-radios__hint">{{optionHint}}</div>{{/optionHint}}
40
+ {{#optionHint}}<div id="{{key}}-{{value}}-item-hint" class="govuk-hint">{{optionHint}}</div>{{/optionHint}}
32
41
  </label>
33
42
  </div>
34
43
  {{#renderChild}}{{/renderChild}}
@@ -14,7 +14,7 @@
14
14
  type="{{type}}"
15
15
  name="{{id}}"
16
16
  id="{{id}}"
17
- class="govuk-input{{#className}} {{className}}{{/className}}{{#error}} invalid-input{{/error}}"
17
+ class="govuk-input{{#className}} {{className}}{{/className}}{{#error}} govuk-input--error{{/error}}"
18
18
  aria-required="{{required}}"
19
19
  {{#value}} value="{{value}}"{{/value}}
20
20
  {{#min}} min="{{min}}"{{/min}}
@@ -1,5 +1,5 @@
1
1
  <div id="{{id}}-group" class="{{#compound}} form-group-compound{{/compound}}{{#formGroupClassName}}{{formGroupClassName}}{{/formGroupClassName}}{{#error}} govuk-form-group--error{{/error}}">
2
- {{#isPageHeading}}<h1 class="govuk-label-wrapper">{{/isPageHeading}}<label for="{{id}}" class="{{labelClassName}}">
2
+ {{#isPageHeading}}<h1 class="govuk-label-wrapper">{{/isPageHeading}}<label for="{{id}}" class="{{labelClassName}}{{#isPageHeading}}govuk-label--l{{/isPageHeading}}">
3
3
  {{{label}}}
4
4
  </label>
5
5
  {{#isPageHeading}}</h1>{{/isPageHeading}}
@@ -10,6 +10,7 @@
10
10
  </p>
11
11
  {{/error}}
12
12
  {{#renderChild}}{{/renderChild}}
13
+ {{#isPrefixOrSuffix}}<div class="govuk-input__wrapper">{{/isPrefixOrSuffix}}
13
14
  {{#attributes}}
14
15
  {{#prefix}}
15
16
  <div class="govuk-input__prefix" aria-hidden="true">{{prefix}}</div>
@@ -19,7 +20,7 @@
19
20
  type="{{type}}"
20
21
  name="{{id}}"
21
22
  id="{{id}}"
22
- class="{{^className}}govuk-input{{/className}}{{#className}}{{className}}{{/className}}{{#error}} invalid-input{{/error}}"
23
+ class="{{^className}}govuk-input{{/className}}{{#className}}{{className}}{{/className}}{{#error}} govuk-input--error{{/error}}"
23
24
  aria-required="{{required}}"
24
25
  {{#value}} value="{{value}}"{{/value}}
25
26
  {{#min}} min="{{min}}"{{/min}}
@@ -35,7 +36,8 @@
35
36
  >
36
37
  {{#attributes}}
37
38
  {{#suffix}}
38
- <div class="govuk-input__prefix" aria-hidden="true">{{suffix}}</div>
39
+ <div class="govuk-input__suffix" aria-hidden="true">{{suffix}}</div>
39
40
  {{/suffix}}
40
41
  {{/attributes}}
42
+ {{#isPrefixOrSuffix}}</div>{{/isPrefixOrSuffix}}
41
43
  </div>
@@ -5,6 +5,15 @@
5
5
  {{legend}}
6
6
  {{#isPageHeading}}</h1>{{/isPageHeading}}
7
7
  </legend>
8
+ {{#isWarning}}
9
+ <div class="govuk-warning-text">
10
+ <span class="govuk-warning-text__icon" aria-hidden="true">!</span>
11
+ <strong class="govuk-warning-text__text">
12
+ <span class="govuk-warning-text__assistive">Warning</span>
13
+ {{warning}}
14
+ </strong>
15
+ </div>
16
+ {{/isWarning}}
8
17
  {{#hint}}<div id="{{key}}-hint" class="govuk-hint">{{hint}}</div>{{/hint}}
9
18
  {{#error}}<p id="{{key}}-error" class="govuk-error-message"><span class="govuk-visually-hidden">Error:</span> {{error.message}}</p>{{/error}}
10
19
  {{{detail}}}
@@ -9,7 +9,7 @@
9
9
  {{/error}}
10
10
  </label>
11
11
  {{#isPageHeading}}</h1>{{/isPageHeading}}
12
- <select id="{{id}}" class="govuk-select{{#className}} {{className}}{{/className}}{{#error}} invalid-input{{/error}}" name="{{id}}" aria-required="{{required}}">
12
+ <select id="{{id}}" class="govuk-select{{#className}} {{className}}{{/className}}{{#error}} govuk-select--error{{/error}}" name="{{id}}" aria-required="{{required}}">
13
13
  {{#options}}
14
14
  <option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option>
15
15
  {{/options}}
@@ -2,7 +2,7 @@
2
2
  <div class="govuk-character-count" data-module="govuk-character-count" data-maxlength="{{maxlength}}">
3
3
  {{/maxlength}}
4
4
  <div id="{{id}}-group" class="{{#compound}}form-group-compound {{/compound}}{{#formGroupClassName}}{{formGroupClassName}}{{/formGroupClassName}}{{#error}} govuk-form-group--error{{/error}}">
5
- {{#isPageHeading}}<h1 class="govuk-label-wrapper">{{/isPageHeading}}<label for="{{id}}" class="{{labelClassName}}">
5
+ {{#isPageHeading}}<h1 class="govuk-label-wrapper">{{/isPageHeading}}<label for="{{id}}" class="{{labelClassName}}{{#isPageHeading}}govuk-label--l{{/isPageHeading}}">
6
6
  {{{label}}}
7
7
  {{#error}}
8
8
  <p id="{{id}}-error" class="govuk-error-message">
@@ -16,7 +16,7 @@
16
16
  <textarea
17
17
  name="{{id}}"
18
18
  id="{{id}}"
19
- class="govuk-textarea{{#className}} {{className}}{{/className}} {{#maxlength}}maxlength{{/maxlength}}{{#error}} invalid-input{{/error}}"
19
+ class="govuk-textarea{{#className}} {{className}}{{/className}} {{#maxlength}}maxlength{{/maxlength}}{{#error}} govuk-input--error{{/error}}"
20
20
  aria-required="{{required}}"
21
21
  {{#maxlength}} maxlength="{{maxlength}}"{{/maxlength}}
22
22
  {{#attributes}}
@@ -44,9 +44,16 @@
44
44
  {{/gaTagId}}
45
45
  {{/cookieMessage}}
46
46
  {{$footerSupportLinks}}
47
- <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/cookies">{{#t}}base.cookies{{/t}}</a></li>
48
- <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/accessibility">{{#t}}base.accessibility{{/t}}</a></li>
49
- <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/terms-and-conditions">{{#t}}base.terms{{/t}}</a></li>
47
+ <ul class="govuk-footer__inline-list">
48
+ {{#footerSupportLinks}}
49
+ <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="{{path}}">{{#t}}{{property}}{{/t}}</a></li>
50
+ {{/footerSupportLinks}}
51
+ {{^footerSupportLinks}}
52
+ <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/cookies">{{#t}}base.cookies{{/t}}</a></li>
53
+ <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/accessibility">{{#t}}base.accessibility{{/t}}</a></li>
54
+ <li class="govuk-footer__inline-list-item"><a class="govuk-footer__link" href="/terms-and-conditions">{{#t}}base.terms{{/t}}</a></li>
55
+ {{/footerSupportLinks}}
56
+ </ul>
50
57
  {{/footerSupportLinks}}
51
58
  {{$bodyEnd}}
52
59
  {{> partials-gatag}}
@@ -20,7 +20,7 @@
20
20
  </div>
21
21
  <div class="gem-c-cookie-banner__confirmation govuk-width-container" tabindex="0" hidden="" id="cookie-banner-submitted" >
22
22
  <p class="gem-c-cookie-banner__confirmation-message" role="alert">
23
- Your cookie preferences have been saved. You can <a class="govuk-link" data-module="gem-track-click" data-track-category="cookieBanner" data-track-action="Cookie banner settings clicked from confirmation" href="/student-help/cookies">change your cookie settings</a> at any time.
23
+ Your cookie preferences have been saved. You can <a class="govuk-link" data-module="gem-track-click" data-track-category="cookieBanner" data-track-action="Cookie banner settings clicked from confirmation" href="/cookies">change your cookie settings</a> at any time.
24
24
  </p>
25
25
  <div class="govuk-button-group">
26
26
  <button class="gem-c-cookie-banner__hide-button govuk-button" id="hide-cookie-banner">Hide this message</button>
@@ -1,7 +1,8 @@
1
1
  <form action="" method="POST" {{$encoding}}{{/encoding}} autocomplete="off" novalidate="true" spellcheck="false">
2
2
  {{$intro}}
3
- {{#intro}}<span class="govuk-hint" >{{intro}}</span>{{/intro}}
3
+ {{#intro}}<p>{{intro}}</p>{{/intro}}
4
4
  {{/intro}}
5
+
5
6
  {{$form}}{{/form}}
6
7
  {{#csrf-token}}
7
8
  <input type="hidden" name="x-csrf-token" value="{{csrf-token}}" />
@@ -3,8 +3,8 @@
3
3
  <div class="govuk-grid-column-two-thirds">
4
4
  {{$validationSummary}}{{/validationSummary}}
5
5
  {{#captionHeading}}<span class="govuk-caption-l">{{captionHeading}}</span>{{/captionHeading}}
6
- {{#header}}<h1 class="govuk-heading-xl">{{header}}</h1>{{/header}}
7
- {{#subHeading}}<div id="page-header" class="govuk-heading-l">{{subHeading}}</div>{{/subHeading}}
6
+ {{#header}}<h1 class="govuk-heading-l">{{header}}</h1>{{/header}}
7
+ {{#subHeading}}<div id="page-header"><h2 class="govuk-heading-m">{{subHeading}}</h2></div>{{/subHeading}}
8
8
  {{$content}}{{/content}}
9
9
  </div>
10
10
  {{/content-outer}}
@@ -1,8 +1,8 @@
1
1
  <div class="govuk-header__content">
2
2
  {{#startPageRedirectUrl}}
3
- <a href="{{startPageRedirectUrl}}" class="govuk-header__link govuk-header__link--service-name" id="proposition-name">{{$journeyHeader}}{{/journeyHeader}}</a>
3
+ <a href="{{startPageRedirectUrl}}" class="govuk-header__link govuk-header__link--service-name" id="proposition-name">{{$journeyHeader}}{{/journeyHeader}}</a>
4
4
  {{/startPageRedirectUrl}}
5
5
  {{^startPageRedirectUrl}}
6
- <a href="{{baseUrl}}" class="govuk-header__link govuk-header__link--service-name" id="proposition-name">{{$journeyHeader}}{{/journeyHeader}}</a>
6
+ <a href="{{journeyHeaderURL}}" class="govuk-header__link govuk-header__link--service-name" id="proposition-name">{{$journeyHeader}}{{/journeyHeader}}</a>
7
7
  {{/startPageRedirectUrl}}
8
8
  </div>
@@ -5,10 +5,10 @@
5
5
  <dt class="confirm-label">{{label}}</dt>
6
6
  <dd class="confirm-value" data-value="{{value}}">{{value}}</dd>
7
7
  {{#changeLink}}
8
- <dd><span class="link"><a href="{{changeLink}}" id="{{field}}-change-{{index}}">{{#t}}buttons.change{{/t}} <span class="visuallyhidden">{{changeLinkDescription}} from {{value}}</span></a></span></dd>
8
+ <dd><span><a class="govuk-link" href="{{changeLink}}" id="{{field}}-change-{{index}}">{{#t}}buttons.change{{/t}} <span class="visuallyhidden">{{changeLinkDescription}} from {{value}}</span></a></span></dd>
9
9
  {{/changeLink}}
10
10
  {{^changeLink}}
11
- <dd><span class="link"><a href="{{baseUrl}}{{step}}/edit#{{field}}" id="{{field}}-change">{{#t}}buttons.change{{/t}} <span class="visuallyhidden">{{changeLinkDescription}} from {{value}}</span></a></span></dd>
11
+ <dd><span><a class="govuk-link" href="{{baseUrl}}{{step}}/edit#{{field}}" id="{{field}}-change">{{#t}}buttons.change{{/t}} <span class="visuallyhidden">{{changeLinkDescription}} from {{value}}</span></a></span></dd>
12
12
  {{/changeLink}}
13
13
  {{/isSeparator}}
14
14
  </div>
@@ -0,0 +1,7 @@
1
+ <div class="govuk-warning-text">
2
+ <span class="govuk-warning-text__icon" aria-hidden="true">!</span>
3
+ <strong class="govuk-warning-text__text">
4
+ <span class="govuk-warning-text__assistive">Warning</span>
5
+ {{warning}}
6
+ </strong>
7
+ </div>
@@ -1,6 +1,7 @@
1
1
  {{<error}}
2
2
  {{$content}}
3
- <p>{{{content.message}}}</p>
3
+ <h1 class="govuk-heading-l">{{{content.title}}}</h1>
4
+ <h2 class="govuk-heading-m">{{{content.message}}}</h2>
4
5
  <a href="{{startLink}}" class="govuk-button" role="button">{{#t}}buttons.start-again{{/t}}</a>
5
6
  {{/content}}
6
7
  {{/error}}
@@ -12,6 +12,10 @@ $path: "/public/images/" !default;
12
12
  // https://github.com/alphagov/govuk_elements/blob/master/packages/govuk-elements-sass/public/sass/_elements.scss
13
13
  @import "govuk-elements-sass/public/sass/elements";
14
14
 
15
+ // Govuk frontend
16
+ // https://github.com/alphagov/govuk-frontend-docs
17
+ @import "govuk-frontend";
18
+
15
19
  // Custom
16
20
  @import "base";
17
21
  @import "layout";
@@ -25,7 +25,7 @@
25
25
  .govuk-form-group--error {
26
26
  box-sizing: border-box;
27
27
  padding-left: $gutter-half - $validation-bdr-size;
28
- border-left: $validation-bdr-size-lg solid $error-colour;
28
+ //border-left: $validation-bdr-size-lg solid $error-colour;
29
29
 
30
30
  &:focus {
31
31
  outline: $focus-outline;
@@ -42,7 +42,7 @@
42
42
  margin-bottom: 0.5em;
43
43
  }
44
44
  @include bold-19;
45
- color: $error-colour;
45
+ //color: $error-colour;
46
46
  }
47
47
 
48
48
  .invalid-input,
@@ -71,11 +71,20 @@ function formFocus() {
71
71
  var labels;
72
72
  var summaries;
73
73
 
74
- if (getElementFromSummaryLink && getEditPath === 'edit') {
74
+ var editMode = getElementFromSummaryLink && getEditPath === 'edit';
75
+
76
+ if (getElementFromSummaryLink && document.getElementById(getElementFromSummaryLink) && editMode) {
75
77
  document.getElementById(getElementFromSummaryLink).focus();
78
+ }
79
+
80
+ if (getElementFromSummaryLink && document.getElementById(getElementFromSummaryLink + '-group') && editMode) {
76
81
  document.getElementById(getElementFromSummaryLink + '-group').scrollIntoView();
77
82
  }
78
83
 
84
+ if (document.getElementById(getElementFromSummaryLink + '-day') && forms.length === 1 && editMode) {
85
+ document.getElementById(getElementFromSummaryLink + '-day').focus();
86
+ }
87
+
79
88
  if (forms.length > 0) {
80
89
  labels = document.getElementsByTagName('label');
81
90
  if (labels) {
@@ -7,12 +7,14 @@ var groupBy = require('lodash').groupBy;
7
7
  var helpers = require('./helpers');
8
8
  var inputs; var groups;
9
9
  var toggleAttr = 'data-toggle';
10
- var hiddenClass = 'js-hidden';
10
+ var checkboxHiddenClass = 'govuk-checkboxes__conditional--hidden';
11
+ var radioHiddenClass = 'govuk-radios__conditional--hidden';
11
12
 
12
13
  function inputClicked(e, target) {
13
14
  target = target || helpers.target(e);
14
15
  var shown;
15
16
  each(groups[target.name], function (input) {
17
+ var hiddenClass = (input.type.match(/checkbox/)) ? checkboxHiddenClass : radioHiddenClass;
16
18
  var id = input.getAttribute('aria-controls');
17
19
  var toggle = document.getElementById(id);
18
20
  if (toggle) {
@@ -24,7 +24,12 @@ function clicked(e) {
24
24
  }
25
25
 
26
26
  if (inputs) {
27
- inputs[0].focus();
27
+ if (inputs[0].getAttribute('type') === 'hidden') {
28
+ var getVisibleElements = group.querySelectorAll('input[type=text]');
29
+ getVisibleElements[0].focus();
30
+ } else {
31
+ inputs[0].focus();
32
+ }
28
33
  }
29
34
  }
30
35
  }
package/index.js CHANGED
@@ -79,7 +79,7 @@ const getContentSecurityPolicy = (config, res) => {
79
79
  fontSrc: ['fonts.gstatic.com '],
80
80
  scriptSrc: ['www.google-analytics.com', 'ssl.google-analytics.com'],
81
81
  imgSrc: ['www.google-analytics.com', 'ssl.gstatic.com'],
82
- connectSrc: ['www.google-analytics.com']
82
+ connectSrc: ['https://www.google-analytics.com', 'https://region1.google-analytics.com']
83
83
  };
84
84
 
85
85
  if (config.gaTagId) {
@@ -12,6 +12,8 @@ const getContent = (err, translate) => {
12
12
  if (err.code === 'SESSION_TIMEOUT') {
13
13
  err.status = 401;
14
14
  err.template = 'session-timeout';
15
+ err.title = (translate && translate('errors.session.title'));
16
+ err.message = (translate && translate('errors.session.message'));
15
17
  content.title = (translate && translate('errors.session.title'));
16
18
  content.message = (translate && translate('errors.session.message'));
17
19
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hof",
3
3
  "description": "A bootstrap for HOF projects",
4
- "version": "20.0.0-beta.9",
4
+ "version": "20.0.0",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
@@ -72,13 +72,14 @@
72
72
  "minimatch": "^3.0.3",
73
73
  "minimist": "^1.2.6",
74
74
  "mixwith": "^0.1.1",
75
- "moment": "^2.29.2",
75
+ "moment": "^2.29.4",
76
76
  "morgan": "^1.10.0",
77
77
  "mustache": "^2.3.0",
78
78
  "nodemailer": "^6.6.3",
79
79
  "nodemailer-ses-transport": "^1.5.0",
80
80
  "nodemailer-smtp-transport": "^2.7.4",
81
81
  "nodemailer-stub-transport": "^1.1.0",
82
+ "notifications-node-client": "^5.1.1",
82
83
  "redis": "^3.1.2",
83
84
  "reqres": "^3.0.1",
84
85
  "request": "^2.79.0",
@@ -88,6 +89,7 @@
88
89
  "uglify-js": "^3.14.3",
89
90
  "underscore": "^1.12.1",
90
91
  "urijs": "^1.19.11",
92
+ "uuid": "^8.3.2",
91
93
  "winston": "^3.7.2"
92
94
  },
93
95
  "devDependencies": {
@@ -21,6 +21,7 @@ module.exports = {
21
21
  isPageHeading: 'true'
22
22
  },
23
23
  'dateOfBirth': dateComponent('dateOfBirth', {
24
+ mixin: 'input-date',
24
25
  isPageHeading: 'true',
25
26
  validate: [
26
27
  'required',