@gov-cy/govcy-express-services 1.2.0 → 1.3.0-alpha
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/README.md +1 -0
- package/package.json +3 -2
- package/src/index.mjs +190 -49
- package/src/middleware/govcyConfigSiteData.mjs +4 -0
- package/src/middleware/govcyFileDeleteHandler.mjs +41 -19
- package/src/middleware/govcyFileUpload.mjs +14 -1
- package/src/middleware/govcyFileViewHandler.mjs +20 -1
- package/src/middleware/govcyFormsPostHandler.mjs +76 -47
- package/src/middleware/govcyMultipleThingsDeleteHandler.mjs +233 -0
- package/src/middleware/govcyMultipleThingsHubHandler.mjs +247 -0
- package/src/middleware/govcyMultipleThingsItemPage.mjs +460 -0
- package/src/middleware/govcyPageHandler.mjs +17 -5
- package/src/middleware/govcyPageRender.mjs +6 -0
- package/src/middleware/govcyReviewPageHandler.mjs +58 -32
- package/src/middleware/govcyReviewPostHandler.mjs +47 -23
- package/src/middleware/govcyRoutePageHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +0 -5
- package/src/public/img/Plus_24x24.svg +8 -0
- package/src/public/js/govcyFiles.js +92 -71
- package/src/resources/govcyResources.mjs +128 -0
- package/src/utils/govcyApiDetection.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +208 -67
- package/src/utils/govcyFormHandling.mjs +94 -27
- package/src/utils/govcyHandleFiles.mjs +58 -13
- package/src/utils/govcyMultipleThingsValidation.mjs +132 -0
- package/src/utils/govcySubmitData.mjs +221 -88
- package/src/utils/govcyValidator.mjs +19 -7
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
2
|
+
import { populateFormData } from "../utils/govcyFormHandling.mjs";
|
|
3
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
4
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
5
|
+
import { logger } from "../utils/govcyLogger.mjs";
|
|
6
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
7
|
+
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
8
|
+
import { getFormData } from "../utils/govcyFormHandling.mjs";
|
|
9
|
+
import { validateFormElements } from "../utils/govcyValidator.mjs";
|
|
10
|
+
import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
|
|
11
|
+
import nunjucks from "nunjucks";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared builder for add/edit item pages
|
|
15
|
+
* @param {Object} req
|
|
16
|
+
* @param {Object} res
|
|
17
|
+
* @param {Object} next
|
|
18
|
+
* @param {Object} initialData - prefilled form data ({} for add, object for edit)
|
|
19
|
+
* @param {String} actionUrl - form action URL
|
|
20
|
+
* @param {String} mode - add or edit
|
|
21
|
+
* @param {Number|null} index - index of the item being edited (null for add)
|
|
22
|
+
*/
|
|
23
|
+
function multiplePageBuilder(req, res, next, initialData, actionUrl, mode, index = null) {
|
|
24
|
+
|
|
25
|
+
// Extract siteId and pageUrl from request
|
|
26
|
+
let { siteId, pageUrl } = req.params;
|
|
27
|
+
|
|
28
|
+
// get service data
|
|
29
|
+
let serviceCopy = req.serviceData;
|
|
30
|
+
|
|
31
|
+
// 🔍 Find the page by pageUrl
|
|
32
|
+
const page = getPageConfigData(serviceCopy, pageUrl);
|
|
33
|
+
|
|
34
|
+
// --- MultipleThings sanity checks ---
|
|
35
|
+
const mtConfig = page.multipleThings;
|
|
36
|
+
if (!mtConfig) {
|
|
37
|
+
logger.debug(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`, req);
|
|
38
|
+
return handleMiddlewareError(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`, 404, next);
|
|
39
|
+
// return next(new Error(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`));
|
|
40
|
+
}
|
|
41
|
+
if (!mtConfig.listPage || !mtConfig.listPage.title) {
|
|
42
|
+
logger.debug(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, req);
|
|
43
|
+
return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
|
|
44
|
+
}
|
|
45
|
+
if (!mtConfig.itemTitleTemplate || !mtConfig.min === undefined || !mtConfig.min === null || !mtConfig.max) {
|
|
46
|
+
logger.debug(`🚨 multipleThings.itemTitleTemplate, .min and .max are required for ${siteId}/${pageUrl}`, req);
|
|
47
|
+
return handleMiddlewareError(`🚨 multipleThings.itemTitleTemplate, .min and .max are required for ${siteId}/${pageUrl}`, 404, next);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Deep copy pageTemplate to avoid modifying the original
|
|
51
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
52
|
+
|
|
53
|
+
// ----- Conditional logic comes here
|
|
54
|
+
// Check if the page has conditions and apply logic
|
|
55
|
+
const result = evaluatePageConditions(page, req.session, req.params.siteId, req);
|
|
56
|
+
if (result.result === false) {
|
|
57
|
+
return res.redirect(`/${req.params.siteId}/${result.redirect}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Change the title and H1 to append "Add" or "Change" suffix
|
|
61
|
+
const suffix =
|
|
62
|
+
mode === "add"
|
|
63
|
+
? govcyResources.staticResources.text.multipleThingsAddSuffix
|
|
64
|
+
: govcyResources.staticResources.text.multipleThingsEditSuffix;
|
|
65
|
+
|
|
66
|
+
// Append suffix to page title
|
|
67
|
+
if (typeof page?.pageData?.title === "object") {
|
|
68
|
+
for (const lang of Object.keys(page.pageData.title)) {
|
|
69
|
+
page.pageData.title[lang] += ` ${suffix[lang] || ""}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const mainSection = pageTemplateCopy.sections.find(sec => sec.name === "main");
|
|
74
|
+
if (mainSection && Array.isArray(mainSection.elements)) {
|
|
75
|
+
// Find the form element inside main
|
|
76
|
+
const formEl = mainSection.elements.find(el => el.element === "form");
|
|
77
|
+
|
|
78
|
+
if (formEl && Array.isArray(formEl.params?.elements)) {
|
|
79
|
+
// Find the H1 textElement inside the form
|
|
80
|
+
const h1Element = formEl.params.elements.find(
|
|
81
|
+
el => el.element === "textElement" && el.params?.type === "h1"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (h1Element && h1Element.params?.text) {
|
|
85
|
+
// Append the suffix based on mode
|
|
86
|
+
if (typeof h1Element.params.text === "object") {
|
|
87
|
+
for (const lang of Object.keys(h1Element.params.text)) {
|
|
88
|
+
h1Element.params.text[lang] += ` ${suffix[lang] || ""}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
//⚙️ Process forms before rendering
|
|
97
|
+
pageTemplateCopy.sections.forEach(section => {
|
|
98
|
+
section.elements.forEach(element => {
|
|
99
|
+
if (element.element === "form") {
|
|
100
|
+
logger.debug("Processing form element for multipleThings item:", element, req);
|
|
101
|
+
// set form action
|
|
102
|
+
element.params.action = actionUrl;
|
|
103
|
+
// Set form method to POST
|
|
104
|
+
element.params.method = "POST";
|
|
105
|
+
// ➕ Add CSRF token
|
|
106
|
+
element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
|
|
107
|
+
// 🔍 Find the first button with `prototypeNavigate`
|
|
108
|
+
const button = element.params.elements.find(subElement =>
|
|
109
|
+
// subElement.element === "button" && subElement.params.prototypeNavigate
|
|
110
|
+
subElement.element === "button"
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// ⚙️ Modify the button if it exists
|
|
114
|
+
if (button) {
|
|
115
|
+
// Remove `prototypeNavigate`
|
|
116
|
+
if (button.params.prototypeNavigate) {
|
|
117
|
+
delete button.params.prototypeNavigate;
|
|
118
|
+
}
|
|
119
|
+
// Set `type` to "submit"
|
|
120
|
+
button.params.type = "submit";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle form data
|
|
124
|
+
let theData = {};
|
|
125
|
+
|
|
126
|
+
//--------- Handle Validation Errors ---------
|
|
127
|
+
let validationErrors = null;
|
|
128
|
+
|
|
129
|
+
// Get all validation errors for this page (could be plain object or keyed map)
|
|
130
|
+
let validationErrorsAll = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl);
|
|
131
|
+
|
|
132
|
+
if (validationErrorsAll) {
|
|
133
|
+
// Determine whether this is add/edit
|
|
134
|
+
const { index } = req.params;
|
|
135
|
+
const isAdd = req.originalUrl.includes("/multiple/add");
|
|
136
|
+
const key = isAdd ? "add" : (index !== undefined ? index : null);
|
|
137
|
+
|
|
138
|
+
if (key) {
|
|
139
|
+
// If not keyed yet, wrap them under this key
|
|
140
|
+
if (!validationErrorsAll[key]
|
|
141
|
+
&& (validationErrorsAll.errors || validationErrorsAll.errorSummary)) {
|
|
142
|
+
validationErrorsAll = { [key]: validationErrorsAll };
|
|
143
|
+
}
|
|
144
|
+
validationErrors = validationErrorsAll[key] || null;
|
|
145
|
+
} else {
|
|
146
|
+
// Normal single-page case
|
|
147
|
+
validationErrors = validationErrorsAll;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Populate form data
|
|
152
|
+
if (validationErrors) {
|
|
153
|
+
theData = validationErrors.formData || {};
|
|
154
|
+
} else {
|
|
155
|
+
theData = initialData || {};
|
|
156
|
+
}
|
|
157
|
+
//--------- End of Handle Validation Errors ---------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
populateFormData(
|
|
161
|
+
element.params.elements,
|
|
162
|
+
theData,
|
|
163
|
+
validationErrors,
|
|
164
|
+
req.session,
|
|
165
|
+
siteId,
|
|
166
|
+
pageUrl,
|
|
167
|
+
req.globalLang,
|
|
168
|
+
null,
|
|
169
|
+
req.query.route,
|
|
170
|
+
mode,
|
|
171
|
+
index
|
|
172
|
+
);
|
|
173
|
+
// if there are validation errors, add an error summary
|
|
174
|
+
if (validationErrors?.errorSummary?.length > 0) {
|
|
175
|
+
element.params.elements.unshift(
|
|
176
|
+
govcyResources.errorSummary(validationErrors.errorSummary)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logger.debug("Processed multipleThings item form element:", element, req);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Attach processed page
|
|
186
|
+
req.processedPage = {
|
|
187
|
+
pageData: {
|
|
188
|
+
"site": serviceCopy.site,
|
|
189
|
+
"pageData": {
|
|
190
|
+
"title": page.pageData.title,
|
|
191
|
+
"layout": page.pageData.layout,
|
|
192
|
+
"mainLayout": page.pageData.mainLayout
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
pageTemplate: pageTemplateCopy
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
logger.debug("Processed multipleThings item page:", req.processedPage, req);
|
|
199
|
+
next();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* GET handler for add new item
|
|
204
|
+
*/
|
|
205
|
+
export function govcyMultipleThingsAddHandler() {
|
|
206
|
+
return (req, res, next) => {
|
|
207
|
+
try {
|
|
208
|
+
|
|
209
|
+
const { siteId, pageUrl } = req.params;
|
|
210
|
+
const route = req.query?.route;
|
|
211
|
+
const actionUrl = `/${siteId}/${pageUrl}/multiple/add${route === "review" ? `?route=review` : ""}`;
|
|
212
|
+
// Use draft if it exists, otherwise seed an empty one
|
|
213
|
+
let draft = dataLayer.getMultipleDraft(req.session, siteId, pageUrl);
|
|
214
|
+
if (!draft) {
|
|
215
|
+
draft = {};
|
|
216
|
+
dataLayer.setMultipleDraft(req.session, siteId, pageUrl, draft);
|
|
217
|
+
}
|
|
218
|
+
multiplePageBuilder(req, res, next, draft, actionUrl, "add", null);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return next(error);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* GET handler for edit existing item
|
|
227
|
+
*/
|
|
228
|
+
export function govcyMultipleThingsEditHandler() {
|
|
229
|
+
return (req, res, next) => {
|
|
230
|
+
try {
|
|
231
|
+
const { siteId, pageUrl, index } = req.params;
|
|
232
|
+
const route = req.query?.route;
|
|
233
|
+
|
|
234
|
+
// Validate index
|
|
235
|
+
const idx = parseInt(index, 10);
|
|
236
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
237
|
+
if (!Array.isArray(items)) items = [];
|
|
238
|
+
|
|
239
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
240
|
+
return handleMiddlewareError(
|
|
241
|
+
`🚨 multipleThings edit index not found for ${siteId}/${pageUrl} (index=${index})`,
|
|
242
|
+
404,
|
|
243
|
+
next
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const initialData = items[idx];
|
|
248
|
+
const actionUrl = `/${siteId}/${pageUrl}/multiple/edit/${idx}${route === "review" ? `?route=review` : ""}`;
|
|
249
|
+
multiplePageBuilder(req, res, next, initialData, actionUrl, "edit", idx);
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return next(error);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
*
|
|
258
|
+
* POST handler for adding a new item
|
|
259
|
+
*/
|
|
260
|
+
export function govcyMultipleThingsAddPostHandler() {
|
|
261
|
+
return (req, res, next) => {
|
|
262
|
+
try {
|
|
263
|
+
const { siteId, pageUrl } = req.params;
|
|
264
|
+
const service = req.serviceData;
|
|
265
|
+
const page = getPageConfigData(service, pageUrl);
|
|
266
|
+
|
|
267
|
+
// 1. Check page conditions
|
|
268
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
269
|
+
if (conditionResult.result === false) {
|
|
270
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect, (req.query?.route === "review" ? "review" : "")));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2. Find form element
|
|
274
|
+
let formElement = null;
|
|
275
|
+
for (const section of page.pageTemplate.sections) {
|
|
276
|
+
formElement = section.elements.find(el => el.element === "form");
|
|
277
|
+
if (formElement) break;
|
|
278
|
+
}
|
|
279
|
+
if (!formElement) {
|
|
280
|
+
return handleMiddlewareError("🚨 Form definition not found.", 500, next);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 3. Get form data
|
|
284
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl);
|
|
285
|
+
|
|
286
|
+
// 4. Validate
|
|
287
|
+
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
288
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
289
|
+
// store validation errors under the "add" key
|
|
290
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData, "add");
|
|
291
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 5. Commit new item into array
|
|
295
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
296
|
+
if (!Array.isArray(items)) items = [];
|
|
297
|
+
|
|
298
|
+
const mtConfig = page.multipleThings;
|
|
299
|
+
// Check max limit
|
|
300
|
+
// Sanity check
|
|
301
|
+
if (!mtConfig || !mtConfig.max) {
|
|
302
|
+
return handleMiddlewareError("🚨 multipleThings.max not configured.", 500, next);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!mtConfig.listPage || !mtConfig.listPage.title) {
|
|
306
|
+
return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 6. Enforce max limit
|
|
310
|
+
if (items.length >= mtConfig.max) {
|
|
311
|
+
// process message
|
|
312
|
+
// Deep copy page title (so we don’t mutate template)
|
|
313
|
+
let maxMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMaxMessage));
|
|
314
|
+
// Replace label placeholders on page title
|
|
315
|
+
for (const lang of Object.keys(maxMsg)) {
|
|
316
|
+
maxMsg[lang] = maxMsg[lang].replace("{{max}}", mtConfig.max);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
|
|
320
|
+
{
|
|
321
|
+
_global:
|
|
322
|
+
{
|
|
323
|
+
message: maxMsg,
|
|
324
|
+
pageUrl: govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : ""))
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
formData,
|
|
328
|
+
"add"
|
|
329
|
+
);
|
|
330
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 7. Check dedupe
|
|
334
|
+
if (mtConfig.dedupe) {
|
|
335
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
336
|
+
const newTitle = env.renderString(mtConfig.itemTitleTemplate, formData);
|
|
337
|
+
const duplicate = items.some(it => env.renderString(mtConfig.itemTitleTemplate, it) === newTitle);
|
|
338
|
+
if (duplicate) {
|
|
339
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
|
|
340
|
+
{
|
|
341
|
+
_global:
|
|
342
|
+
{
|
|
343
|
+
message: govcyResources.staticResources.text.multipleThingsDedupeMessage
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
formData,
|
|
347
|
+
"add"
|
|
348
|
+
);
|
|
349
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 8. Save item + clear draft
|
|
354
|
+
items.push(formData);
|
|
355
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, items);
|
|
356
|
+
dataLayer.clearMultipleDraft(req.session, siteId, pageUrl);
|
|
357
|
+
|
|
358
|
+
// 9. Temp save
|
|
359
|
+
(async () => { try { await tempSaveIfConfigured(req.session, service, siteId); } catch (e) { } })();
|
|
360
|
+
|
|
361
|
+
// 10. Redirect back to the hub
|
|
362
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : "")));
|
|
363
|
+
} catch (error) { return next(error); }
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* POST handler for editing an existing item
|
|
369
|
+
*/
|
|
370
|
+
export function govcyMultipleThingsEditPostHandler() {
|
|
371
|
+
return (req, res, next) => {
|
|
372
|
+
try {
|
|
373
|
+
const { siteId, pageUrl, index } = req.params;
|
|
374
|
+
const service = req.serviceData;
|
|
375
|
+
const page = getPageConfigData(service, pageUrl);
|
|
376
|
+
|
|
377
|
+
// 1. Check page conditions
|
|
378
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
379
|
+
if (conditionResult.result === false) {
|
|
380
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect, (req.query?.route === "review" ? "review" : "")));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 2. Find form element
|
|
384
|
+
let formElement = null;
|
|
385
|
+
for (const section of page.pageTemplate.sections) {
|
|
386
|
+
formElement = section.elements.find(el => el.element === "form");
|
|
387
|
+
if (formElement) break;
|
|
388
|
+
}
|
|
389
|
+
if (!formElement) {
|
|
390
|
+
return handleMiddlewareError("🚨 Form definition not found.", 500, next);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 3. Get form data
|
|
394
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl, index);
|
|
395
|
+
|
|
396
|
+
// 4. Get current items array
|
|
397
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
398
|
+
if (!Array.isArray(items)) items = [];
|
|
399
|
+
|
|
400
|
+
const idx = parseInt(index, 10);
|
|
401
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
402
|
+
return handleMiddlewareError(
|
|
403
|
+
`🚨 multipleThings edit index not found for ${siteId}/${pageUrl} (index=${index})`,
|
|
404
|
+
404,
|
|
405
|
+
next
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 5. Validate
|
|
410
|
+
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
411
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
412
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData, index);
|
|
413
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 6. Dedupe check (skip current index)
|
|
417
|
+
const mtConfig = page.multipleThings;
|
|
418
|
+
|
|
419
|
+
// Sanity check
|
|
420
|
+
if (!mtConfig) {
|
|
421
|
+
return handleMiddlewareError("🚨 multipleThings not configured.", 500, next);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!mtConfig.listPage || !mtConfig.listPage.title) {
|
|
425
|
+
return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (mtConfig?.dedupe) {
|
|
429
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
430
|
+
const newTitle = env.renderString(mtConfig.itemTitleTemplate, formData);
|
|
431
|
+
const duplicate = items.some((it, i) =>
|
|
432
|
+
i !== idx && env.renderString(mtConfig.itemTitleTemplate, it) === newTitle
|
|
433
|
+
);
|
|
434
|
+
if (duplicate) {
|
|
435
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
|
|
436
|
+
{ _global: { message: govcyResources.staticResources.text.multipleThingsDedupeMessage } },
|
|
437
|
+
formData,
|
|
438
|
+
index
|
|
439
|
+
);
|
|
440
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 7. Save back into array
|
|
445
|
+
items[idx] = formData;
|
|
446
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, items);
|
|
447
|
+
|
|
448
|
+
// 8. Temp save
|
|
449
|
+
(async () => {
|
|
450
|
+
try { await tempSaveIfConfigured(req.session, service, siteId); }
|
|
451
|
+
catch (e) { /* already logged */ }
|
|
452
|
+
})();
|
|
453
|
+
|
|
454
|
+
// 9. Redirect back to the hub
|
|
455
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : "")));
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return next(error);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as govcyResources from "../resources/govcyResources.mjs";
|
|
|
4
4
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
5
5
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
6
6
|
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
7
|
+
import { govcyMultipleThingsHubHandler } from "./govcyMultipleThingsHubHandler.mjs";
|
|
7
8
|
// import {flattenContext, evaluateExpressionWithFlattening, evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -38,11 +39,13 @@ export function govcyPageHandler() {
|
|
|
38
39
|
if (result.result === false) {
|
|
39
40
|
return res.redirect(`/${req.params.siteId}/${result.redirect}`);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
44
|
-
|
|
42
|
+
|
|
43
|
+
// ----- MultipleThings hub handling
|
|
44
|
+
if (page.multipleThings) {
|
|
45
|
+
logger.debug(`Rendering multipleThings hub for pageUrl: ${pageUrl}`, req);
|
|
46
|
+
return govcyMultipleThingsHubHandler(req, res, next, page, serviceCopy);
|
|
45
47
|
}
|
|
48
|
+
|
|
46
49
|
//⚙️ Process forms before rendering
|
|
47
50
|
pageTemplateCopy.sections.forEach(section => {
|
|
48
51
|
section.elements.forEach(element => {
|
|
@@ -87,7 +90,16 @@ export function govcyPageHandler() {
|
|
|
87
90
|
}
|
|
88
91
|
//--------- End of Handle Validation Errors ---------
|
|
89
92
|
|
|
90
|
-
populateFormData(
|
|
93
|
+
populateFormData(
|
|
94
|
+
element.params.elements,
|
|
95
|
+
theData,
|
|
96
|
+
validationErrors,
|
|
97
|
+
req.session,
|
|
98
|
+
siteId,
|
|
99
|
+
pageUrl,
|
|
100
|
+
req.globalLang,
|
|
101
|
+
null,
|
|
102
|
+
req.query.route);
|
|
91
103
|
// if there are validation errors, add an error summary
|
|
92
104
|
if (validationErrors?.errorSummary?.length > 0) {
|
|
93
105
|
element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
|
|
2
2
|
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
3
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Middleware function to render pages using the GovCy Frontend Renderer.
|
|
@@ -18,6 +19,11 @@ export function renderGovcyPage() {
|
|
|
18
19
|
const renderer = new govcyFrontendRenderer();
|
|
19
20
|
const { processedPage } = req;
|
|
20
21
|
processedPage.pageTemplate.sections.push(afterBody);
|
|
22
|
+
|
|
23
|
+
//if user is logged in add the user name section in the page template
|
|
24
|
+
if (dataLayer.getUser(req.session)) {
|
|
25
|
+
processedPage.pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
|
|
26
|
+
}
|
|
21
27
|
const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
|
|
22
28
|
res.send(html);
|
|
23
29
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
2
2
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
3
3
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
4
|
-
import {preparePrintFriendlyData
|
|
4
|
+
import { preparePrintFriendlyData, generateReviewSummary } from "../utils/govcySubmitData.mjs";
|
|
5
5
|
import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
|
|
6
|
+
import { buildMultipleThingsValidationSummary } from "../utils/govcyMultipleThingsValidation.mjs";
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -13,23 +14,23 @@ export function govcyReviewPageHandler() {
|
|
|
13
14
|
return (req, res, next) => {
|
|
14
15
|
try {
|
|
15
16
|
const { siteId } = req.params;
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
// Create a deep copy of the service to avoid modifying the original
|
|
18
19
|
let serviceCopy = req.serviceData;
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
// Deep copy renderer pageData from
|
|
21
22
|
let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
// Handle isTesting
|
|
24
25
|
pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
// Base page template structure
|
|
27
28
|
let pageTemplate = {
|
|
28
29
|
sections: [
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
30
|
+
// {
|
|
31
|
+
// name: "beforeMain",
|
|
32
|
+
// elements: [govcyResources.staticResources.elements.backLink]
|
|
33
|
+
// }
|
|
33
34
|
]
|
|
34
35
|
};
|
|
35
36
|
// Construct page title
|
|
@@ -40,12 +41,12 @@ export function govcyReviewPageHandler() {
|
|
|
40
41
|
// if serviceCopy has site.reviewPageHeader use it otherwise use the static resource. it should test if serviceCopy.site.reviewPageHeader[req.globalLang] exists
|
|
41
42
|
text: (
|
|
42
43
|
serviceCopy?.site?.reviewPageHeader?.[req.globalLang]
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
? serviceCopy.site.reviewPageHeader
|
|
45
|
+
: govcyResources.staticResources.text.checkYourAnswersTitle
|
|
45
46
|
)
|
|
46
47
|
}
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
// Construct submit button
|
|
50
51
|
const submitButton = {
|
|
51
52
|
element: "form",
|
|
@@ -66,40 +67,65 @@ export function govcyReviewPageHandler() {
|
|
|
66
67
|
}
|
|
67
68
|
// Generate the summary list using the utility function
|
|
68
69
|
let printFriendlyData = preparePrintFriendlyData(req, siteId, serviceCopy);
|
|
69
|
-
let summaryList = generateReviewSummary(printFriendlyData,req, siteId);
|
|
70
|
-
|
|
70
|
+
let summaryList = generateReviewSummary(printFriendlyData, req, siteId);
|
|
71
|
+
|
|
72
|
+
let mainElements = [];
|
|
71
73
|
//--------- Handle Validation Errors ---------
|
|
72
74
|
// Check if validation errors exist in the session
|
|
73
75
|
const validationErrors = dataLayer.getSiteSubmissionErrors(req.session, siteId);
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
let summaryItems = [];
|
|
77
|
+
|
|
78
|
+
if (validationErrors && validationErrors.errors) {
|
|
79
|
+
for (const [pageUrl, err] of Object.entries(validationErrors.errors)) {
|
|
80
|
+
// Handle multipleThings hub errors
|
|
81
|
+
if (err.type === "multipleThings") {
|
|
82
|
+
const mtErrors = err.hub?.errors || {};
|
|
83
|
+
// Build summary items for multipleThings errors
|
|
84
|
+
summaryItems.push(...buildMultipleThingsValidationSummary( serviceCopy, mtErrors, siteId, pageUrl, req, "review", false));
|
|
85
|
+
} else {
|
|
86
|
+
// Normal pages: loop through field errors
|
|
87
|
+
for (const [fieldKey, fieldErr] of Object.entries(err)) {
|
|
88
|
+
// Skip type field
|
|
89
|
+
if (fieldKey === "type") continue;
|
|
90
|
+
// Push each field error to summary items
|
|
91
|
+
summaryItems.push({
|
|
92
|
+
link: govcyResources.constructPageUrl(siteId, fieldErr.pageUrl, "review"),
|
|
93
|
+
text: fieldErr.message
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (summaryItems.length > 0) {
|
|
101
|
+
mainElements.unshift(govcyResources.errorSummary(summaryItems));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// const validationErrors = dataLayer.getSiteSubmissionErrors(req.session, siteId);
|
|
105
|
+
// let mainElements = [];
|
|
106
|
+
// if (validationErrors ) {
|
|
107
|
+
// for (const error in validationErrors.errors) {
|
|
108
|
+
// validationErrors.errorSummary.push({
|
|
109
|
+
// link: govcyResources.constructPageUrl(siteId, validationErrors.errors[error].pageUrl, "review"), //`/${siteId}/${error.pageUrl}`,
|
|
110
|
+
// text: validationErrors.errors[error].message
|
|
111
|
+
// });
|
|
112
|
+
// }
|
|
113
|
+
// mainElements.push(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
114
|
+
// }
|
|
84
115
|
//--------- End Handle Validation Errors ---------
|
|
85
116
|
|
|
86
117
|
// Add elements to the main section, the H1, summary list, the submit button and the JS
|
|
87
|
-
mainElements.push(pageH1,
|
|
88
|
-
summaryList,
|
|
118
|
+
mainElements.push(pageH1,
|
|
119
|
+
summaryList,
|
|
89
120
|
submitButton
|
|
90
121
|
);
|
|
91
122
|
// Append generated summary list to the page template
|
|
92
123
|
pageTemplate.sections.push({ name: "main", elements: mainElements });
|
|
93
|
-
|
|
94
|
-
//if user is logged in add he user bane section in the page template
|
|
95
|
-
if (dataLayer.getUser(req.session)) {
|
|
96
|
-
pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
|
|
97
|
-
}
|
|
98
|
-
|
|
124
|
+
|
|
99
125
|
//prepare pageData
|
|
100
126
|
pageData.site = serviceCopy.site;
|
|
101
127
|
pageData.pageData.title = govcyResources.staticResources.text.checkYourAnswersTitle;
|
|
102
|
-
|
|
128
|
+
|
|
103
129
|
// Attach processed page data to the request
|
|
104
130
|
req.processedPage = {
|
|
105
131
|
pageData: pageData,
|