@defra/fcp-sfd-frontend-engine 0.2.11 → 0.3.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.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
32
  mappers: () => mappers,
33
+ presenters: () => presenters,
33
34
  schemas: () => schemas,
34
35
  utils: () => utils
35
36
  });
@@ -155,9 +156,159 @@ var formatValidationErrors = (errors) => {
155
156
  var utils = {
156
157
  formatValidationErrors
157
158
  };
159
+
160
+ // src/presenters/base-presenter.js
161
+ var BACK_LINK_DISPLAY_MAX = 50;
162
+ var formatBackLink = (businessName) => {
163
+ if (businessName.length > BACK_LINK_DISPLAY_MAX) {
164
+ return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}\u2026`;
165
+ }
166
+ return `Back to ${businessName}`;
167
+ };
168
+ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
169
+ if (payloadNumber !== void 0) {
170
+ return payloadNumber;
171
+ }
172
+ if (changedNumber !== void 0) {
173
+ return changedNumber;
174
+ }
175
+ return originalNumber;
176
+ };
177
+ var formatDisplayAddress = (address) => {
178
+ const { lookup, manual, postcode, country, city } = address;
179
+ let addressLines = [];
180
+ if (lookup.uprn) {
181
+ const buildingAndStreet = [
182
+ lookup.buildingNumberRange,
183
+ lookup.street
184
+ ].filter(Boolean).join(" ");
185
+ addressLines = [
186
+ lookup.pafOrganisationName,
187
+ lookup.flatName,
188
+ lookup.buildingName,
189
+ buildingAndStreet,
190
+ lookup.doubleDependentLocality,
191
+ lookup.dependentLocality,
192
+ city,
193
+ lookup.county
194
+ ];
195
+ } else {
196
+ addressLines = [
197
+ manual.line1,
198
+ manual.line2,
199
+ manual.line3,
200
+ city,
201
+ manual.line4,
202
+ // County
203
+ manual.line5
204
+ ];
205
+ }
206
+ return [
207
+ ...addressLines.filter(Boolean),
208
+ postcode,
209
+ country
210
+ ];
211
+ };
212
+ var buildAddressLine = (parts) => {
213
+ return parts.filter(Boolean).join(", ") || null;
214
+ };
215
+ var buildStreetLine = (buildingRange, street) => {
216
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
217
+ };
218
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
219
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
220
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
221
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
222
+ county: lookup.county ?? null,
223
+ city: city ?? null,
224
+ country: country ?? null,
225
+ postcode: postcode ?? null
226
+ });
227
+ var formatManualAddress = (manual, city, country, postcode) => ({
228
+ address1: manual.line1 ?? null,
229
+ address2: manual.line2 ?? null,
230
+ address3: manual.line3 ?? null,
231
+ city: city ?? null,
232
+ county: manual.line4 ?? null,
233
+ country: country ?? null,
234
+ postcode: postcode ?? null
235
+ });
236
+ var formatOriginalAddress = (originalAddress) => {
237
+ const { lookup, manual, city, country, postcode } = originalAddress;
238
+ return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
239
+ };
240
+ var formatChangedAddress = (changeBusinessAddress) => {
241
+ if (!changeBusinessAddress.uprn) {
242
+ return changeBusinessAddress;
243
+ }
244
+ const {
245
+ pafOrganisationName,
246
+ flatName,
247
+ buildingName,
248
+ buildingNumberRange,
249
+ street,
250
+ doubleDependentLocality,
251
+ dependentLocality,
252
+ city,
253
+ county,
254
+ country,
255
+ postcode
256
+ } = changeBusinessAddress;
257
+ return {
258
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
259
+ address2: buildStreetLine(buildingNumberRange, street),
260
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
261
+ city: city ?? null,
262
+ county: county ?? null,
263
+ country: country ?? null,
264
+ postcode: postcode ?? null
265
+ };
266
+ };
267
+ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
268
+ const displayAddresses = addresses.map((address) => ({
269
+ value: `${address.uprn}${address.displayAddress}`,
270
+ text: address.displayAddress,
271
+ selected: (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.uprn) === address.uprn && (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.displayAddress) === address.displayAddress
272
+ }));
273
+ const hasSelectedAddress = displayAddresses.some((addr) => addr.selected);
274
+ const text = addresses.length === 1 ? "1 address found" : `${addresses.length} addresses found`;
275
+ displayAddresses.unshift({
276
+ value: "display",
277
+ text,
278
+ selected: !hasSelectedAddress
279
+ });
280
+ return displayAddresses;
281
+ };
282
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
283
+ const sortedErrors = [];
284
+ for (const section of orderedSectionsToFix) {
285
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
286
+ for (const field of fieldsInSection) {
287
+ if (errors[field]) {
288
+ sortedErrors.push({
289
+ field,
290
+ ...errors[field]
291
+ });
292
+ }
293
+ }
294
+ }
295
+ return sortedErrors;
296
+ };
297
+
298
+ // src/presenters/presenters.js
299
+ var presenters = {
300
+ formatBackLink,
301
+ formatNumber,
302
+ formatDisplayAddress,
303
+ formatOriginalAddress,
304
+ formatChangedAddress,
305
+ formatDisplayAddresses,
306
+ sortErrorsBySectionOrder
307
+ };
158
308
  // Annotate the CommonJS export names for ESM import in node:
159
309
  0 && (module.exports = {
160
310
  mappers,
311
+ presenters,
161
312
  schemas,
162
313
  utils
163
314
  });
package/dist/index.js CHANGED
@@ -118,8 +118,158 @@ var formatValidationErrors = (errors) => {
118
118
  var utils = {
119
119
  formatValidationErrors
120
120
  };
121
+
122
+ // src/presenters/base-presenter.js
123
+ var BACK_LINK_DISPLAY_MAX = 50;
124
+ var formatBackLink = (businessName) => {
125
+ if (businessName.length > BACK_LINK_DISPLAY_MAX) {
126
+ return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}\u2026`;
127
+ }
128
+ return `Back to ${businessName}`;
129
+ };
130
+ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
131
+ if (payloadNumber !== void 0) {
132
+ return payloadNumber;
133
+ }
134
+ if (changedNumber !== void 0) {
135
+ return changedNumber;
136
+ }
137
+ return originalNumber;
138
+ };
139
+ var formatDisplayAddress = (address) => {
140
+ const { lookup, manual, postcode, country, city } = address;
141
+ let addressLines = [];
142
+ if (lookup.uprn) {
143
+ const buildingAndStreet = [
144
+ lookup.buildingNumberRange,
145
+ lookup.street
146
+ ].filter(Boolean).join(" ");
147
+ addressLines = [
148
+ lookup.pafOrganisationName,
149
+ lookup.flatName,
150
+ lookup.buildingName,
151
+ buildingAndStreet,
152
+ lookup.doubleDependentLocality,
153
+ lookup.dependentLocality,
154
+ city,
155
+ lookup.county
156
+ ];
157
+ } else {
158
+ addressLines = [
159
+ manual.line1,
160
+ manual.line2,
161
+ manual.line3,
162
+ city,
163
+ manual.line4,
164
+ // County
165
+ manual.line5
166
+ ];
167
+ }
168
+ return [
169
+ ...addressLines.filter(Boolean),
170
+ postcode,
171
+ country
172
+ ];
173
+ };
174
+ var buildAddressLine = (parts) => {
175
+ return parts.filter(Boolean).join(", ") || null;
176
+ };
177
+ var buildStreetLine = (buildingRange, street) => {
178
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
179
+ };
180
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
181
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
182
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
183
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
184
+ county: lookup.county ?? null,
185
+ city: city ?? null,
186
+ country: country ?? null,
187
+ postcode: postcode ?? null
188
+ });
189
+ var formatManualAddress = (manual, city, country, postcode) => ({
190
+ address1: manual.line1 ?? null,
191
+ address2: manual.line2 ?? null,
192
+ address3: manual.line3 ?? null,
193
+ city: city ?? null,
194
+ county: manual.line4 ?? null,
195
+ country: country ?? null,
196
+ postcode: postcode ?? null
197
+ });
198
+ var formatOriginalAddress = (originalAddress) => {
199
+ const { lookup, manual, city, country, postcode } = originalAddress;
200
+ return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
201
+ };
202
+ var formatChangedAddress = (changeBusinessAddress) => {
203
+ if (!changeBusinessAddress.uprn) {
204
+ return changeBusinessAddress;
205
+ }
206
+ const {
207
+ pafOrganisationName,
208
+ flatName,
209
+ buildingName,
210
+ buildingNumberRange,
211
+ street,
212
+ doubleDependentLocality,
213
+ dependentLocality,
214
+ city,
215
+ county,
216
+ country,
217
+ postcode
218
+ } = changeBusinessAddress;
219
+ return {
220
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
221
+ address2: buildStreetLine(buildingNumberRange, street),
222
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
223
+ city: city ?? null,
224
+ county: county ?? null,
225
+ country: country ?? null,
226
+ postcode: postcode ?? null
227
+ };
228
+ };
229
+ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
230
+ const displayAddresses = addresses.map((address) => ({
231
+ value: `${address.uprn}${address.displayAddress}`,
232
+ text: address.displayAddress,
233
+ selected: (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.uprn) === address.uprn && (previouslyPickedAddress == null ? void 0 : previouslyPickedAddress.displayAddress) === address.displayAddress
234
+ }));
235
+ const hasSelectedAddress = displayAddresses.some((addr) => addr.selected);
236
+ const text = addresses.length === 1 ? "1 address found" : `${addresses.length} addresses found`;
237
+ displayAddresses.unshift({
238
+ value: "display",
239
+ text,
240
+ selected: !hasSelectedAddress
241
+ });
242
+ return displayAddresses;
243
+ };
244
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
245
+ const sortedErrors = [];
246
+ for (const section of orderedSectionsToFix) {
247
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
248
+ for (const field of fieldsInSection) {
249
+ if (errors[field]) {
250
+ sortedErrors.push({
251
+ field,
252
+ ...errors[field]
253
+ });
254
+ }
255
+ }
256
+ }
257
+ return sortedErrors;
258
+ };
259
+
260
+ // src/presenters/presenters.js
261
+ var presenters = {
262
+ formatBackLink,
263
+ formatNumber,
264
+ formatDisplayAddress,
265
+ formatOriginalAddress,
266
+ formatChangedAddress,
267
+ formatDisplayAddresses,
268
+ sortErrorsBySectionOrder
269
+ };
121
270
  export {
122
271
  mappers,
272
+ presenters,
123
273
  schemas,
124
274
  utils
125
275
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.2.11",
3
+ "version": "0.3.0",
4
4
  "description": "Shared frontend engine used by both the internal and external frontend applications.",
5
5
  "main": "./dist/index.cjs",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { mappers } from './mappers/mappers.js'
2
2
  export { schemas } from './schemas/schemas.js'
3
3
  export { utils } from './utils/utils.js'
4
+ export { presenters } from './presenters/presenters.js'
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Base presenter for formatting data for display
3
+ */
4
+
5
+ /**
6
+ * Formats the business name into back link text.
7
+ * If the business name is greater than 50 characters, it will be truncated with an ellipsis.
8
+ */
9
+ const BACK_LINK_DISPLAY_MAX = 50
10
+
11
+ export const formatBackLink = (businessName) => {
12
+ if (businessName.length > BACK_LINK_DISPLAY_MAX) {
13
+ return `Back to ${businessName.slice(0, BACK_LINK_DISPLAY_MAX)}…`
14
+ }
15
+ return `Back to ${businessName}`
16
+ }
17
+
18
+ /**
19
+ * The first time a user loads the phone numbers change page they won't have entered any data, so a payload
20
+ * or a changedNumber won't be present. If a user has a validation issue then we want to replay the payload data to them.
21
+ * We check if payload is not undefined because it could be a user has removed the 'mobile' number for example but
22
+ * incorrectly entered the telephone number so the payload for this would appear as an empty string.
23
+ *
24
+ * Payload is the priority to check and then after that if changedNumber is present then we display that value.
25
+ *
26
+ * @private
27
+ */
28
+ export const formatNumber = (payloadNumber, changedNumber, originalNumber) => {
29
+ if (payloadNumber !== undefined) {
30
+ return payloadNumber
31
+ }
32
+
33
+ if (changedNumber !== undefined) {
34
+ return changedNumber
35
+ }
36
+
37
+ return originalNumber
38
+ }
39
+
40
+ /**
41
+ * Identify the correct address to use from the DAL response.
42
+ *
43
+ * An address lookup is where a user enters a postcode and selects an address
44
+ * returned from an API. Manual input is when a user chooses to type the address in themselves.
45
+ *
46
+ * If the UPRN is populated, the user has selected an address from the lookup.
47
+ * UPRN is only returned by the lookup API; manual addresses will always have `uprn = null`.
48
+ *
49
+ * If both a building number range and a street are present, they are combined into one line so they display together.
50
+ *
51
+ * City, postcode and country are always appended to the final address array.
52
+ *
53
+ * @param {Object} address - The complete address object
54
+ *
55
+ * @returns {string[]} An array of address fields (either from lookup or manual)
56
+ *
57
+ * @private
58
+ */
59
+
60
+ export const formatDisplayAddress = (address) => {
61
+ const { lookup, manual, postcode, country, city } = address
62
+
63
+ let addressLines = []
64
+
65
+ if (lookup.uprn) {
66
+ // If the uprn is populated then the user has selected an address from the lookup
67
+ const buildingAndStreet = [
68
+ lookup.buildingNumberRange,
69
+ lookup.street
70
+ ].filter(Boolean).join(' ')
71
+
72
+ addressLines = [
73
+ lookup.pafOrganisationName,
74
+ lookup.flatName,
75
+ lookup.buildingName,
76
+ buildingAndStreet,
77
+ lookup.doubleDependentLocality,
78
+ lookup.dependentLocality,
79
+ city,
80
+ lookup.county
81
+ ]
82
+ } else {
83
+ // Otherwise the user manually entered the address
84
+ addressLines = [
85
+ manual.line1,
86
+ manual.line2,
87
+ manual.line3,
88
+ city,
89
+ manual.line4, // County
90
+ manual.line5
91
+ ]
92
+ }
93
+
94
+ return [
95
+ ...addressLines.filter(Boolean),
96
+ postcode,
97
+ country
98
+ ]
99
+ }
100
+
101
+ /**
102
+ * Combines address parts into a single line, joining non-empty values with commas.
103
+ *
104
+ * @private
105
+ */
106
+ const buildAddressLine = (parts) => {
107
+ return parts.filter(Boolean).join(', ') || null
108
+ }
109
+
110
+ /**
111
+ * Combines building range and street into a single line, joining with a space.
112
+ *
113
+ * @private
114
+ */
115
+ const buildStreetLine = (buildingRange, street) => {
116
+ return [buildingRange, street].filter(Boolean).join(' ') || null
117
+ }
118
+
119
+ /**
120
+ * Formats a lookup address (from address API) into a consistent flat structure.
121
+ *
122
+ * @private
123
+ */
124
+ const formatLookupAddress = (lookup, city, country, postcode) => ({
125
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
126
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
127
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
128
+ county: lookup.county ?? null,
129
+ city: city ?? null,
130
+ country: country ?? null,
131
+ postcode: postcode ?? null
132
+ })
133
+
134
+ /**
135
+ * Formats a manually entered address into a consistent flat structure.
136
+ *
137
+ * @private
138
+ */
139
+ const formatManualAddress = (manual, city, country, postcode) => ({
140
+ address1: manual.line1 ?? null,
141
+ address2: manual.line2 ?? null,
142
+ address3: manual.line3 ?? null,
143
+ city: city ?? null,
144
+ county: manual.line4 ?? null,
145
+ country: country ?? null,
146
+ postcode: postcode ?? null
147
+ })
148
+
149
+ /**
150
+ * Formats an original address fetched from the DAL into a flattened structure
151
+ * suitable for display or form population on the `address-enter` pages.
152
+ *
153
+ * If the address contains a UPRN, it is treated as a lookup address;
154
+ * otherwise, it is treated as a manually entered address.
155
+ *
156
+ * @param {Object} originalAddress - The full address object from the DAL
157
+ *
158
+ * @returns {Object} A flattened address object with consistent keys
159
+ */
160
+ export const formatOriginalAddress = (originalAddress) => {
161
+ const { lookup, manual, city, country, postcode } = originalAddress
162
+ return lookup.uprn
163
+ ? formatLookupAddress(lookup, city, country, postcode)
164
+ : formatManualAddress(manual, city, country, postcode)
165
+ }
166
+
167
+ /**
168
+ * Formats a changed address object into a consistent structure for form display.
169
+ *
170
+ * If the address includes a UPRN, it indicates the user selected it from an address lookup.
171
+ * In that case, the lookup fields are combined and mapped into the manual address format used by the form.
172
+ *
173
+ * If the address does not include a UPRN, it is assumed to be manually entered and returned as-is.
174
+ *
175
+ * @param {Object} changeBusinessAddress - The changed address object to format
176
+ *
177
+ * @returns {Object} A formatted address object with fields `address1`, `address2`, `address3`,
178
+ * `city`, `county`, `country`, and `postcode`.
179
+ */
180
+ export const formatChangedAddress = (changeBusinessAddress) => {
181
+ if (!changeBusinessAddress.uprn) {
182
+ // manual address (no lookup used)
183
+ return changeBusinessAddress
184
+ }
185
+
186
+ const {
187
+ pafOrganisationName,
188
+ flatName,
189
+ buildingName,
190
+ buildingNumberRange,
191
+ street,
192
+ doubleDependentLocality,
193
+ dependentLocality,
194
+ city,
195
+ county,
196
+ country,
197
+ postcode
198
+ } = changeBusinessAddress
199
+
200
+ return {
201
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
202
+ address2: buildStreetLine(buildingNumberRange, street),
203
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
204
+ city: city ?? null,
205
+ county: county ?? null,
206
+ country: country ?? null,
207
+ postcode: postcode ?? null
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Formats a list of address objects for display in a dropdown menu.
213
+ *
214
+ * Each address object is transformed into an option with:
215
+ * - `value`: a concatenation of the address UPRN and displayAddress
216
+ * - `text`: the formatted display address string
217
+ * - `selected`: true only if it matches the previously picked address
218
+ *
219
+ * A summary option is prepended to the start of the list, showing how many
220
+ * addresses were found. This summary option is selected by default unless
221
+ * a previously picked address exists.
222
+ *
223
+ * This function is shared across presenters that display address selection
224
+ * lists (e.g. personal or business address flows).
225
+ *
226
+ * Note: The `value` combines `uprn` and `displayAddress` to ensure uniqueness.
227
+ * Some addresses (for example, postcode LL55 2NF) have been observed to share
228
+ * the same UPRN, which caused incorrect selections when UPRN alone was used.
229
+ * Concatenating both fields guarantees each dropdown option has a unique value.
230
+ *
231
+ * @param {Array<Object>} addresses - List of address objects with `uprn` and `displayAddress` properties
232
+ * @param {Object} [previouslyPickedAddress] - Optional object representing the address previously selected by the user
233
+ *
234
+ * @returns {Array<Object>} Array of formatted address options ready for display
235
+ */
236
+ export const formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
237
+ const displayAddresses = addresses.map(address => ({
238
+ value: `${address.uprn}${address.displayAddress}`,
239
+ text: address.displayAddress,
240
+ selected:
241
+ previouslyPickedAddress?.uprn === address.uprn &&
242
+ previouslyPickedAddress?.displayAddress === address.displayAddress
243
+ }))
244
+
245
+ // Check if any address is already selected
246
+ const hasSelectedAddress = displayAddresses.some(addr => addr.selected)
247
+
248
+ // Add a display summary option to the beginning of the list
249
+ // e.g. "18 addresses found"
250
+ const text = addresses.length === 1 ? '1 address found' : `${addresses.length} addresses found`
251
+
252
+ displayAddresses.unshift({
253
+ value: 'display',
254
+ text,
255
+ selected: !hasSelectedAddress
256
+ })
257
+
258
+ return displayAddresses
259
+ }
260
+
261
+ /**
262
+ * Shared helper used by base presenters to sort validation errors so they
263
+ * appear in the same order as the sections and fields shown on a Fix List page.
264
+ *
265
+ * Fix List pages are built dynamically depending on:
266
+ * - which sections need fixing, and
267
+ * - which field the user originally selected.
268
+ *
269
+ * Because of that, we can’t rely on the order of the validation object.
270
+ * We need to deliberately sort the errors so they match:
271
+ * 1. The order the sections appear on the page, and
272
+ * 2. The logical order of fields within each section.
273
+ *
274
+ * This function keeps that logic in one reusable place so it can be used
275
+ * by Business, Personal, or any future Fix List presenter.
276
+ *
277
+ * It returns the errors as an array, already arranged in the correct
278
+ * display order for the UI.
279
+ */
280
+ export const sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
281
+ const sortedErrors = []
282
+
283
+ for (const section of orderedSectionsToFix) {
284
+ // A section (i.e 'address') can have multiple fields (i.e 'line1', 'line2', 'line3')
285
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || []
286
+
287
+ for (const field of fieldsInSection) {
288
+ // If there's an error for this field, add it to the sorted list with the error details
289
+ if (errors[field]) {
290
+ sortedErrors.push({
291
+ field,
292
+ ...errors[field]
293
+ })
294
+ }
295
+ }
296
+ }
297
+
298
+ return sortedErrors
299
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ formatBackLink,
3
+ formatNumber,
4
+ formatDisplayAddress,
5
+ formatOriginalAddress,
6
+ formatChangedAddress,
7
+ formatDisplayAddresses,
8
+ sortErrorsBySectionOrder
9
+ } from './base-presenter.js'
10
+
11
+ export const presenters = {
12
+ formatBackLink,
13
+ formatNumber,
14
+ formatDisplayAddress,
15
+ formatOriginalAddress,
16
+ formatChangedAddress,
17
+ formatDisplayAddresses,
18
+ sortErrorsBySectionOrder
19
+ }