@planning-inspectorate/dynamic-forms 2.4.0 → 3.1.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 (58) hide show
  1. package/README.md +12 -1
  2. package/package.json +3 -2
  3. package/src/components/address/question.js +25 -19
  4. package/src/components/address/question.test.js +1 -1
  5. package/src/components/date/question.js +7 -21
  6. package/src/components/date/question.test.js +1 -1
  7. package/src/components/date-period/question.js +17 -25
  8. package/src/components/date-period/question.test.js +1 -1
  9. package/src/components/date-time/question.js +10 -23
  10. package/src/components/identifier/question.js +3 -6
  11. package/src/components/manage-list/README.md +97 -0
  12. package/src/components/manage-list/answer-summary-list.njk +10 -0
  13. package/src/components/manage-list/index.njk +61 -0
  14. package/src/components/manage-list/manage-list-section.js +39 -0
  15. package/src/components/manage-list/manage-list-section.test.js +52 -0
  16. package/src/components/manage-list/question.js +147 -0
  17. package/src/components/manage-list/question.test.js +139 -0
  18. package/src/components/multi-field-input/index.njk +1 -1
  19. package/src/components/multi-field-input/question.js +12 -13
  20. package/src/components/multi-field-input/question.test.js +8 -8
  21. package/src/components/number-entry/question.js +7 -13
  22. package/src/components/radio/question.js +2 -4
  23. package/src/components/select/question.js +2 -4
  24. package/src/components/single-line-input/question.js +5 -6
  25. package/src/components/text-entry/question.js +5 -5
  26. package/src/components/text-entry-redact/question.js +18 -9
  27. package/src/components/text-entry-redact/question.test.js +12 -19
  28. package/src/components/unit-option-entry/question.js +9 -8
  29. package/src/components/utils/nunjucks.js +20 -0
  30. package/src/controller.js +39 -10
  31. package/src/controller.test.js +90 -24
  32. package/src/index.js +3 -2
  33. package/src/journey/journey-response.js +2 -2
  34. package/src/journey/journey-types.d.ts +21 -1
  35. package/src/journey/journey.js +64 -18
  36. package/src/journey/journey.test.js +80 -15
  37. package/src/questions/options-question.js +6 -7
  38. package/src/questions/question-props.d.ts +2 -1
  39. package/src/questions/question-types.d.ts +26 -0
  40. package/src/questions/question.js +119 -15
  41. package/src/questions/question.test.js +98 -5
  42. package/src/questions/questions.js +3 -1
  43. package/src/section-types.d.ts +18 -0
  44. package/src/section.js +40 -11
  45. package/src/section.test.js +89 -0
  46. package/test/journey.js +5 -0
  47. package/test/mock/journey.js +17 -0
  48. package/test/questions.js +30 -1
  49. package/test/questions.test.js +104 -40
  50. package/test/snapshots/check-your-answers-not-started.html +11 -0
  51. package/test/snapshots/check-your-answers-with-answers.html +11 -0
  52. package/test/snapshots/manage-list-answer-summary.html +14 -0
  53. package/test/snapshots/manage-list-render-questions.html +97 -0
  54. package/test/snapshots/manage-list-render.html +91 -0
  55. package/test/snapshots/travel-companions.html +19 -0
  56. package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
  57. package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
  58. package/tsconfig.json +2 -1
