@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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
2
2
|
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
3
|
-
import { validateFormElements
|
|
3
|
+
import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
|
|
4
4
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
5
5
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
6
6
|
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
7
7
|
import { getFormData } from "../utils/govcyFormHandling.mjs"
|
|
8
|
-
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
8
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
9
9
|
import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
|
|
10
|
+
import { validateMultipleThings } from "../utils/govcyMultipleThingsValidation.mjs";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -16,13 +17,13 @@ export function govcyFormsPostHandler() {
|
|
|
16
17
|
return (req, res, next) => {
|
|
17
18
|
try {
|
|
18
19
|
const { siteId, pageUrl } = req.params;
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
// ⤵️ Load service and check if it exists
|
|
21
22
|
const service = req.serviceData;
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
// ⤵️ Find the current page based on the URL
|
|
24
25
|
const page = getPageConfigData(service, pageUrl);
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
// ----- Conditional logic comes here
|
|
27
28
|
// ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
|
|
28
29
|
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
@@ -30,61 +31,88 @@ export function govcyFormsPostHandler() {
|
|
|
30
31
|
logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
|
|
31
32
|
return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
// 🔍 Find the form definition inside `pageTemplate.sections`
|
|
35
36
|
let formElement = null;
|
|
36
37
|
for (const section of page.pageTemplate.sections) {
|
|
37
38
|
formElement = section.elements.find(el => el.element === "form");
|
|
38
39
|
if (formElement) break;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
if (!formElement) {
|
|
42
43
|
return handleMiddlewareError("🚨 Form definition not found.", 500, next);
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
// const formData = req.body; // Submitted data
|
|
46
|
-
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
47
|
-
|
|
48
|
-
// ☑️ Start validation from top-level form elements
|
|
49
|
-
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
50
|
-
|
|
51
|
-
// ❌ Return validation errors if any exist
|
|
52
|
-
if (Object.keys(validationErrors).length > 0) {
|
|
53
|
-
logger.debug("🚨 Validation errors:", validationErrors, req);
|
|
54
|
-
logger.info("🚨 Validation errors on:", req.originalUrl);
|
|
55
|
-
// store the validation errors
|
|
56
|
-
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData);
|
|
57
|
-
//redirect to the same page with error summary
|
|
58
|
-
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
//⤴️ Store validated form data in session
|
|
62
|
-
dataLayer.storePageData(req.session, siteId, pageUrl, formData);
|
|
63
|
-
|
|
64
|
-
// 🔄 Fire-and-forget temporary save (non-blocking)
|
|
65
|
-
(async () => {
|
|
66
|
-
try { await tempSaveIfConfigured(req.session, service, siteId); }
|
|
67
|
-
catch (e) { /* already logged internally */ }
|
|
68
|
-
})();
|
|
69
|
-
|
|
70
|
-
logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
|
|
71
|
-
logger.info("✅ Form submitted successfully:", req.originalUrl);
|
|
72
|
-
|
|
73
|
-
// 🔍 Determine next page (if applicable)
|
|
45
|
+
|
|
74
46
|
let nextPage = null;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
47
|
+
|
|
48
|
+
// ----- MultipleThings hub handling -----
|
|
49
|
+
if (page.multipleThings) {
|
|
50
|
+
// Get current items from session
|
|
51
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl) || [];
|
|
52
|
+
if (!Array.isArray(items)) {
|
|
53
|
+
items = [];
|
|
54
|
+
}
|
|
55
|
+
// Validate the items array against multipleThings config (min, max, per-item validation)
|
|
56
|
+
const errors = validateMultipleThings(page, items, service.site.lang);
|
|
57
|
+
|
|
58
|
+
// If there are validation errors, store them in session and redirect back to the hub with error summary
|
|
59
|
+
if (Object.keys(errors).length > 0) {
|
|
60
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, errors, null, "hub");
|
|
61
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No validation errors, proceed to next page
|
|
65
|
+
logger.debug("✅ multipleThings hub validated successfully:", items, req);
|
|
66
|
+
logger.info("✅ multipleThings hub validated successfully:", req.originalUrl);
|
|
67
|
+
nextPage = req.query.route === "review"
|
|
68
|
+
? govcyResources.constructPageUrl(siteId, "review")
|
|
69
|
+
: page.pageData.nextPage;
|
|
70
|
+
|
|
71
|
+
} else { // Regular form page
|
|
72
|
+
|
|
73
|
+
// const formData = req.body; // Submitted data
|
|
74
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
75
|
+
|
|
76
|
+
// ☑️ Start validation from top-level form elements
|
|
77
|
+
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
78
|
+
|
|
79
|
+
// ❌ Return validation errors if any exist
|
|
80
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
81
|
+
logger.debug("🚨 Validation errors:", validationErrors, req);
|
|
82
|
+
logger.info("🚨 Validation errors on:", req.originalUrl);
|
|
83
|
+
// store the validation errors
|
|
84
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData);
|
|
85
|
+
//redirect to the same page with error summary
|
|
86
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//⤴️ Store validated form data in session
|
|
90
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, formData);
|
|
91
|
+
|
|
92
|
+
// 🔄 Fire-and-forget temporary save (non-blocking)
|
|
93
|
+
(async () => {
|
|
94
|
+
try { await tempSaveIfConfigured(req.session, service, siteId); }
|
|
95
|
+
catch (e) { /* already logged internally */ }
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
|
|
99
|
+
logger.info("✅ Form submitted successfully:", req.originalUrl);
|
|
100
|
+
|
|
101
|
+
// 🔍 Determine next page (if applicable)
|
|
102
|
+
for (const section of page.pageTemplate.sections) {
|
|
103
|
+
const form = section.elements.find(el => el.element === "form");
|
|
104
|
+
if (form) {
|
|
105
|
+
//handle review route
|
|
106
|
+
if (req.query.route === "review") {
|
|
107
|
+
nextPage = govcyResources.constructPageUrl(siteId, "review");
|
|
108
|
+
} else {
|
|
109
|
+
nextPage = page.pageData.nextPage;
|
|
110
|
+
//nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate;
|
|
111
|
+
}
|
|
84
112
|
}
|
|
85
113
|
}
|
|
114
|
+
|
|
86
115
|
}
|
|
87
|
-
|
|
88
116
|
// ➡️ Redirect to the next page if defined, otherwise return success
|
|
89
117
|
if (nextPage) {
|
|
90
118
|
logger.debug("🔄 Redirecting to next page:", nextPage, req);
|
|
@@ -92,6 +120,7 @@ export function govcyFormsPostHandler() {
|
|
|
92
120
|
return res.redirect(govcyResources.constructPageUrl(siteId, `${nextPage.split('/').pop()}`));
|
|
93
121
|
}
|
|
94
122
|
res.json({ success: true, message: "Form submitted successfully" });
|
|
123
|
+
|
|
95
124
|
} catch (error) {
|
|
96
125
|
return next(error); // Pass error to govcyHttpErrorHandler
|
|
97
126
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
2
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
3
|
+
import { logger } from "../utils/govcyLogger.mjs";
|
|
4
|
+
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
5
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
6
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
7
|
+
import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
|
|
8
|
+
import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
|
|
9
|
+
import nunjucks from "nunjucks";
|
|
10
|
+
import { URL } from "url";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Middleware to show a confirmation page before deleting a multipleThings item.
|
|
14
|
+
*/
|
|
15
|
+
export function govcyMultipleThingsDeletePageHandler() {
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
try {
|
|
18
|
+
const { siteId, pageUrl, index } = req.params;
|
|
19
|
+
const serviceCopy = req.serviceData;
|
|
20
|
+
const page = getPageConfigData(serviceCopy, pageUrl);
|
|
21
|
+
|
|
22
|
+
// --- Sanity checks ---
|
|
23
|
+
const mtConfig = page.multipleThings;
|
|
24
|
+
if (!mtConfig) {
|
|
25
|
+
return handleMiddlewareError(`🚨 multipleThings config not found for ${siteId}/${pageUrl}`, 404, next);
|
|
26
|
+
}
|
|
27
|
+
if (!mtConfig.listPage || !mtConfig.listPage.title) {
|
|
28
|
+
return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
|
|
29
|
+
}
|
|
30
|
+
if (!mtConfig.itemTitleTemplate || !mtConfig.min === undefined || !mtConfig.min === null || !mtConfig.max) {
|
|
31
|
+
return handleMiddlewareError(`🚨 multipleThings.itemTitleTemplate, .min and .max are required for ${siteId}/${pageUrl}`, 404, next);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Conditions ---
|
|
35
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
36
|
+
if (conditionResult.result === false) {
|
|
37
|
+
logger.debug("⛔️ Page condition evaluated to true on GET — skipping and redirecting:", conditionResult);
|
|
38
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Validate index ---
|
|
42
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
43
|
+
if (!Array.isArray(items)) items = [];
|
|
44
|
+
const idx = parseInt(index, 10);
|
|
45
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
46
|
+
return handleMiddlewareError(`🚨 multipleThings delete index not found for ${siteId}/${pageUrl} (index=${index})`, 404, next);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const item = items[idx];
|
|
50
|
+
|
|
51
|
+
// --- Build page title ---
|
|
52
|
+
// We’ll use the itemTitleTemplate to render the item title
|
|
53
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
54
|
+
const itemTitle = env.renderString(mtConfig.itemTitleTemplate, item);
|
|
55
|
+
|
|
56
|
+
// Base page template
|
|
57
|
+
let pageTemplate = {
|
|
58
|
+
sections: [
|
|
59
|
+
{
|
|
60
|
+
name: "beforeMain",
|
|
61
|
+
elements: [govcyResources.staticResources.elements.backLink]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Deep copy page title (so we don’t mutate template)
|
|
67
|
+
let pageTitle = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsDeleteTitle));
|
|
68
|
+
|
|
69
|
+
// Replace label placeholders on page title
|
|
70
|
+
for (const lang of Object.keys(pageTitle)) {
|
|
71
|
+
pageTitle[lang] = pageTitle[lang].replace("{{item}}", itemTitle);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Radios for confirmation
|
|
75
|
+
const pageRadios = {
|
|
76
|
+
element: "radios",
|
|
77
|
+
params: {
|
|
78
|
+
id: "deleteItem",
|
|
79
|
+
name: "deleteItem",
|
|
80
|
+
legend: pageTitle,
|
|
81
|
+
isPageHeading: true,
|
|
82
|
+
classes: "govcy-mb-6",
|
|
83
|
+
items: [
|
|
84
|
+
{ value: "yes", text: govcyResources.staticResources.text.deleteYesOption },
|
|
85
|
+
{ value: "no", text: govcyResources.staticResources.text.deleteNoOption }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Form element
|
|
91
|
+
const formElement = {
|
|
92
|
+
element: "form",
|
|
93
|
+
params: {
|
|
94
|
+
action: govcyResources.constructPageUrl(siteId, `${pageUrl}/multiple/delete/${index}`, req.query.route === "review" ? "review" : ""),
|
|
95
|
+
method: "POST",
|
|
96
|
+
elements: [
|
|
97
|
+
pageRadios,
|
|
98
|
+
{
|
|
99
|
+
element: "button",
|
|
100
|
+
params: {
|
|
101
|
+
type: "submit",
|
|
102
|
+
text: govcyResources.staticResources.text.continue
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
govcyResources.csrfTokenInput(req.csrfToken())
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// --------- Handle Validation Errors ---------
|
|
111
|
+
// Add validation errors if present
|
|
112
|
+
const validationErrors = [];
|
|
113
|
+
let mainElements = [];
|
|
114
|
+
if (req?.query?.hasError) {
|
|
115
|
+
validationErrors.push({
|
|
116
|
+
link: "#deleteItem-option-1",
|
|
117
|
+
text: govcyResources.staticResources.text.multipleThingsDeleteValidationError
|
|
118
|
+
});
|
|
119
|
+
mainElements.push(govcyResources.errorSummary(validationErrors));
|
|
120
|
+
formElement.params.elements[0].params.error = govcyResources.staticResources.text.multipleThingsDeleteValidationError;
|
|
121
|
+
}
|
|
122
|
+
//--------- End Handle Validation Errors ---------
|
|
123
|
+
|
|
124
|
+
mainElements.push(formElement);
|
|
125
|
+
pageTemplate.sections.push({ name: "main", elements: mainElements });
|
|
126
|
+
|
|
127
|
+
const pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
|
|
128
|
+
// Handle isTesting
|
|
129
|
+
pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
|
|
130
|
+
|
|
131
|
+
pageData.site = serviceCopy.site;
|
|
132
|
+
|
|
133
|
+
pageData.pageData.title = pageTitle;
|
|
134
|
+
|
|
135
|
+
req.processedPage = { pageData, pageTemplate };
|
|
136
|
+
logger.debug("Processed delete item page data:", req.processedPage);
|
|
137
|
+
next();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return next(error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Middleware to handle delete item POST for multipleThings
|
|
147
|
+
*/
|
|
148
|
+
export function govcyMultipleThingsDeletePostHandler() {
|
|
149
|
+
return (req, res, next) => {
|
|
150
|
+
try {
|
|
151
|
+
const { siteId, pageUrl, index } = req.params;
|
|
152
|
+
const service = req.serviceData;
|
|
153
|
+
const page = getPageConfigData(service, pageUrl);
|
|
154
|
+
|
|
155
|
+
// --- Sanity checks ---
|
|
156
|
+
const mtConfig = page.multipleThings;
|
|
157
|
+
if (!mtConfig) {
|
|
158
|
+
return handleMiddlewareError(
|
|
159
|
+
`🚨 multipleThings config not found for ${siteId}/${pageUrl}`,
|
|
160
|
+
404,
|
|
161
|
+
next
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Conditions ---
|
|
166
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
167
|
+
if (conditionResult.result === false) {
|
|
168
|
+
logger.debug(
|
|
169
|
+
"⛔️ Page condition evaluated to true on POST — skipping and redirecting:",
|
|
170
|
+
conditionResult
|
|
171
|
+
);
|
|
172
|
+
return res.redirect(
|
|
173
|
+
govcyResources.constructPageUrl(siteId, conditionResult.redirect)
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Validate index ---
|
|
178
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
179
|
+
if (!Array.isArray(items)) items = [];
|
|
180
|
+
const idx = parseInt(index, 10);
|
|
181
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
182
|
+
return handleMiddlewareError(
|
|
183
|
+
`🚨 multipleThings delete index not found for ${siteId}/${pageUrl} (index=${index})`,
|
|
184
|
+
404,
|
|
185
|
+
next
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Validate form value ---
|
|
190
|
+
if (
|
|
191
|
+
!req?.body?.deleteItem ||
|
|
192
|
+
(req.body.deleteItem !== "yes" && req.body.deleteItem !== "no")
|
|
193
|
+
) {
|
|
194
|
+
logger.debug(
|
|
195
|
+
"⛔️ No deleteItem value provided on POST — redirecting with error:",
|
|
196
|
+
req.body
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${pageUrl}/multiple/delete/${index}`;
|
|
200
|
+
let myUrl = new URL(pageBaseReturnUrl);
|
|
201
|
+
if (req.query.route === "review") {
|
|
202
|
+
myUrl.searchParams.set("route", "review");
|
|
203
|
+
}
|
|
204
|
+
myUrl.searchParams.set("hasError", "1");
|
|
205
|
+
|
|
206
|
+
return res.redirect(
|
|
207
|
+
govcyResources.constructErrorSummaryUrl(myUrl.pathname + myUrl.search)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Handle deletion ---
|
|
212
|
+
if (req.body.deleteItem === "yes") {
|
|
213
|
+
items.splice(idx, 1); // remove the item
|
|
214
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, items);
|
|
215
|
+
logger.info(`Item deleted by user`, { siteId, pageUrl, index });
|
|
216
|
+
|
|
217
|
+
//Temp save
|
|
218
|
+
(async () => { try { await tempSaveIfConfigured(req.session, service, siteId); } catch (e) { } })();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Redirect back to the hub ---
|
|
222
|
+
const hubUrl = govcyResources.constructPageUrl(
|
|
223
|
+
siteId,
|
|
224
|
+
pageUrl,
|
|
225
|
+
req.query.route === "review" ? "review" : ""
|
|
226
|
+
);
|
|
227
|
+
return res.redirect(hubUrl);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error("Error in govcyMultipleThingsDeletePostHandler middleware:", error);
|
|
230
|
+
return next(error);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/middleware/govcyMultipleThingsHubHandler.mjs
|
|
2
|
+
import e from "express";
|
|
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 { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
7
|
+
import { buildMultipleThingsValidationSummary } from "../utils/govcyMultipleThingsValidation.mjs";
|
|
8
|
+
import nunjucks from "nunjucks";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Middleware to render a MultipleThings hub page.
|
|
12
|
+
|
|
13
|
+
* @param {Object} page - The page config from the JSON (includes multipleThings block)
|
|
14
|
+
* @param {Object} serviceCopy - The service config (full JSON)
|
|
15
|
+
*/
|
|
16
|
+
export function govcyMultipleThingsHubHandler(req, res, next, page, serviceCopy) {
|
|
17
|
+
try {
|
|
18
|
+
const { siteId, pageUrl } = req.params;
|
|
19
|
+
const mtConfig = page.multipleThings;
|
|
20
|
+
let addLinkCounter = 0;
|
|
21
|
+
|
|
22
|
+
// Sanity checks
|
|
23
|
+
if (!mtConfig) {
|
|
24
|
+
logger.debug("🚨 multipleThings config not found in page config", req);
|
|
25
|
+
return handleMiddlewareError(`🚨 multipleThings config not found in page config`, 500, next);
|
|
26
|
+
}
|
|
27
|
+
if (!mtConfig.listPage
|
|
28
|
+
|| !mtConfig.listPage.title) {
|
|
29
|
+
logger.debug("🚨 multipleThings.listPage.title is required", req);
|
|
30
|
+
return handleMiddlewareError(`🚨 multipleThings.listPage.title is required`, 500, next);
|
|
31
|
+
}
|
|
32
|
+
if (!mtConfig.itemTitleTemplate
|
|
33
|
+
|| !mtConfig.min === undefined || !mtConfig.min === null
|
|
34
|
+
|| !mtConfig.max) {
|
|
35
|
+
logger.debug("🚨 multipleThings.itemTitleTemplate, .min and .max are required", req);
|
|
36
|
+
return handleMiddlewareError(`🚨 multipleThings.itemTitleTemplate, .min and .max are required`, 500, next);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Grab items from formData
|
|
40
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
41
|
+
if (!Array.isArray(items)) items = [];
|
|
42
|
+
|
|
43
|
+
logger.debug(`MultipleThings hub for ${siteId}/${pageUrl}, items: ${items.length}`);
|
|
44
|
+
|
|
45
|
+
// Build hub template
|
|
46
|
+
const hubTemplate = {
|
|
47
|
+
sections: [
|
|
48
|
+
{
|
|
49
|
+
name: "main",
|
|
50
|
+
elements: [
|
|
51
|
+
// TODO: Add form element here
|
|
52
|
+
{
|
|
53
|
+
element: "form",
|
|
54
|
+
params: {
|
|
55
|
+
action: govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : "")),
|
|
56
|
+
method: "POST",
|
|
57
|
+
elements: []
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ➕ Add CSRF token
|
|
66
|
+
hubTemplate.sections[0].elements[0].params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
|
|
67
|
+
|
|
68
|
+
//--------- Handle Validation Errors ---------
|
|
69
|
+
// check if there are validation errors from the session (from POST handler)
|
|
70
|
+
const v = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl, "hub");
|
|
71
|
+
const hubErrors = v?.hub?.errors;
|
|
72
|
+
|
|
73
|
+
if (hubErrors && Object.keys(hubErrors).length > 0) {
|
|
74
|
+
// Build validation error summary
|
|
75
|
+
let validationErrors = buildMultipleThingsValidationSummary( serviceCopy, hubErrors, siteId, pageUrl, req, req.query?.route || "");
|
|
76
|
+
// Add error summary to the top of the form
|
|
77
|
+
hubTemplate.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors));
|
|
78
|
+
}
|
|
79
|
+
//--------- End of Handle Validation Errors ---------
|
|
80
|
+
|
|
81
|
+
// 1. Add topElements if provided
|
|
82
|
+
if (Array.isArray(mtConfig.listPage.topElements)) {
|
|
83
|
+
hubTemplate.sections[0].elements[0].params.elements.push(...mtConfig.listPage.topElements);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//If items are less than max show the add another button
|
|
87
|
+
if (items.length < mtConfig.max) {
|
|
88
|
+
// If addButtonPlacement is "top" or "both", add the "Add another" button at the top
|
|
89
|
+
if (mtConfig?.listPage?.addButtonPlacement === "top" || mtConfig?.listPage?.addButtonPlacement === "both") {
|
|
90
|
+
hubTemplate.sections[0].elements[0].params.elements.push(
|
|
91
|
+
govcyResources.getMultipleThingsLink("add", siteId, pageUrl, serviceCopy.site.lang, "", (req.query?.route === "review" ? "review" : "")
|
|
92
|
+
, mtConfig?.listPage?.addButtonText?.[serviceCopy.site.lang], addLinkCounter)
|
|
93
|
+
);
|
|
94
|
+
addLinkCounter++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 2. Add list of items or empty state
|
|
98
|
+
if (items.length === 0) {
|
|
99
|
+
hubTemplate.sections[0].elements[0].params.elements.push({
|
|
100
|
+
element: "inset",
|
|
101
|
+
// if no emptyState provided use the static resource
|
|
102
|
+
params: {
|
|
103
|
+
id: "multipleThingsList",
|
|
104
|
+
text: (
|
|
105
|
+
mtConfig?.listPage?.emptyState?.[serviceCopy.site.lang]
|
|
106
|
+
? mtConfig.listPage.emptyState
|
|
107
|
+
: govcyResources.staticResources.text.multipleThingsEnptyState
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
//nunjucks.renderString(template, item);
|
|
114
|
+
// Build dynamic table items using Nunjucks to render item titles
|
|
115
|
+
const tableItems = items.map((item, idx) => {
|
|
116
|
+
// Render the title string from the template (e.g. "{{title}} – {{institution}}")
|
|
117
|
+
// If item = { title: "BSc CS", institution: "UCY" }, it becomes "BSc CS – UCY"
|
|
118
|
+
const safeItem = (item && typeof item === "object") ? item : {};
|
|
119
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
120
|
+
let title;
|
|
121
|
+
try {
|
|
122
|
+
title = env.renderString(mtConfig.itemTitleTemplate, safeItem);
|
|
123
|
+
// Fallback if Nunjucks returned empty or only whitespace
|
|
124
|
+
if (!title || !title.trim()) {
|
|
125
|
+
title = govcyResources.staticResources.text.untitled[serviceCopy.site.lang] || govcyResources.staticResources.text.untitled["el"];
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// Log the error and fallback
|
|
129
|
+
logger.error(`Error rendering itemTitleTemplate for ${siteId}/${pageUrl}, index=${idx}: ${err.message}`, req);
|
|
130
|
+
title = govcyResources.staticResources.text.untitled[serviceCopy.site.lang] || govcyResources.staticResources.text.untitled["el"];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
// Table row text
|
|
136
|
+
text: { [serviceCopy.site.lang]: title },
|
|
137
|
+
|
|
138
|
+
// Row actions (edit / remove)
|
|
139
|
+
actions: [
|
|
140
|
+
{
|
|
141
|
+
text: govcyResources.staticResources.text.change,
|
|
142
|
+
// Edit route for this item
|
|
143
|
+
href: `/${siteId}/${pageUrl}/multiple/edit/${idx}${req.query?.route === "review" ? "?route=review" : ""}`,
|
|
144
|
+
visuallyHiddenText: { [serviceCopy.site.lang]: ` ${title}` }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
text: govcyResources.staticResources.text.delete,
|
|
148
|
+
// Delete route for this item
|
|
149
|
+
href: `/${siteId}/${pageUrl}/multiple/delete/${idx}${req.query?.route === "review" ? "?route=review" : ""}`,
|
|
150
|
+
visuallyHiddenText: { [serviceCopy.site.lang]: ` ${title}` }
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Push the table into the hub template
|
|
157
|
+
hubTemplate.sections[0].elements[0].params.elements.push({
|
|
158
|
+
element: "multipleThingsTable",
|
|
159
|
+
params: {
|
|
160
|
+
id: `multipleThingsList`,
|
|
161
|
+
classes: "govcy-multiple-table", // CSS hook
|
|
162
|
+
items: tableItems // The rows we just built
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//If items are less than max show the add another button
|
|
169
|
+
if (items.length < mtConfig.max) {
|
|
170
|
+
// If addButtonPlacement is "bottom" or "both", add the "Add another" button at the bottom
|
|
171
|
+
if (mtConfig?.listPage?.addButtonPlacement === "bottom" || mtConfig?.listPage?.addButtonPlacement === "both" || addLinkCounter === 0) {
|
|
172
|
+
hubTemplate.sections[0].elements[0].params.elements.push(
|
|
173
|
+
govcyResources.getMultipleThingsLink("add", siteId, pageUrl, serviceCopy.site.lang, "", (req.query?.route === "review" ? "review" : "")
|
|
174
|
+
, mtConfig?.listPage?.addButtonText?.[serviceCopy.site.lang], addLinkCounter)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// process message
|
|
179
|
+
// Deep copy page title (so we don’t mutate template)
|
|
180
|
+
let maxMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMaxMessage));
|
|
181
|
+
|
|
182
|
+
// Replace label placeholders on page title
|
|
183
|
+
for (const lang of Object.keys(maxMsg)) {
|
|
184
|
+
maxMsg[lang] = maxMsg[lang].replace("{{max}}", mtConfig.max);
|
|
185
|
+
}
|
|
186
|
+
hubTemplate.sections[0].elements[0].params.elements.push({
|
|
187
|
+
element: "warning",
|
|
188
|
+
// if no emptyState provided use the static resource
|
|
189
|
+
params: {
|
|
190
|
+
text: maxMsg
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 5. Add Continue button
|
|
196
|
+
hubTemplate.sections[0].elements[0].params.elements.push(
|
|
197
|
+
{
|
|
198
|
+
element: "button",
|
|
199
|
+
// if no emptyState provided use the static resource
|
|
200
|
+
params: {
|
|
201
|
+
text: (
|
|
202
|
+
mtConfig?.listPage?.continueButtonText?.[serviceCopy.site.lang]
|
|
203
|
+
? mtConfig.listPage.continueButtonText
|
|
204
|
+
: govcyResources.staticResources.text.continue
|
|
205
|
+
),
|
|
206
|
+
variant: "primary",
|
|
207
|
+
type: "submit"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
//if mtConfig.hasBackLink == true add section beforeMain with backlink element
|
|
214
|
+
if (mtConfig.listPage?.hasBackLink == true) {
|
|
215
|
+
hubTemplate.sections.unshift({
|
|
216
|
+
name: "beforeMain",
|
|
217
|
+
elements: [
|
|
218
|
+
{
|
|
219
|
+
element: "backLink",
|
|
220
|
+
params: {}
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// if (dataLayer.getUser(req.session)) {
|
|
226
|
+
// hubTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
|
|
227
|
+
// }
|
|
228
|
+
|
|
229
|
+
// Attach processedPage (like govcyPageHandler does)
|
|
230
|
+
req.processedPage = {
|
|
231
|
+
pageData: {
|
|
232
|
+
site: serviceCopy.site,
|
|
233
|
+
pageData: {
|
|
234
|
+
title: mtConfig.listPage.title,
|
|
235
|
+
layout: page?.pageData?.layout || "layouts/govcyBase.njk",
|
|
236
|
+
mainLayout: page?.pageData?.mainLayout || "two-third"
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
pageTemplate: hubTemplate
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
next();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.debug("Error in govcyMultipleThingsHubHandler middleware:", error.message);
|
|
245
|
+
return next(error); // Pass the error to the next middleware
|
|
246
|
+
}
|
|
247
|
+
}
|