@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.
@@ -1,12 +1,13 @@
1
1
  import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
2
2
  import * as govcyResources from "../resources/govcyResources.mjs";
3
- import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
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
- for (const section of page.pageTemplate.sections) {
76
- const form = section.elements.find(el => el.element === "form");
77
- if (form) {
78
- //handle review route
79
- if (req.query.route === "review") {
80
- nextPage = govcyResources.constructPageUrl(siteId, "review");
81
- } else {
82
- nextPage = page.pageData.nextPage;
83
- //nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate;
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
+ }