@gov-cy/govcy-express-services 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
5
5
  "author": "DMRID - DSF Team",
6
6
  "license": "MIT",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@gov-cy/dsf-email-templates": "^2.1.15",
60
- "@gov-cy/govcy-frontend-renderer": "^1.26.10",
60
+ "@gov-cy/govcy-frontend-renderer": "^1.27.0",
61
61
  "axios": "^1.9.0",
62
62
  "cookie-parser": "^1.4.7",
63
63
  "dotenv": "^16.3.1",
@@ -1,6 +1,7 @@
1
1
  import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
2
2
  import * as govcyResources from "../resources/govcyResources.mjs";
3
3
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
4
+ import { markCurrentNavigation } from "../utils/govcyUtils.mjs";
4
5
 
5
6
  /**
6
7
  * Middleware function to render pages using the GovCy Frontend Renderer.
@@ -26,6 +27,7 @@ export function renderGovcyPage() {
26
27
  }
27
28
  // Add custom CSS path
28
29
  processedPage.pageData.site.customCSSFile = `/css/govcyExpress.css`;
30
+ markCurrentNavigation(req, processedPage.pageData);
29
31
  const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
30
32
  res.send(html);
31
33
  };
@@ -164,7 +164,7 @@ export function populateFormData(
164
164
  if (element.element === "radios" && element.params.items) {
165
165
  element.params.items.forEach(item => {
166
166
  if (item.conditionalElements) {
167
- populateFormData(item.conditionalElements, theData, validationErrors, store, siteId, pageUrl, lang, fileInputElements, routeParam);
167
+ populateFormData(item.conditionalElements, theData, validationErrors, store, siteId, pageUrl, lang, fileInputElements, routeParam, mode, index);
168
168
 
169
169
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
170
170
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
@@ -18,8 +18,89 @@ export function handleMiddlewareError(message, status, next) {
18
18
  * @param {string} dateString - The date string in the format YYYY-MM-DD.
19
19
  * @returns {string} The formatted date in the format D/M/YYYY.
20
20
  */
21
- export function dateStringISOtoDMY(dateString) {
22
- if (typeof dateString !== "string" || !dateString.trim()) return "";
23
- const [year, month, day] = dateString.trim().split("-");
24
- return `${parseInt(day)}/${parseInt(month)}/${year}`;
25
- }
21
+ export function dateStringISOtoDMY(dateString) {
22
+ if (typeof dateString !== "string" || !dateString.trim()) return "";
23
+ const [year, month, day] = dateString.trim().split("-");
24
+ return `${parseInt(day)}/${parseInt(month)}/${year}`;
25
+ }
26
+
27
+ /**
28
+ * Marks the active navigation item in renderer page data based on the current request path.
29
+ *
30
+ * @param {object} req - Express request object.
31
+ * @param {object} pageData - Renderer page data object containing `site.navigation.items`.
32
+ * @returns {void}
33
+ */
34
+ export function markCurrentNavigation(req, pageData) {
35
+ const navItems = pageData?.site?.navigation?.items;
36
+ if (!Array.isArray(navItems) || navItems.length === 0) return;
37
+
38
+ // Use req.path as base and keep case-sensitive behavior (do not lowercase).
39
+ const reqPath = typeof req?.path === "string" ? req.path : "";
40
+ if (!reqPath) return;
41
+
42
+ // Normalize trailing slash so /site/page and /site/page/ are treated as the same path (root "/" remains "/").
43
+ const normalizePath = (pathValue) => {
44
+ if (typeof pathValue !== "string" || pathValue === "") return "";
45
+ return pathValue.length > 1 && pathValue.endsWith("/") ? pathValue.slice(0, -1) : pathValue;
46
+ };
47
+
48
+ // Reset all navigation items current flag to false before we mark the active one.
49
+ navItems.forEach((item) => {
50
+ if (item && typeof item === "object") {
51
+ item.current = false;
52
+ }
53
+ });
54
+
55
+ let currentPath = normalizePath(reqPath);
56
+
57
+ // First try exact matching against navigation item href/link values.
58
+ const findExactMatchIndex = (pathValue) =>
59
+ navItems.findIndex((item) => {
60
+ // Resolve navigation path from `href` first (renderer schema), then fallback to `link` for compatibility.
61
+ const rawNavPath = item?.href ?? item?.link;
62
+
63
+ // Support multilingual href objects by picking request/global language first, then site language, then common fallbacks.
64
+ const navPath = (rawNavPath && typeof rawNavPath === "object")
65
+ ? (rawNavPath?.[req?.globalLang] ??
66
+ rawNavPath?.[pageData?.site?.lang] ??
67
+ rawNavPath?.el ??
68
+ rawNavPath?.en ??
69
+ rawNavPath?.tr ??
70
+ Object.values(rawNavPath)[0])
71
+ : rawNavPath;
72
+
73
+ return normalizePath(navPath) === pathValue;
74
+ });
75
+
76
+ // Build match candidates so "/:siteId" and "/:siteId/index" are treated as aliases for home.
77
+ const matchCandidates = [currentPath];
78
+ const siteOnlyMatch = currentPath.match(/^\/([^/]+)$/);
79
+ const siteIndexMatch = currentPath.match(/^\/([^/]+)\/index$/);
80
+ if (siteOnlyMatch) {
81
+ matchCandidates.push(`/${siteOnlyMatch[1]}/index`);
82
+ } else if (siteIndexMatch) {
83
+ matchCandidates.push(`/${siteIndexMatch[1]}`);
84
+ }
85
+
86
+ let matchIndex = -1;
87
+ for (const candidate of matchCandidates) {
88
+ matchIndex = findExactMatchIndex(candidate);
89
+ if (matchIndex !== -1) break;
90
+ }
91
+
92
+ // If exact match is not found, map known nested routes to the parent page path and try exact match again.
93
+ if (matchIndex === -1) {
94
+ const nestedMatch = currentPath.match(/^\/([^/]+)\/([^/]+)\/(multiple|delete-file|view-file|update-my-details-response)(?:\/.*)?$/);
95
+ if (nestedMatch) {
96
+ const [, siteId, pageUrl] = nestedMatch;
97
+ currentPath = `/${siteId}/${pageUrl}`;
98
+ matchIndex = findExactMatchIndex(currentPath);
99
+ }
100
+ }
101
+
102
+ // Mark only one navigation item as current (the first deterministic match).
103
+ if (matchIndex >= 0) {
104
+ navItems[matchIndex].current = true;
105
+ }
106
+ }