hof 20.0.0-beta.9 → 20.0.0

Sign up to get free protection for your applications and to get access to all the features.
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',