@@ -0,0 +1,147 @@
1
+ import { Question } from '#question';
2
+ import { Uuid } from '#src/lib/uuid.js';
3
+ import { nunjucksEnv } from '#src/components/utils/nunjucks.js';
4
+
5
+ /**
6
+ * @typedef {Object} ManageListQuestionParameters
7
+ * @property {string} titleSingular - the single name of the list item, e.g. "Holiday activity"
8
+ * @property {boolean} [showManageListQuestions] - whether to show the question titles as well as answers on the manage list summary page
9
+ * @property {boolean} [showAnswersInSummary] - whether to show the answers on the main check-your-answers page (or just a count)
10
+ */
11
+
12
+ export default class ManageListQuestion extends Question {
13
+ /** @type {import('../../section.js').Section} */
14
+ #section;
15
+ /** @type {boolean} */
16
+ #showAnswersInSummary;
17
+
18
+ /**
19
+ * @param {import('#question-types').QuestionParameters & ManageListQuestionParameters} params
20
+ */
21
+ constructor(params) {
22
+ super({
23
+ ...params,
24
+ pageTitle: params.title,
25
+ viewFolder: 'manage-list',
26
+ viewData: {
27
+ ...params.viewData,
28
+ titleSingular: params.titleSingular,
29
+ showManageListQuestions: params.showManageListQuestions
30
+ }
31
+ });
32
+ this.#showAnswersInSummary = params.showAnswersInSummary || false;
33
+ }
34
+
35
+ /**
36
+ * Is this question a manage list question?
37
+ *
38
+ * Used by controller and other logic.
39
+ * Added as a getter so custom components can also be a manage list question.
40
+ *
41
+ * @returns {boolean}
42
+ */
43
+ get isManageListQuestion() {
44
+ return true;
45
+ }
46
+
47
+ /**
48
+ * @param {import('#question').QuestionViewModel} viewModel
49
+ */
50
+ addCustomDataToViewModel(viewModel) {
51
+ viewModel.question.addAnotherLink = this.#addAnotherLink;
52
+ viewModel.question.firstQuestionUrl = this.#firstQuestionUrl;
53
+ viewModel.question.valueSummary = [];
54
+ if (viewModel.question.value && Array.isArray(viewModel.question.value)) {
55
+ viewModel.question.valueSummary = viewModel.question.value.map((v) => {
56
+ return {
57
+ id: v.id,
58
+ value: this.#formatItemAnswers(v)
59
+ };
60
+ });
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Format the answers to each of the manage list questions
66
+ * @param {{id: string, [k: string]: string}} answer
67
+ * @returns {{question: string, answer: string}[]}
68
+ */
69
+ #formatItemAnswers(answer) {
70
+ if (this.section.questions.length === 0) {
71
+ return [];
72
+ }
73
+ const mockJourney = {
74
+ getCurrentQuestionUrl() {}
75
+ };
76
+ return this.section.questions.map((q) => {
77
+ const formatted = q
78
+ .formatAnswerForSummary('', mockJourney, answer[q.fieldName])
79
+ .map((a) => a.value)
80
+ .join(', ');
81
+
82
+ return {
83
+ question: q.title,
84
+ answer: formatted
85
+ };
86
+ });
87
+ }
88
+
89
+ formatAnswerForSummary(sectionSegment, journey, answer) {
90
+ let formattedAnswer = this.notStartedText;
91
+ if (answer && Array.isArray(answer)) {
92
+ if (this.#showAnswersInSummary) {
93
+ const answers = answer.map((a) => this.#formatItemAnswers(a));
94
+ formattedAnswer = nunjucksEnv().render('components/manage-list/answer-summary-list.njk', { answers });
95
+ } else if (answer.length > 0) {
96
+ formattedAnswer = `${answer.length} ${this.title}`;
97
+ }
98
+ }
99
+ const action = this.getAction(sectionSegment, journey, answer);
100
+ const key = this.title ?? this.question;
101
+ return [
102
+ {
103
+ key: key,
104
+ value: formattedAnswer,
105
+ action: action
106
+ }
107
+ ];
108
+ }
109
+
110
+ /**
111
+ * Create an 'add another' link,
112
+ * to the first question in the manage list section
113
+ *
114
+ * @returns {string}
115
+ */
116
+ get #addAnotherLink() {
117
+ if (this.section.questions.length === 0) {
118
+ return '';
119
+ }
120
+ const nextItemId = Uuid.randomUUID();
121
+ const firstQuestion = this.#firstQuestionUrl;
122
+ return `add/${nextItemId}/${firstQuestion}`;
123
+ }
124
+
125
+ /**
126
+ * First question URL
127
+ *
128
+ * @returns {string}
129
+ */
130
+ get #firstQuestionUrl() {
131
+ if (this.section.questions.length === 0) {
132
+ return '';
133
+ }
134
+ return this.section.questions[0].url;
135
+ }
136
+
137
+ get section() {
138
+ if (!this.#section) {
139
+ throw new Error('manage list section not set');
140
+ }
141
+ return this.#section;
142
+ }
143
+
144
+ set section(section) {
145
+ this.#section = section;
146
+ }
147
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import ManageListQuestion from '#src/components/manage-list/question.js';
4
+ import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
5
+ import { mockJourney } from '#test/mock/journey.js';
6
+ import { mockRandomUUID } from '#src/lib/uuid.test.js';
7
+ import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
8
+ import path from 'path';
9
+ import { snapshotsDir } from '#test/utils/utils.js';
10
+
11
+ describe('components/manage-list/question', () => {
12
+ const TITLE = 'Things';
13
+ const QUESTION = 'Manage list';
14
+ const DESCRIPTION = 'A list of things';
15
+ const FIELDNAME = 'manageListTest';
16
+ const newQuestion = (options = {}) => {
17
+ return new ManageListQuestion({
18
+ title: TITLE,
19
+ question: QUESTION,
20
+ description: DESCRIPTION,
21
+ fieldName: FIELDNAME,
22
+ titleSingular: 'Thing',
23
+ ...options
24
+ });
25
+ };
26
+ it('should create', () => {
27
+ const q = newQuestion();
28
+ assert.strictEqual(q.title, TITLE);
29
+ assert.strictEqual(q.question, QUESTION);
30
+ assert.strictEqual(q.description, DESCRIPTION);
31
+ assert.strictEqual(q.fieldName, FIELDNAME);
32
+ assert.strictEqual(q.viewFolder, 'manage-list');
33
+ assert.strictEqual(q.isManageListQuestion, true);
34
+ });
35
+
36
+ it('should populate addAnotherLink & firstQuestionUrl', (ctx) => {
37
+ const q = newQuestion();
38
+ mockRandomUUID(ctx);
39
+ q.section = new ManageListSection().addQuestion({ url: 'first-question' });
40
+ const viewModel = q.prepQuestionForRendering({}, mockJourney());
41
+ // add/<v4-guid>/<question-url>
42
+ assert.match(viewModel?.question?.addAnotherLink, /^add\/00000000-0000-0000-0000-000000000000\/first-question/);
43
+ assert.strictEqual(viewModel?.question?.firstQuestionUrl, 'first-question');
44
+ });
45
+
46
+ const questionWithManageQuestions = (ctx, options) => {
47
+ const q = newQuestion(options);
48
+ const innerQ1 = {
49
+ url: 'first-question',
50
+ title: 'Q 1',
51
+ formatAnswerForSummary() {
52
+ return [{ value: 'mock answer' }];
53
+ }
54
+ };
55
+ const innerQ2 = {
56
+ url: 'second-question',
57
+ title: 'Q 2',
58
+ formatAnswerForSummary() {
59
+ return [{ value: 'mock answer 2' }];
60
+ }
61
+ };
62
+ mockRandomUUID(ctx);
63
+ const journey = mockJourney();
64
+ journey.response.answers = {
65
+ [FIELDNAME]: [{ id: 'id-1' }, { id: 'id-2' }, { id: 'id-3' }]
66
+ };
67
+ q.section = new ManageListSection().addQuestion(innerQ1).addQuestion(innerQ2);
68
+ return { q, journey };
69
+ };
70
+
71
+ it('should format answers to questions in the section', (ctx) => {
72
+ const { q, journey } = questionWithManageQuestions(ctx);
73
+ const viewModel = q.prepQuestionForRendering({}, journey);
74
+ assert.ok(Array.isArray(viewModel?.question?.valueSummary));
75
+ assert.strictEqual(viewModel?.question?.valueSummary.length, 3);
76
+ assert.deepStrictEqual(viewModel?.question?.valueSummary[0], {
77
+ id: 'id-1',
78
+ value: [
79
+ { question: 'Q 1', answer: 'mock answer' },
80
+ { question: 'Q 2', answer: 'mock answer 2' }
81
+ ]
82
+ });
83
+ });
84
+
85
+ it('should format list answer for summary', (ctx) => {
86
+ const q = newQuestion();
87
+ mockRandomUUID(ctx);
88
+ const answerForSummary = q.formatAnswerForSummary('section-1', mockJourney(), [{}, {}, {}]);
89
+ assert.strictEqual(answerForSummary.length, 1);
90
+ assert.strictEqual(answerForSummary[0].value, '3 Things');
91
+ });
92
+
93
+ it('should support showing all answers in the format list answer for summary', (ctx) => {
94
+ const { q } = questionWithManageQuestions(ctx, { showAnswersInSummary: true });
95
+ const answerForSummary = q.formatAnswerForSummary('section-1', mockJourney(), [{}, {}, {}]);
96
+ assert.strictEqual(answerForSummary.length, 1);
97
+ ctx.assert.fileSnapshot(
98
+ answerForSummary[0].value.replaceAll('\r\n', '\n'),
99
+ path.join(snapshotsDir(), 'manage-list-answer-summary.html'),
100
+ {
101
+ serializers: [(v) => v]
102
+ }
103
+ );
104
+ });
105
+
106
+ it('should render with answers', (ctx) => {
107
+ const { q, journey } = questionWithManageQuestions(ctx);
108
+ const viewModel = q.prepQuestionForRendering({}, journey);
109
+ const nunjucks = configureNunjucksTestEnv();
110
+ const res = {
111
+ render: mock.fn((view, data) => nunjucks.render(view + '.njk', data))
112
+ };
113
+ q.renderAction(res, viewModel);
114
+ assert.strictEqual(res.render.mock.callCount(), 1);
115
+ ctx.assert.fileSnapshot(res.render.mock.calls[0].result, path.join(snapshotsDir(), 'manage-list-render.html'), {
116
+ serializers: [(v) => v]
117
+ });
118
+ });
119
+
120
+ it('should render with answers & questions', (ctx) => {
121
+ const { q, journey } = questionWithManageQuestions(ctx, {
122
+ showManageListQuestions: true
123
+ });
124
+ const viewModel = q.prepQuestionForRendering({}, journey);
125
+ const nunjucks = configureNunjucksTestEnv();
126
+ const res = {
127
+ render: mock.fn((view, data) => nunjucks.render(view + '.njk', data))
128
+ };
129
+ q.renderAction(res, viewModel);
130
+ assert.strictEqual(res.render.mock.callCount(), 1);
131
+ ctx.assert.fileSnapshot(
132
+ res.render.mock.calls[0].result,
133
+ path.join(snapshotsDir(), 'manage-list-render-questions.html'),
134
+ {
135
+ serializers: [(v) => v]
136
+ }
137
+ );
138
+ });
139
+ });
@@ -5,7 +5,7 @@
5
5
  {% from "govuk/components/fieldset/macro.njk" import govukFieldset %}
6
6
  {% from "govuk/components/button/macro.njk" import govukButton %}
7
7
 
8
- {% set inputFields = question.inputFields %}
8
+ {% set inputFields = question.value %}
9
9
 
10
10
  {% block dynQuestionContent %}
11
11
  <div class="govuk-grid-row">
@@ -1,4 +1,4 @@
1
- import { Question } from '../../questions/question.js';
1
+ import { Question } from '#question';
2
2
  import escape from 'escape-html';
3
3
  import { nl2br } from '../../lib/utils.js';
4
4
 
@@ -56,22 +56,21 @@ export default class MultiFieldInputQuestion extends Question {
56
56
  }
57
57
  }
