@gov-cy/govcy-express-services 1.1.1 → 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 +16 -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 +62 -31
- package/src/middleware/govcyReviewPostHandler.mjs +47 -23
- package/src/middleware/govcyRoutePageHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +7 -6
- 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 +222 -88
- package/src/utils/govcyValidator.mjs +19 -7
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// utils/govcyMultipleThingsValidation.mjs
|
|
2
|
+
import { validateFormElements } from "./govcyValidator.mjs";
|
|
3
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
4
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate multipleThings items against the page's form definition.
|
|
8
|
+
* @param {object} page the page configuration object
|
|
9
|
+
* @param {array} items the array of items to validate
|
|
10
|
+
* @param {string} lang the language code for error messages
|
|
11
|
+
* @returns {object} errors object containing validation errors, if any
|
|
12
|
+
*/
|
|
13
|
+
export function validateMultipleThings(page, items, lang) {
|
|
14
|
+
const errors = {};
|
|
15
|
+
|
|
16
|
+
// 1. Min check
|
|
17
|
+
if (items.length < page.multipleThings.min) {
|
|
18
|
+
// Deep copy page title (so we don’t mutate template)
|
|
19
|
+
let minMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMinMessage));
|
|
20
|
+
|
|
21
|
+
// Replace label placeholders on page title
|
|
22
|
+
for (const lang of Object.keys(minMsg)) {
|
|
23
|
+
minMsg[lang] = minMsg[lang].replace("{{min}}", page.multipleThings.min);
|
|
24
|
+
}
|
|
25
|
+
errors._global = {
|
|
26
|
+
message: minMsg,
|
|
27
|
+
link: "#addNewItem0"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return errors; // early exit
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Max check (rare, but safe)
|
|
34
|
+
if (items.length > page.multipleThings.max) {
|
|
35
|
+
// Deep copy page title (so we don’t mutate template)
|
|
36
|
+
let maxMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMaxMessage));
|
|
37
|
+
|
|
38
|
+
// Replace label placeholders on page title
|
|
39
|
+
for (const lang of Object.keys(maxMsg)) {
|
|
40
|
+
maxMsg[lang] = maxMsg[lang].replace("{{max}}", page.multipleThings.max);
|
|
41
|
+
}
|
|
42
|
+
errors._global = {
|
|
43
|
+
message: maxMsg,
|
|
44
|
+
link: "#multipleThingsList"
|
|
45
|
+
};
|
|
46
|
+
return errors; // early exit
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Per-item validation
|
|
50
|
+
items.forEach((item, idx) => {
|
|
51
|
+
const formElement = page.pageTemplate.sections
|
|
52
|
+
.flatMap(s => s.elements)
|
|
53
|
+
.find(el => el.element === "form");
|
|
54
|
+
if (!formElement) return; // safety
|
|
55
|
+
|
|
56
|
+
const vErrors = validateFormElements(formElement.params.elements, item);
|
|
57
|
+
if (Object.keys(vErrors).length > 0) {
|
|
58
|
+
errors[idx] = vErrors;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Normalize validation errors into a summary list array.
|
|
68
|
+
* Works for both hub- and review-level errors.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} service - The full service configuration object
|
|
71
|
+
* @param {object} hubErrors - Validation errors object (from dataLayer)
|
|
72
|
+
* @param {string} siteId - Current site id
|
|
73
|
+
* @param {string} pageUrl - Page url (for hub links)
|
|
74
|
+
* @param {object} req - Express request (for lang + route info)
|
|
75
|
+
* @param {string} route - Current route (e.g. "review" or "")
|
|
76
|
+
* @param {boolean} isHub - Whether this is for the hub page (true) or review page (false)
|
|
77
|
+
*
|
|
78
|
+
* @returns {Array<{text: object, link: string}>}
|
|
79
|
+
*/
|
|
80
|
+
export function buildMultipleThingsValidationSummary(service, hubErrors, siteId, pageUrl, req, route = "", isHub = true) {
|
|
81
|
+
let validationErrors = [];
|
|
82
|
+
|
|
83
|
+
// For each error
|
|
84
|
+
for (const [key, err] of Object.entries(hubErrors)) {
|
|
85
|
+
let pageTitle = { en: "", el: "", tr: "" };
|
|
86
|
+
if (!isHub) {
|
|
87
|
+
// 🔍 Find the page by pageUrl
|
|
88
|
+
const page = getPageConfigData(service, pageUrl);
|
|
89
|
+
for (const lang of Object.keys(page.multipleThings.listPage.title)) {
|
|
90
|
+
pageTitle[lang] = (page.multipleThings.listPage.title[lang] || "") + " - ";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (key === "_global") {
|
|
95
|
+
let msg = err.message;
|
|
96
|
+
// Replace label placeholders on page title
|
|
97
|
+
for (const lang of Object.keys(msg)) {
|
|
98
|
+
msg[lang] = pageTitle[lang]
|
|
99
|
+
+ (msg[lang] || '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add to validationErrors array
|
|
103
|
+
validationErrors.push({
|
|
104
|
+
text: msg,
|
|
105
|
+
link: (isHub
|
|
106
|
+
? err.link || `#multipleThingsList`
|
|
107
|
+
: `/${siteId}/${pageUrl}` + (route === "review" ? "?route=review" : ""))
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
// Deep copy page title (so we don’t mutate template)
|
|
111
|
+
let msg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsItemsValidationPrefix));
|
|
112
|
+
const idx = parseInt(key, 10);
|
|
113
|
+
|
|
114
|
+
for (const fieldErr of Object.values(err)) {
|
|
115
|
+
// Replace label placeholders on page title
|
|
116
|
+
for (const lang of Object.keys(msg)) {
|
|
117
|
+
msg[lang] = pageTitle[lang]
|
|
118
|
+
+ msg[lang].replace("{{index}}", idx + 1)
|
|
119
|
+
+ (fieldErr.message?.[req.globalLang] || '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add to validationErrors array
|
|
123
|
+
validationErrors.push({
|
|
124
|
+
text: msg,
|
|
125
|
+
link: `/${siteId}/${pageUrl}/multiple/edit/${idx}${route === "review" ? "?route=review" : ""}`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return validationErrors;
|
|
132
|
+
}
|
|
@@ -4,7 +4,9 @@ import * as dataLayer from "./govcyDataLayer.mjs";
|
|
|
4
4
|
import { DSFEmailRenderer } from '@gov-cy/dsf-email-templates';
|
|
5
5
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
6
6
|
import { evaluatePageConditions } from "./govcyExpressions.mjs";
|
|
7
|
+
import { getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
7
8
|
import { logger } from "./govcyLogger.mjs";
|
|
9
|
+
import nunjucks from "nunjucks";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Prepares the submission data for the service, including raw data, print-friendly data, and renderer data.
|
|
@@ -48,61 +50,110 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
48
50
|
|
|
49
51
|
if (!formElement) continue; // ⛔ Skip pages without a <form> element
|
|
50
52
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
+
// 🔹 Case A: multipleThings page → array of items
|
|
54
|
+
if (page.multipleThings) {
|
|
55
|
+
// get the items array from session (or empty array if not found / not an array)
|
|
56
|
+
let items = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
57
|
+
if (!Array.isArray(items)) items = [];
|
|
58
|
+
|
|
59
|
+
submissionData[pageUrl] = [];
|
|
60
|
+
|
|
61
|
+
items.forEach((item, idx) => {
|
|
62
|
+
const itemData = {};
|
|
63
|
+
for (const el of formElement.params.elements || []) {
|
|
64
|
+
if (!ALLOWED_FORM_ELEMENTS.includes(el.element)) continue;
|
|
65
|
+
const elId = el.params?.id || el.params?.name;
|
|
66
|
+
if (!elId) continue;
|
|
67
|
+
|
|
68
|
+
// ✅ normalized with index
|
|
69
|
+
let value = getValue(el, pageUrl, req, siteId, idx);
|
|
70
|
+
itemData[elId] = value;
|
|
71
|
+
|
|
72
|
+
// handle fileInput special naming
|
|
73
|
+
if (el.element === "fileInput") {
|
|
74
|
+
itemData[elId + "Attachment"] = value;
|
|
75
|
+
delete itemData[elId];
|
|
76
|
+
}
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
// radios conditional elements
|
|
79
|
+
if (el.element === "radios") {
|
|
80
|
+
for (const radioItem of el.params.items || []) {
|
|
81
|
+
for (const condEl of radioItem.conditionalElements || []) {
|
|
82
|
+
if (!ALLOWED_FORM_ELEMENTS.includes(condEl.element)) continue;
|
|
83
|
+
const condId = condEl.params?.id || condEl.params?.name;
|
|
84
|
+
if (!condId) continue;
|
|
85
|
+
let condValue = getValue(condEl, pageUrl, req, siteId, idx);
|
|
86
|
+
itemData[condId] = condValue;
|
|
87
|
+
|
|
88
|
+
if (condEl.element === "fileInput") {
|
|
89
|
+
itemData[condId + "Attachment"] = condValue;
|
|
90
|
+
delete itemData[condId];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
submissionData[pageUrl].push(itemData);
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
// 🔹 Case B: normal page → single object
|
|
57
100
|
|
|
58
|
-
// ✅
|
|
59
|
-
if
|
|
101
|
+
// submissionData[pageUrl] = { formData: {} }; // ✅ Now initialize only if a form is present
|
|
102
|
+
submissionData[pageUrl] = {}; // ✅ Now initialize only if a form is present
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
// Traverse the form elements inside the form
|
|
105
|
+
for (const element of formElement.params.elements || []) {
|
|
106
|
+
const elType = element.element;
|
|
63
107
|
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
// ✅ Skip non-input elements like buttons
|
|
109
|
+
if (!ALLOWED_FORM_ELEMENTS.includes(elType)) continue;
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
submissionData[pageUrl][elId] = value;
|
|
111
|
+
const elId = element.params?.id || element.params?.name;
|
|
112
|
+
if (!elId) continue; // ⛔ Skip elements with no id/name
|
|
70
113
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
74
|
-
// submissionData[pageUrl].formData[elId + "Attachment"] = value;
|
|
75
|
-
submissionData[pageUrl][elId + "Attachment"] = value;
|
|
76
|
-
// delete submissionData[pageUrl].formData[elId];
|
|
77
|
-
delete submissionData[pageUrl][elId];
|
|
78
|
-
}
|
|
114
|
+
// 🟢 Use helper to get session value (or "" fallback if missing)
|
|
115
|
+
const value = getValue(element, pageUrl, req, siteId) ?? "";
|
|
79
116
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
// Store in submissionData
|
|
118
|
+
// submissionData[pageUrl].formData[elId] = value;
|
|
119
|
+
submissionData[pageUrl][elId] = value;
|
|
120
|
+
|
|
121
|
+
// handle fileInput
|
|
122
|
+
if (elType === "fileInput") {
|
|
123
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
124
|
+
// submissionData[pageUrl].formData[elId + "Attachment"] = value;
|
|
125
|
+
submissionData[pageUrl][elId + "Attachment"] = value;
|
|
126
|
+
// delete submissionData[pageUrl].formData[elId];
|
|
127
|
+
delete submissionData[pageUrl][elId];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 🔄 If radios with conditionalElements, walk ALL options
|
|
131
|
+
if (elType === "radios" && Array.isArray(element.params?.items)) {
|
|
132
|
+
for (const radioItem of element.params.items) {
|
|
133
|
+
const condEls = radioItem.conditionalElements;
|
|
134
|
+
if (!Array.isArray(condEls)) continue;
|
|
135
|
+
|
|
136
|
+
for (const condElement of condEls) {
|
|
137
|
+
const condType = condElement.element;
|
|
138
|
+
if (!ALLOWED_FORM_ELEMENTS.includes(condType)) continue;
|
|
139
|
+
|
|
140
|
+
const condId = condElement.params?.id || condElement.params?.name;
|
|
141
|
+
if (!condId) continue;
|
|
142
|
+
|
|
143
|
+
// Again: read from session or fallback to ""
|
|
144
|
+
const condValue = getValue(condElement, pageUrl, req, siteId) ?? "";
|
|
145
|
+
|
|
146
|
+
// Store even if the field was not visible to user
|
|
147
|
+
// submissionData[pageUrl].formData[condId] = condValue;
|
|
148
|
+
submissionData[pageUrl][condId] = condValue;
|
|
149
|
+
// handle fileInput
|
|
150
|
+
if (condType === "fileInput") {
|
|
151
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
152
|
+
// submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
|
|
153
|
+
submissionData[pageUrl][condId + "Attachment"] = condValue;
|
|
154
|
+
// delete submissionData[pageUrl].formData[condId];
|
|
155
|
+
delete submissionData[pageUrl][condId];
|
|
156
|
+
}
|
|
106
157
|
}
|
|
107
158
|
}
|
|
108
159
|
}
|
|
@@ -122,7 +173,7 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
122
173
|
submissionUsername: dataLayer.getUser(req.session).name,
|
|
123
174
|
submissionEmail: dataLayer.getUser(req.session).email,
|
|
124
175
|
submissionData: submissionData, // Raw data as submitted by the user in each page
|
|
125
|
-
submissionDataVersion: service.site?.submissionDataVersion || service.site?.submission_data_version ||"", // The submission data version
|
|
176
|
+
submissionDataVersion: service.site?.submissionDataVersion || service.site?.submission_data_version || "", // The submission data version
|
|
126
177
|
printFriendlyData: printFriendlyData, // Print-friendly data
|
|
127
178
|
rendererData: reviewSummaryList, // Renderer data of the summary list
|
|
128
179
|
rendererVersion: service.site?.rendererVersion || service.site?.renderer_version || "", // The renderer version
|
|
@@ -174,7 +225,9 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
174
225
|
// and extract the form data from the session
|
|
175
226
|
for (const page of service.pages) {
|
|
176
227
|
const fields = [];
|
|
228
|
+
const items = []; // ✅ new
|
|
177
229
|
// const currentPageUrl = page.pageData.url;
|
|
230
|
+
|
|
178
231
|
// ----- Conditional logic comes here
|
|
179
232
|
// Skip page if conditions indicate it should redirect (i.e. not be shown)
|
|
180
233
|
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
@@ -196,7 +249,11 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
196
249
|
|
|
197
250
|
//create the field object and push it to the fields array
|
|
198
251
|
// value of the field is handled by getValueLabel function
|
|
199
|
-
const field = createFieldObject(
|
|
252
|
+
const field = createFieldObject(
|
|
253
|
+
formElement,
|
|
254
|
+
rawValue,
|
|
255
|
+
getValueLabel(formElement, rawValue, page.pageData.url, req, siteId, service),
|
|
256
|
+
service);
|
|
200
257
|
fields.push(field);
|
|
201
258
|
|
|
202
259
|
// Handle conditional elements (only for radios for now)
|
|
@@ -223,11 +280,24 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
223
280
|
}
|
|
224
281
|
}
|
|
225
282
|
|
|
283
|
+
// Special case: multipleThings page → extract item titles // ✅ new
|
|
284
|
+
if (page.multipleThings) {
|
|
285
|
+
let mtItems = dataLayer.getPageData(req.session, siteId, page.pageData.url);
|
|
286
|
+
if (Array.isArray(mtItems)) {
|
|
287
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
288
|
+
for (const item of mtItems) {
|
|
289
|
+
const itemTitle = env.renderString(page.multipleThings.itemTitleTemplate, item);
|
|
290
|
+
items.push({ itemTitle, ...item });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
226
295
|
if (fields.length > 0) {
|
|
227
296
|
submissionData.push({
|
|
228
297
|
pageUrl: page.pageData.url,
|
|
229
298
|
pageTitle: page.pageData.title,
|
|
230
|
-
fields
|
|
299
|
+
fields,
|
|
300
|
+
items: (page.multipleThings ? items : null) // ✅ new
|
|
231
301
|
});
|
|
232
302
|
}
|
|
233
303
|
}
|
|
@@ -243,12 +313,13 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
243
313
|
* @param {string} name The name of the form element
|
|
244
314
|
* @param {object} req The request object
|
|
245
315
|
* @param {string} siteId The site ID
|
|
316
|
+
* @param {number} index The index of the date input
|
|
246
317
|
* @returns {string} The raw date input in ISO format (YYYY-MM-DD) or an empty string if not found
|
|
247
318
|
*/
|
|
248
|
-
function getDateInputISO(pageUrl, name, req, siteId) {
|
|
249
|
-
const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day
|
|
250
|
-
const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month
|
|
251
|
-
const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year
|
|
319
|
+
function getDateInputISO(pageUrl, name, req, siteId, index = null) {
|
|
320
|
+
const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`, index);
|
|
321
|
+
const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`, index);
|
|
322
|
+
const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`, index);
|
|
252
323
|
if (!day || !month || !year) return "";
|
|
253
324
|
|
|
254
325
|
// Pad day and month with leading zero if needed
|
|
@@ -265,12 +336,13 @@ function getDateInputISO(pageUrl, name, req, siteId) {
|
|
|
265
336
|
* @param {string} name The name of the form element
|
|
266
337
|
* @param {object} req The request object
|
|
267
338
|
* @param {string} siteId The site ID
|
|
339
|
+
* @param {number} index The index of the date input
|
|
268
340
|
* @returns {string} The raw date input in DMY format (DD/MM/YYYY) or an empty string if not found
|
|
269
341
|
*/
|
|
270
|
-
function getDateInputDMY(pageUrl, name, req, siteId) {
|
|
271
|
-
const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day
|
|
272
|
-
const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month
|
|
273
|
-
const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year
|
|
342
|
+
function getDateInputDMY(pageUrl, name, req, siteId, index = null) {
|
|
343
|
+
const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`, index);
|
|
344
|
+
const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`, index);
|
|
345
|
+
const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`, index);
|
|
274
346
|
if (!day || !month || !year) return "";
|
|
275
347
|
return `${day}/${month}/${year}`; // EU format: DD/MM/YYYY
|
|
276
348
|
}
|
|
@@ -303,19 +375,20 @@ function createFieldObject(formElement, value, valueLabel, service) {
|
|
|
303
375
|
* @param {string} pageUrl The page URL
|
|
304
376
|
* @param {object} req The request object
|
|
305
377
|
* @param {string} siteId The site ID
|
|
378
|
+
* @param {number} index The index of the form element (for multipleThings)
|
|
306
379
|
* @returns {string} The value of the form element from the session or an empty string if not found
|
|
307
380
|
*/
|
|
308
|
-
function getValue(formElement, pageUrl, req, siteId) {
|
|
381
|
+
function getValue(formElement, pageUrl, req, siteId, index = null) {
|
|
309
382
|
// handle raw value
|
|
310
383
|
let value = ""
|
|
311
384
|
if (formElement.element === "dateInput") {
|
|
312
|
-
value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
|
|
385
|
+
value = getDateInputISO(pageUrl, formElement.params.name, req, siteId, index);
|
|
313
386
|
} else if (formElement.element === "fileInput") {
|
|
314
387
|
// unneeded handle of `Attachment` at the end
|
|
315
388
|
// value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
|
|
316
|
-
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
389
|
+
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name, index);
|
|
317
390
|
} else {
|
|
318
|
-
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
391
|
+
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name, index);
|
|
319
392
|
}
|
|
320
393
|
|
|
321
394
|
// 🔁 Normalize checkboxes: always return an array
|
|
@@ -486,26 +559,49 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
486
559
|
|
|
487
560
|
|
|
488
561
|
|
|
562
|
+
const env = new nunjucks.Environment(null, { autoescape: true });
|
|
563
|
+
// One template renders multiple things
|
|
564
|
+
const multipleThingsTemplate = `
|
|
565
|
+
<div><strong>${govcyResources.staticResources.text.multipleThingsEntries[req.globalLang] || govcyResources.staticResources.text.multipleThingsEntries["el"]}</strong></div>
|
|
566
|
+
<ol class="govcy-mt-2">
|
|
567
|
+
{% for it in items -%}
|
|
568
|
+
<li>{{ it.itemTitle | trim }}</li>
|
|
569
|
+
{%- endfor %}
|
|
570
|
+
</ol>
|
|
571
|
+
`;
|
|
489
572
|
|
|
490
573
|
// Loop through each page in the submission data
|
|
491
574
|
for (const page of submissionData) {
|
|
492
575
|
// Get the page URL, title, and fields
|
|
493
|
-
const { pageUrl, pageTitle, fields } = page;
|
|
576
|
+
const { pageUrl, pageTitle, fields, items } = page;
|
|
494
577
|
|
|
495
578
|
|
|
496
579
|
let summaryListInner = { element: "summaryList", params: { items: [] } };
|
|
497
580
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// check if field.value is an object with `sha256` and `fileId` properties
|
|
504
|
-
if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
|
|
505
|
-
summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
|
|
581
|
+
// Special handling: multipleThings page → show <ol> of itemTitle only
|
|
582
|
+
if (Array.isArray(items)) {
|
|
583
|
+
summaryListInner = { element: "htmlElement", params: { text: {} } };
|
|
584
|
+
if (items.length == 0) {
|
|
585
|
+
summaryListInner.params.text = govcyResources.staticResources.text.multipleThingsEmptyStateReview
|
|
506
586
|
} else {
|
|
507
|
-
//
|
|
508
|
-
|
|
587
|
+
// Build ordered list HTML
|
|
588
|
+
let htmlByLang = env.renderString(multipleThingsTemplate, { items });
|
|
589
|
+
// Set the HTML for each language
|
|
590
|
+
summaryListInner.params.text = govcyResources.getMultilingualObject(htmlByLang, htmlByLang, htmlByLang);
|
|
591
|
+
}
|
|
592
|
+
} else { // Normal page → keep old behavior
|
|
593
|
+
// loop through each field and add it to the summary entry
|
|
594
|
+
for (const field of fields) {
|
|
595
|
+
const label = field.label;
|
|
596
|
+
const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
|
|
597
|
+
// --- HACK --- to see if this is a file element
|
|
598
|
+
// check if field.value is an object with `sha256` and `fileId` properties
|
|
599
|
+
if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
|
|
600
|
+
summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
|
|
601
|
+
} else {
|
|
602
|
+
// add the field to the summary entry
|
|
603
|
+
summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
|
|
604
|
+
}
|
|
509
605
|
}
|
|
510
606
|
}
|
|
511
607
|
|
|
@@ -557,7 +653,8 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
557
653
|
{
|
|
558
654
|
component: "bodySuccess",
|
|
559
655
|
params: {
|
|
560
|
-
title:
|
|
656
|
+
title: service?.site?.successEmailHeader?.[req.globalLang]
|
|
657
|
+
|| govcyResources.getLocalizeContent(govcyResources.staticResources.text.submissionSuccessTitle, req.globalLang),
|
|
561
658
|
body: `${govcyResources.getLocalizeContent(govcyResources.staticResources.text.yourSubmissionId, req.globalLang)} ${submissionId}`
|
|
562
659
|
}
|
|
563
660
|
}
|
|
@@ -572,10 +669,12 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
572
669
|
}
|
|
573
670
|
);
|
|
574
671
|
|
|
672
|
+
const env = new nunjucks.Environment(null, { autoescape: true })
|
|
673
|
+
|
|
575
674
|
// For each page in the submission data
|
|
576
675
|
for (const page of submissionData) {
|
|
577
676
|
// Get the page URL, title, and fields
|
|
578
|
-
const { pageUrl, pageTitle, fields } = page;
|
|
677
|
+
const { pageUrl, pageTitle, fields, items } = page;
|
|
579
678
|
|
|
580
679
|
// Add data title to the body
|
|
581
680
|
body.push(
|
|
@@ -586,19 +685,54 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
586
685
|
}
|
|
587
686
|
);
|
|
588
687
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
|
|
688
|
+
if (Array.isArray(items)) {
|
|
689
|
+
// 🔹 MultipleThings page → loop through items
|
|
690
|
+
// multipleThings → use itemTitleTemplate
|
|
691
|
+
const pageConfig = getPageConfigData(service, pageUrl);
|
|
692
|
+
const template = pageConfig.multipleThings?.itemTitleTemplate || "{{itemTitle}}";
|
|
693
|
+
|
|
694
|
+
if (items.length === 0) {
|
|
695
|
+
// Empty state message
|
|
696
|
+
body.push({
|
|
697
|
+
component: "bodyParagraph",
|
|
698
|
+
body: govcyResources.getLocalizeContent(
|
|
699
|
+
govcyResources.staticResources.text.multipleThingsEmptyStateReview,
|
|
700
|
+
req.globalLang
|
|
701
|
+
)
|
|
702
|
+
});
|
|
703
|
+
} else {
|
|
704
|
+
// Build ordered list of item titles
|
|
705
|
+
const listItems = items.map(item => {
|
|
706
|
+
const safeTitle = env.renderString(template, item);
|
|
707
|
+
return safeTitle; // already escaped
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Add ordered list to the body
|
|
711
|
+
body.push({
|
|
712
|
+
component: "bodyList",
|
|
713
|
+
params: {
|
|
714
|
+
type: "ol",
|
|
715
|
+
items: listItems
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
// 🔹 Normal page → continue below
|
|
721
|
+
let dataUl = [];
|
|
722
|
+
// loop through each field and add it to the summary entry
|
|
723
|
+
for (const field of fields) {
|
|
724
|
+
const label = govcyResources.getLocalizeContent(field.label, req.globalLang);
|
|
725
|
+
const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
|
|
726
|
+
dataUl.push({ key: label, value: valueLabel });
|
|
727
|
+
}
|
|
728
|
+
// add data to the body
|
|
729
|
+
body.push(
|
|
730
|
+
{
|
|
731
|
+
component: "bodyKeyValue",
|
|
732
|
+
params: { type: "ul", items: dataUl },
|
|
733
|
+
});
|
|
595
734
|
}
|
|
596
|
-
|
|
597
|
-
body.push(
|
|
598
|
-
{
|
|
599
|
-
component: "bodyKeyValue",
|
|
600
|
-
params: { type: "ul", items: dataUl },
|
|
601
|
-
});
|
|
735
|
+
|
|
602
736
|
|
|
603
737
|
}
|
|
604
738
|
|
|
@@ -42,7 +42,7 @@ function validateValue(value, rules) {
|
|
|
42
42
|
mobileCY: (val) => {
|
|
43
43
|
const normalized = val.replace(/[\s\-()]/g, ''); // Remove spaces, hyphens, and parentheses
|
|
44
44
|
return /^(?:\+357|00357)?9\d{7}$/.test(normalized); // Match Cypriot mobile numbers
|
|
45
|
-
|
|
45
|
+
},
|
|
46
46
|
iban: (val) => {
|
|
47
47
|
const cleanedIBAN = val.replace(/[\s-]/g, '').toUpperCase(); // Remove spaces/hyphens and convert to uppercase
|
|
48
48
|
const regex = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/;
|
|
@@ -96,6 +96,13 @@ function validateValue(value, rules) {
|
|
|
96
96
|
}
|
|
97
97
|
return normalizedVal <= max;
|
|
98
98
|
},
|
|
99
|
+
// ✅ New rule: maxCurrentYear
|
|
100
|
+
maxCurrentYear: (val) => {
|
|
101
|
+
const normalizedVal = normalizeNumber(val);
|
|
102
|
+
if (isNaN(normalizedVal)) return false;
|
|
103
|
+
const currentYear = new Date().getFullYear();
|
|
104
|
+
return normalizedVal <= currentYear;
|
|
105
|
+
},
|
|
99
106
|
minValueDate: (val, minDate) => {
|
|
100
107
|
const valueDate = parseDate(val); // Parse the input date
|
|
101
108
|
const min = parseDate(minDate); // Parse the minimum date
|
|
@@ -129,6 +136,10 @@ function validateValue(value, rules) {
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
// Skip validation if the value is empty
|
|
140
|
+
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === "")) {
|
|
141
|
+
continue; // let "required" handle emptiness
|
|
142
|
+
}
|
|
132
143
|
// Check for "valid" rules (e.g., numeric, telCY, etc.)
|
|
133
144
|
if (check === "valid" && validationRules[checkValue]) {
|
|
134
145
|
const isValid = validationRules[checkValue](value);
|
|
@@ -157,12 +168,12 @@ function validateValue(value, rules) {
|
|
|
157
168
|
if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
|
|
158
169
|
return message;
|
|
159
170
|
}
|
|
160
|
-
|
|
171
|
+
|
|
161
172
|
// Check for "maxValue"
|
|
162
173
|
if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
|
|
163
174
|
return message;
|
|
164
175
|
}
|
|
165
|
-
|
|
176
|
+
|
|
166
177
|
// Check for "minValueDate"
|
|
167
178
|
if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
|
|
168
179
|
return message;
|
|
@@ -178,6 +189,7 @@ function validateValue(value, rules) {
|
|
|
178
189
|
return message;
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
return null;
|
|
@@ -314,10 +326,10 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
314
326
|
.filter(Boolean) // Remove empty values
|
|
315
327
|
.join("-") // Join remaining parts
|
|
316
328
|
: (conditionalElement.element === "fileInput") // Handle fileInput
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
// unneeded handle of `Attachment` at the end
|
|
330
|
+
// ? formData[`${conditionalElement.params.name}Attachment`] || ""
|
|
331
|
+
? formData[`${conditionalElement.params.name}`] || ""
|
|
332
|
+
: formData[conditionalElement.params.name] || ""; // Get submitted value
|
|
321
333
|
|
|
322
334
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
|
|
323
335
|
if (["checkboxes", "radios", "select"].includes(conditionalElement.element) && conditionalFieldValue !== "") {
|