@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
246 lines (223 loc) β’ 11.7 kB
JavaScript
// src/middleware/govcyMultipleThingsHubHandler.mjs
import e from "express";
import * as govcyResources from "../resources/govcyResources.mjs";
import * as dataLayer from "../utils/govcyDataLayer.mjs";
import { logger } from "../utils/govcyLogger.mjs";
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
import { buildMultipleThingsValidationSummary } from "../utils/govcyMultipleThingsValidation.mjs";
import nunjucks from "nunjucks";
/**
* Middleware to render a MultipleThings hub page.
* @param {Object} page - The page config from the JSON (includes multipleThings block)
* @param {Object} serviceCopy - The service config (full JSON)
*/
export function govcyMultipleThingsHubHandler(req, res, next, page, serviceCopy) {
try {
const { siteId, pageUrl } = req.params;
const mtConfig = page.multipleThings;
let addLinkCounter = 0;
// Sanity checks
if (!mtConfig) {
logger.debug("π¨ multipleThings config not found in page config", req);
return handleMiddlewareError(`π¨ multipleThings config not found in page config`, 500, next);
}
if (!mtConfig.listPage
|| !mtConfig.listPage.title) {
logger.debug("π¨ multipleThings.listPage.title is required", req);
return handleMiddlewareError(`π¨ multipleThings.listPage.title is required`, 500, next);
}
if (!mtConfig.itemTitleTemplate
|| !mtConfig.min === undefined || !mtConfig.min === null
|| !mtConfig.max) {
logger.debug("π¨ multipleThings.itemTitleTemplate, .min and .max are required", req);
return handleMiddlewareError(`π¨ multipleThings.itemTitleTemplate, .min and .max are required`, 500, next);
}
// Grab items from formData
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
if (!Array.isArray(items)) items = [];
logger.debug(`MultipleThings hub for ${siteId}/${pageUrl}, items: ${items.length}`);
// Build hub template
const hubTemplate = {
sections: [
{
name: "main",
elements: [
{
element: "form",
params: {
action: govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : "")),
method: "POST",
elements: []
}
}
]
}
]
};
// β Add CSRF token
hubTemplate.sections[0].elements[0].params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
//--------- Handle Validation Errors ---------
// check if there are validation errors from the session (from POST handler)
const v = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl, "hub");
const hubErrors = v?.hub?.errors;
if (hubErrors && Object.keys(hubErrors).length > 0) {
// Build validation error summary
let validationErrors = buildMultipleThingsValidationSummary( serviceCopy, hubErrors, siteId, pageUrl, req, req.query?.route || "");
// Add error summary to the top of the form
hubTemplate.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors));
}
//--------- End of Handle Validation Errors ---------
// 1. Add topElements if provided
if (Array.isArray(mtConfig.listPage.topElements)) {
hubTemplate.sections[0].elements[0].params.elements.push(...mtConfig.listPage.topElements);
}
//If items are less than max show the add another button
if (items.length < mtConfig.max) {
// If addButtonPlacement is "top" or "both", add the "Add another" button at the top
if (mtConfig?.listPage?.addButtonPlacement === "top" || mtConfig?.listPage?.addButtonPlacement === "both") {
hubTemplate.sections[0].elements[0].params.elements.push(
govcyResources.getMultipleThingsLink("add", siteId, pageUrl, serviceCopy.site.lang, "", (req.query?.route === "review" ? "review" : "")
, mtConfig?.listPage?.addButtonText?.[serviceCopy.site.lang], addLinkCounter)
);
addLinkCounter++;
}
}
// 2. Add list of items or empty state
if (items.length === 0) {
hubTemplate.sections[0].elements[0].params.elements.push({
element: "inset",
// if no emptyState provided use the static resource
params: {
id: "multipleThingsList",
text: (
mtConfig?.listPage?.emptyState?.[serviceCopy.site.lang]
? mtConfig.listPage.emptyState
: govcyResources.staticResources.text.multipleThingsEnptyState
)
}
});
}
else {
//nunjucks.renderString(template, item);
// Build dynamic table items using Nunjucks to render item titles
const tableItems = items.map((item, idx) => {
// Render the title string from the template (e.g. "{{title}} β {{institution}}")
// If item = { title: "BSc CS", institution: "UCY" }, it becomes "BSc CS β UCY"
const safeItem = (item && typeof item === "object") ? item : {};
const env = new nunjucks.Environment(null, { autoescape: false });
let title;
try {
title = env.renderString(mtConfig.itemTitleTemplate, safeItem);
// Fallback if Nunjucks returned empty or only whitespace
if (!title || !title.trim()) {
title = govcyResources.staticResources.text.untitled[serviceCopy.site.lang] || govcyResources.staticResources.text.untitled["el"];
}
} catch (err) {
// Log the error and fallback
logger.error(`Error rendering itemTitleTemplate for ${siteId}/${pageUrl}, index=${idx}: ${err.message}`, req);
title = govcyResources.staticResources.text.untitled[serviceCopy.site.lang] || govcyResources.staticResources.text.untitled["el"];
}
return {
// Table row text
text: { [serviceCopy.site.lang]: title },
// Row actions (edit / remove)
actions: [
{
text: govcyResources.staticResources.text.change,
// Edit route for this item
href: `/${siteId}/${pageUrl}/multiple/edit/${idx}${req.query?.route === "review" ? "?route=review" : ""}`,
visuallyHiddenText: { [serviceCopy.site.lang]: ` ${title}` }
},
{
text: govcyResources.staticResources.text.delete,
// Delete route for this item
href: `/${siteId}/${pageUrl}/multiple/delete/${idx}${req.query?.route === "review" ? "?route=review" : ""}`,
visuallyHiddenText: { [serviceCopy.site.lang]: ` ${title}` }
}
]
};
});
// Push the table into the hub template
hubTemplate.sections[0].elements[0].params.elements.push({
element: "multipleThingsTable",
params: {
id: `multipleThingsList`,
classes: "govcy-multiple-table", // CSS hook
items: tableItems // The rows we just built
}
});
}
//If items are less than max show the add another button
if (items.length < mtConfig.max) {
// If addButtonPlacement is "bottom" or "both", add the "Add another" button at the bottom
if (mtConfig?.listPage?.addButtonPlacement === "bottom" || mtConfig?.listPage?.addButtonPlacement === "both" || addLinkCounter === 0) {
hubTemplate.sections[0].elements[0].params.elements.push(
govcyResources.getMultipleThingsLink("add", siteId, pageUrl, serviceCopy.site.lang, "", (req.query?.route === "review" ? "review" : "")
, mtConfig?.listPage?.addButtonText?.[serviceCopy.site.lang], addLinkCounter)
);
}
} else {
// process message
// Deep copy page title (so we donβt mutate template)
let maxMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMaxMessage));
// Replace label placeholders on page title
for (const lang of Object.keys(maxMsg)) {
maxMsg[lang] = maxMsg[lang].replace("{{max}}", mtConfig.max);
}
hubTemplate.sections[0].elements[0].params.elements.push({
element: "warning",
// if no emptyState provided use the static resource
params: {
text: maxMsg
}
});
}
// 5. Add Continue button
hubTemplate.sections[0].elements[0].params.elements.push(
{
element: "button",
// if no emptyState provided use the static resource
params: {
text: (
mtConfig?.listPage?.continueButtonText?.[serviceCopy.site.lang]
? mtConfig.listPage.continueButtonText
: govcyResources.staticResources.text.continue
),
variant: "primary",
type: "submit"
}
}
);
//if mtConfig.hasBackLink == true add section beforeMain with backlink element
if (mtConfig.listPage?.hasBackLink == true) {
hubTemplate.sections.unshift({
name: "beforeMain",
elements: [
{
element: "backLink",
params: {}
}
]
});
}
// if (dataLayer.getUser(req.session)) {
// hubTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
// }
// Attach processedPage (like govcyPageHandler does)
req.processedPage = {
pageData: {
site: serviceCopy.site,
pageData: {
title: mtConfig.listPage.title,
layout: page?.pageData?.layout || "layouts/govcyBase.njk",
mainLayout: page?.pageData?.mainLayout || "two-third"
}
},
pageTemplate: hubTemplate
};
next();
} catch (error) {
logger.debug("Error in govcyMultipleThingsHubHandler middleware:", error.message);
return next(error); // Pass the error to the next middleware
}
}