58
58
 
59
- prepQuestionForRendering(section, journey, customViewData, payload) {
60
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
61
-
62
- const inputFields = this.inputFields.map((inputField) => {
63
- return payload
64
- ? { ...inputField, value: this.#formatValue(payload[inputField.fieldName], inputField.formatTextFunction) }
65
- : {
66
- ...inputField,
67
- value: this.#formatValue(journey.response.answers[inputField.fieldName], inputField.formatTextFunction)
68
- };
59
+ answerForViewModel(answers) {
60
+ return this.inputFields.map((inputField) => {
61
+ return {
62
+ ...inputField,
63
+ value: this.#formatValue(answers[inputField.fieldName], inputField.formatTextFunction)
64
+ };
69
65
  });
66
+ }
70
67
 
71
- viewModel.question.inputFields = inputFields;
68
+ /**
69
+ * @param {import('#question').QuestionViewModel} viewModel
70
+ */
71
+ addCustomDataToViewModel(viewModel) {
72
72
  viewModel.question.label = this.label;
73
73
  viewModel.question.attributes = this.inputAttributes;
74
- return viewModel;
75
74
  }
76
75
 
77
76
  /**
@@ -73,8 +73,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
73
73
  const result = question.prepQuestionForRendering({}, journey, customViewData);
74
74
 
75
75
  assert.strictEqual(result.question?.fieldName, FIELDNAME);
76
- assert.strictEqual(result.question?.inputFields[0].fieldName, INPUTFIELDS[0].fieldName);
77
- assert.strictEqual(result.question?.inputFields[1].fieldName, INPUTFIELDS[1].fieldName);
76
+ assert.strictEqual(result.question?.value[0].fieldName, INPUTFIELDS[0].fieldName);
77
+ assert.strictEqual(result.question?.value[1].fieldName, INPUTFIELDS[1].fieldName);
78
78
  assert.strictEqual(result.hello, 'hi');
79
79
  });
80
80
  it('should set inputFields with values from payload if provided', () => {
@@ -94,8 +94,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
94
94
 
95
95
  const result = question.prepQuestionForRendering({}, journey, {}, payload);
96
96
 
97
- assert.strictEqual(result.question?.inputFields[0].value, 'payloadValue1');
98
- assert.strictEqual(result.question?.inputFields[1].value, 'payloadValue2');
97
+ assert.strictEqual(result.question?.value[0].value, 'payloadValue1');
98
+ assert.strictEqual(result.question?.value[1].value, 'payloadValue2');
99
99
  });
100
100
  it('should set inputFields with values from journey response if payload is not provided', () => {
101
101
  const question = createMultiFieldInputQuestion();
@@ -113,8 +113,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
113
113
 
114
114
  const result = question.prepQuestionForRendering({}, journey, {});
115
115
 
116
- assert.strictEqual(result.question?.inputFields[0].value, 'responseValue1');
117
- assert.strictEqual(result.question?.inputFields[1].value, 'responseValue2');
116
+ assert.strictEqual(result.question?.value[0].value, 'responseValue1');
117
+ assert.strictEqual(result.question?.value[1].value, 'responseValue2');
118
118
  });
119
119
  it('should set inputFields with formatted values from journey response if formatTextFunction is provided', () => {
120
120
  const formatTextFunction = (value) => `Formatted: ${value}`;
@@ -136,8 +136,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
136
136
 
137
137
  const result = question.prepQuestionForRendering({}, journey, {});
138
138
 
139
- assert.strictEqual(result.question?.inputFields[0].value, 'Formatted: responseValue1');
140
- assert.strictEqual(result.question?.inputFields[1].value, 'Formatted: responseValue2');
139
+ assert.strictEqual(result.question?.value[0].value, 'Formatted: responseValue1');
140
+ assert.strictEqual(result.question?.value[1].value, 'Formatted: responseValue2');
141
141
  });
142
142
  });
143
143
 
@@ -1,4 +1,4 @@
1
- import { Question } from '../../questions/question.js';
1
+ import { Question } from '#question';
2
2
  import { getPersistedNumberAnswer } from '../utils/persisted-number-answer.js';
3
3
 
4
4
  /**
@@ -21,22 +21,16 @@ export default class NumberEntryQuestion extends Question {
21
21
  this.label = label;
22
22
  }
23
23
 
24
+ answerForViewModel(answers) {
25
+ return getPersistedNumberAnswer(answers[this.fieldName] || '');
26
+ }
27
+
24
28
  /**
25
- * adds label and suffix property to view model
29
+ * @param {import('#question').QuestionViewModel} viewModel
26
30
  */
27
- prepQuestionForRendering(section, journey, customViewData, payload) {
28
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
29
-
30
- const answer = journey.response.answers[this.fieldName];
31
- const persistedAnswer = getPersistedNumberAnswer(answer);
32
-
33
- viewModel.question.value = persistedAnswer;
34
- viewModel.answer = persistedAnswer;
35
-
31
+ addCustomDataToViewModel(viewModel) {
36
32
  viewModel.question.label = this.label;
37
33
  viewModel.question.suffix = this.suffix;
38
-
39
- return viewModel;
40
34
  }
41
35
 
42
36
  /**
@@ -60,13 +60,11 @@ export default class RadioQuestion extends OptionsQuestion {
60
60
  }
61
61
 
62
62
  /**
63
- * adds label property to view model
63
+ * @param {import('#question').QuestionViewModel} viewModel
64
64
  */
65
- prepQuestionForRendering(section, journey, customViewData, payload) {
66
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
65
+ addCustomDataToViewModel(viewModel) {
67
66
  viewModel.question.label = this.label;
68
67
  viewModel.question.legend = this.legend;
69
- return viewModel;
70
68
  }
71
69
 
72
70
  /**
@@ -54,13 +54,11 @@ export default class SelectQuestion extends OptionsQuestion {
54
54
  }
55
55
 
56
56
  /**
57
- * adds label property to view model
57
+ * @param {import('#question').QuestionViewModel} viewModel
58
58
  */
59
- prepQuestionForRendering(section, journey, customViewData, payload) {
60
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
59
+ addCustomDataToViewModel(viewModel) {
61
60
  viewModel.question.label = this.label;
62
61
  viewModel.question.legend = this.legend;
63
- return viewModel;
64
62
  }
65
63
 
66
64
  /**
@@ -1,4 +1,4 @@
1
- import { Question } from '../../questions/question.js';
1
+ import { Question } from '#question';
2
2
 
3
3
  /**
4
4
  * @typedef {import('../../questions/question.js').QuestionViewModel} QuestionViewModel
@@ -32,17 +32,16 @@ export default class SingleLineInputQuestion extends Question {
32
32
  this.classes = params.classes || '';
33
33
  }
34
34
 
35
- prepQuestionForRendering(section, journey, customViewData, payload) {
36
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
35
+ /**
36
+ * @param {import('#question').QuestionViewModel} viewModel
37
+ */
38
+ addCustomDataToViewModel(viewModel) {
37
39
  viewModel.question.label = this.label;
38
- viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
39
-
40
40
  // Extract type from attributes to pass separately to avoid duplication
41
41
  const { type, ...otherAttributes } = this.inputAttributes;
42
42
  viewModel.question.attributes = otherAttributes;
43
43
  viewModel.question.type = type;
44
44
 
45
45
  viewModel.question.classes = this.classes;
46
- return viewModel;
47
46
  }
48
47
  }
@@ -1,4 +1,4 @@
1
- import { Question } from '../../questions/question.js';
1
+ import { Question } from '#question';
2
2
  /**
3
3
  * @typedef {import('../../questions/question.js').QuestionViewModel} QuestionViewModel
4
4
  * @typedef {import('../../journey/journey.js').Journey} Journey
@@ -33,11 +33,11 @@ export default class TextEntryQuestion extends Question {
33
33
  this.label = label;
34
34
  }
35
35
 
36
- prepQuestionForRendering(section, journey, customViewData, payload) {
37
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
36
+ /**
37
+ * @param {import('#question').QuestionViewModel} viewModel
38
+ */
39
+ addCustomDataToViewModel(viewModel) {
38
40
  viewModel.question.label = this.label;
39
41
  viewModel.question.textEntryCheckbox = this.textEntryCheckbox;
40
- viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
41
- return viewModel;
42
42
  }
43
43
  }
@@ -1,4 +1,4 @@
1
- import { Question } from '../../questions/question.js';
1
+ import { Question } from '#question';
2
2
  import { nl2br } from '../../lib/utils.js';
3
3
 
4
4
  /**
@@ -68,18 +68,27 @@ export default class TextEntryRedactQuestion extends Question {
68
68
  return super.getDataToSave(req, journeyResponse);
69
69
  }
70
70
 
71
- prepQuestionForRendering(section, journey, customViewData, payload) {
72
- let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
71
+ prepQuestionForRendering(section, journey, customViewData, payload, options) {
72
+ const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
73
+ const answers = this.answerObjectFromJourneyResponse(journey.response, options);
74
+ const answer = viewModel.question.value;
75
+ viewModel.question.valueRedacted = answers[this.fieldName + 'Redacted'] || answer;
76
+ viewModel.question.valueOriginal = answers[this.fieldName + 'Original'] || answer;
77
+ return viewModel;
78
+ }
79
+
80
+ answerForViewModel(answers) {
81
+ return nl2br(answers[this.fieldName]);
82
+ }
83
+
84
+ /**
85
+ * @param {import('#question').QuestionViewModel} viewModel
86
+ */
87
+ addCustomDataToViewModel(viewModel) {
73
88
  viewModel.question.label = this.label;
74
89
  viewModel.question.textEntryCheckbox = this.textEntryCheckbox;
75
- viewModel.question.value = nl2br(payload ? payload[viewModel.question.fieldName] : viewModel.question.value);
76
- viewModel.question.valueRedacted =
77
- journey.response.answers[this.fieldName + 'Redacted'] || viewModel.question.value;
78
- viewModel.question.valueOriginal =
79
- journey.response.answers[this.fieldName + 'Original'] || viewModel.question.value;
80
90
  viewModel.question.summaryText = this.summaryText;
81
91
  viewModel.showSuggestionsUi = this.showSuggestionsUi;
82
- return viewModel;
83
92
  }
84
93
 
85
94
  formatAnswerForSummary(sectionSegment, journey, answer, capitals = true) {
@@ -5,7 +5,7 @@ import { configureNunjucksTestEnv } from '../../../test/utils/nunjucks.js';
5
5
 
6
6
  describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
7
7
  const TITLE = 'title';
8
- const QUESTION = 'Question?';
8
+ const QUESTION = 'Redaction Question';
9
9
  const FIELDNAME = 'field-name';
10
10
  const VALIDATORS = [1, 2];
11
11
  const HTML = '/path/to/html.njk';
@@ -130,18 +130,15 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
130
130
  return 'back';
131
131
  },
132
132
  response: {
133
- answers: {}
133
+ answers: {
134
+ 'field-name': 'value',
135
+ 'field-nameRedacted': 'value-redacted',
136
+ 'field-nameOriginal': 'value'
137
+ }
134
138
  }
135
139
  };
136
140
  const customViewData = {
137
141
  layoutTemplate: 'views/layout-journey.njk',
138
- question: {
139
- question: 'Redaction Question',
140
- fieldName: 'field-name',
141
- value: 'value',
142
- valueRedacted: 'value-redacted',
143
- valueOriginal: 'value'
144
- },
145
142
  redactionSuggestions: [
146
143
  { category: 'Person', suggestion: 'Test Person' },
147
144
  { category: 'Address', suggestion: '123 Fake Street' }
@@ -177,17 +174,13 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
177
174
  return 'back';
178
175
  },
179
176
  response: {
180
- answers: {}
177
+ answers: {
178
+ 'field-name': 'This is my comment.\nIt has multiple lines.\r\nHere is another line.'
179
+ }
181
180
  }
182
181
  };
183
182
  const customViewData = {
184
183
  layoutTemplate: 'views/layout-journey.njk',
185
- question: {
186
- question: 'Redaction Question',
187
- fieldName: 'field-name',
188
- value: 'This is my comment.\nIt has multiple lines.\r\nHere is another line.',
189
- valueRedacted: 'value-redacted'
190
- },
191
184
  redactionSuggestions: [
192
185
  { category: 'Person', suggestion: 'Test Person' },
193
186
  { category: 'Address', suggestion: '123 Fake Street' }
@@ -235,7 +228,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
235
228
  action: {
236
229
  href: '/redacted-comment',
237
230
  text: 'Change',
238
- visuallyHiddenText: 'Question?'
231
+ visuallyHiddenText: 'Redaction Question'
239
232
  }
240
233
  }
241
234
  ]);
@@ -271,7 +264,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
271
264
  action: {
272
265
  href: '/redacted-comment',
273
266
  text: 'Change',
274
- visuallyHiddenText: 'Question?'
267
+ visuallyHiddenText: 'Redaction Question'
275
268
  }
276
269
  }
277
270
  ]);
@@ -308,7 +301,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
308
301
  action: {
309
302
  href: '/redacted-comment',
310
303
  text: 'Change',
311
- visuallyHiddenText: 'Question?'
304
+ visuallyHiddenText: 'Redaction Question'
312
305
  }
313
306
  }
314
307
  ]);
@@ -71,19 +71,20 @@ export default class UnitOptionEntryQuestion extends Question {
71
71
  * @param {Journey} journey - the journey we are in
72
72
  * @param {Record<string, unknown>} [customViewData] additional data to send to view
73
73
  * @param {Record<string, unknown>} [payload]
74
+ * @param {import('#src/questions/question-types.d.ts').PrepQuestionForRenderingOptions} options
74
75
  * @returns {QuestionViewModel & {
75
76
  * question: QuestionViewModel['question'] & {
76
77
  * options:UnitOption[]
77
78
  * }
78
79
  * }}
79
80
  */
80
- prepQuestionForRendering(section, journey, customViewData, payload) {
81
- const answer = payload ? payload[this.fieldName] : journey.response.answers[this.fieldName] || '';
82
-
83
- const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
81
+ prepQuestionForRendering(section, journey, customViewData, payload, options) {
82
+ const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
83
+ const answer = viewModel.question.value;
84
+ const answers = this.answerObjectFromJourneyResponse(journey.response, options);
84
85
 
85
86
  /** @type {Array<UnitOption>} */
86
- const options = [];
87
+ viewModel.question.options = [];
87
88
 
88
89
  for (const option of this.options) {
89
90
  let optionData = { ...option };
@@ -101,7 +102,7 @@ export default class UnitOptionEntryQuestion extends Question {
101
102
  if (conditionalIsJustHTML(conditionalField)) continue;
102
103
 
103
104
  const conversionFactor = conditionalField.conversionFactor || 1;
104
- const unconvertedAnswer = journey.response.answers[this.conditionalFieldName];
105
+ const unconvertedAnswer = answers[this.conditionalFieldName];
105
106
 
106
107
  const existingValue =
107
108
  answer === optionData.value && typeof unconvertedAnswer === 'number'
@@ -119,10 +120,10 @@ export default class UnitOptionEntryQuestion extends Question {
119
120
  };
120
121
  }
121
122
 
122
- options.push(optionData);
123
+ viewModel.question.options.push(optionData);
123
124
  }
124
125
 
125
- return { ...viewModel, question: { ...viewModel.question, options } };
126
+ return viewModel;
126
127
  }
127
128
 
128
129
  /**