@gov-cy/govcy-express-services 1.2.0 → 1.3.0-alpha.1

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.
@@ -20,24 +20,53 @@ import * as govcyResources from "../resources/govcyResources.mjs";
20
20
  * @param {string} lang The language
21
21
  * @param {Object} fileInputElements The file input elements
22
22
  * @param {string} routeParam The route parameter
23
+ * @param {string} mode The mode, either "single" (default), "add", or "edit"
24
+ * @param {number|null} index The index of the item being edited (null for single or add mode)
23
25
  */
24
- export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
26
+ export function populateFormData(
27
+ formElements,
28
+ theData,
29
+ validationErrors,
30
+ store = {},
31
+ siteId = "",
32
+ pageUrl = "",
33
+ lang = "el",
34
+ fileInputElements = null,
35
+ routeParam = "",
36
+ mode = "single",
37
+ index = null
38
+ ) {
25
39
  const inputElements = ALLOWED_FORM_ELEMENTS;
26
40
  const isRootCall = !fileInputElements;
41
+ let elementId = "";
42
+ let firstElementId = "";
43
+
27
44
  if (isRootCall) {
28
45
  fileInputElements = {};
29
46
  }
30
47
  // Recursively populate form data with session data
31
48
  formElements.forEach(element => {
32
49
  if (inputElements.includes(element.element)) {
50
+ // Get the element ID and field name
51
+ elementId = (element.element === "checkboxes" || element.element === "radios") // if checkboxes or radios
52
+ ? `${element.params.id}-option-1` // use the id of the first option
53
+ : (element.element === "dateInput") //if dateInput
54
+ ? `${element.params.id}_day` // use the id of the day input
55
+ : element.params.id; // else use the id of the element
56
+
33
57
  const fieldName = element.params.name;
34
58
 
59
+ // Store the ID of the first input element (for error summary link)
60
+ if (!firstElementId) {
61
+ firstElementId = elementId;
62
+ }
63
+
35
64
  // Handle `dateInput` separately
36
65
  if (element.element === "dateInput") {
37
66
  element.params.dayValue = theData[`${fieldName}_day`] || "";
38
67
  element.params.monthValue = theData[`${fieldName}_month`] || "";
39
68
  element.params.yearValue = theData[`${fieldName}_year`] || "";
40
- //Handle `datePicker` separately
69
+ //Handle `datePicker` separately
41
70
  } else if (element.element === "datePicker") {
42
71
  const val = theData[fieldName];
43
72
 
@@ -68,25 +97,49 @@ export function populateFormData(formElements, theData, validationErrors, store
68
97
  } else if (element.element === "fileInput") {
69
98
  // For fileInput, we change the element.element to "fileView" and set the
70
99
  // fileId and sha256 from the session store
71
- // unneeded handle of `Attachment` at the end
72
- // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
73
- const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
74
- // TODO: Ask Andreas how to handle empty file inputs
100
+ // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
101
+
102
+ // 1) Prefer file from theData (could be draft in add mode, or item object in edit)
103
+ let fileData = theData[fieldName];
104
+
105
+ // 2) If not found, fall back to dataLayer (normal page behaviour)
106
+ if (!fileData) {
107
+ if (mode === "edit" && index !== null) {
108
+ // In edit mode, try to get the file for the specific item index
109
+ fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName, index);
110
+ } else {
111
+ // In single or add mode, get the file normally
112
+ fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
113
+ }
114
+ }
115
+
75
116
  if (fileData) {
76
117
  element.element = "fileView";
77
118
  element.params.fileId = fileData.fileId;
78
119
  element.params.sha256 = fileData.sha256;
79
120
  element.params.visuallyHiddenText = element.params.label;
80
- // TODO: Also need to set the `view` and `download` URLs
81
- element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
121
+
122
+ // Build base path based on mode
123
+ let basePath = `/${siteId}/${pageUrl}`;
124
+ if (mode === "add") {
125
+ basePath += "/multiple/add";
126
+ } else if (mode === "edit" && index !== null) {
127
+ basePath += `/multiple/edit/${index}`;
128
+ }
129
+
130
+ // View link
131
+ element.params.viewHref = `${basePath}/view-file/${fieldName}`;
82
132
  element.params.viewTarget = "_blank";
83
- element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
133
+ // Delete link (preserve ?route=review if present)
134
+ element.params.deleteHref = `${basePath}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`
135
+
136
+
84
137
  } else {
85
138
  // TODO: Ask Andreas how to handle empty file inputs
86
139
  element.params.value = "";
87
- }
140
+ }
88
141
  fileInputElements[fieldName] = element;
89
- // Handle all other input elements (textInput, checkboxes, radios, etc.)
142
+ // Handle all other input elements (textInput, checkboxes, radios, etc.)
90
143
  } else {
91
144
  element.params.value = theData[fieldName] || "";
92
145
  }
@@ -95,11 +148,11 @@ export function populateFormData(formElements, theData, validationErrors, store
95
148
  if (validationErrors?.errors?.[fieldName]) {
96
149
  element.params.error = validationErrors.errors[fieldName].message;
97
150
  //populate the error summary
98
- const elementId = (element.element === "checkboxes" || element.element === "radios") // if checkboxes or radios
99
- ? `${element.params.id}-option-1` // use the id of the first option
100
- : (element.element === "dateInput") //if dateInput
101
- ? `${element.params.id}_day` // use the id of the day input
102
- : element.params.id; // else use the id of the element
151
+ // const elementId = (element.element === "checkboxes" || element.element === "radios") // if checkboxes or radios
152
+ // ? `${element.params.id}-option-1` // use the id of the first option
153
+ // : (element.element === "dateInput") //if dateInput
154
+ // ? `${element.params.id}_day` // use the id of the day input
155
+ // : element.params.id; // else use the id of the element
103
156
  validationErrors.errorSummary.push({
104
157
  link: `#${elementId}`,
105
158
  text: validationErrors.errors[fieldName].message
@@ -111,8 +164,8 @@ export function populateFormData(formElements, theData, validationErrors, store
111
164
  if (element.element === "radios" && element.params.items) {
112
165
  element.params.items.forEach(item => {
113
166
  if (item.conditionalElements) {
114
- populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
115
-
167
+ populateFormData(item.conditionalElements, theData, validationErrors, store, siteId, pageUrl, lang, fileInputElements, routeParam);
168
+
116
169
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
117
170
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
118
171
  item.conditionalHasErrors = true;
@@ -121,6 +174,25 @@ export function populateFormData(formElements, theData, validationErrors, store
121
174
  });
122
175
  }
123
176
  });
177
+
178
+ // 🔴 Handle _global validation errors (collection-level, not tied to a field)
179
+ if (isRootCall && validationErrors?.errors?._global) {
180
+ validationErrors.errorSummary = validationErrors.errorSummary || [];
181
+
182
+ // Decide where the link should point
183
+ let linkTarget = `#${firstElementId}`; // default anchor at top of the form
184
+ if (validationErrors.errors._global.pageUrl) {
185
+ // If pageUrl is provided (e.g. for max items), point back to hub
186
+ linkTarget = `${validationErrors.errors._global.pageUrl}`;
187
+ }
188
+
189
+ // Push into the error summary
190
+ validationErrors.errorSummary.push({
191
+ link: linkTarget,
192
+ text: validationErrors.errors._global.message
193
+ });
194
+ }
195
+
124
196
  // add file input elements's definition in js object
125
197
  if (isRootCall && Object.keys(fileInputElements).length > 0) {
126
198
  const scriptTag = `
@@ -155,9 +227,10 @@ export function populateFormData(formElements, theData, validationErrors, store
155
227
  * @param {Object} store - The session store .
156
228
  * @param {string} siteId - The site ID .
157
229
  * @param {string} pageUrl - The page URL .
230
+ * @param {number|null} index - The index of the item being edited for multiple items
158
231
  * @returns {Object} filteredData - The filtered form data.
159
232
  */
160
- export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
233
+ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "", index = null) {
161
234
  const filteredData = {};
162
235
  elements.forEach(element => {
163
236
  const { name } = element.params || {};
@@ -174,12 +247,12 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
174
247
  if (item.conditionalElements) {
175
248
  Object.assign(
176
249
  filteredData,
177
- getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
250
+ getFormData(item.conditionalElements, formData, store, siteId, pageUrl, index)
178
251
  );
179
252
  }
180
253
  });
181
254
  }
182
-
255
+
183
256
  }
184
257
  // Handle dateInput
185
258
  else if (element.element === "dateInput") {
@@ -189,13 +262,13 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
189
262
  filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
190
263
  filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
191
264
  filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
192
- // handle fileInput
265
+ // handle fileInput
193
266
  } else if (element.element === "fileInput") {
194
267
  // fileInput elements are already stored in the store when it was uploaded
195
268
  // so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
196
269
  // unneeded handle of `Attachment` at the end
197
270
  // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
198
- const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
271
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name, index);
199
272
  if (fileData) {
200
273
  // unneeded handle of `Attachment` at the end
201
274
  // filteredData[name + "Attachment"] = fileData;
@@ -206,7 +279,7 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
206
279
  // filteredData[name + "Attachment"] = ""; // or handle as needed
207
280
  filteredData[name] = ""; // or handle as needed
208
281
  }
209
- // Handle other elements (e.g., textInput, textArea, datePicker)
282
+ // Handle other elements (e.g., textInput, textArea, datePicker)
210
283
  } else {
211
284
  filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
212
285
  }
@@ -216,9 +289,9 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
216
289
  // Object.assign(filteredData, getFormData(element.conditionalElements, formData));
217
290
  // }
218
291
  }
219
-
292
+
220
293
  });
221
-
294
+
222
295
 
223
296
  return filteredData;
224
- }
297
+ }
@@ -18,9 +18,21 @@ import { logger } from './govcyLogger.mjs';
18
18
  * @param {string} opts.pageUrl - Page URL
19
19
  * @param {string} opts.elementName - Name of file input
20
20
  * @param {object} opts.file - File object from multer (req.file)
21
+ * @param {string} opts.mode - Upload mode ("single" | "multipleThingsDraft" | "multipleThingsEdit")
22
+ * @param {number|null} opts.index - Numeric index for edit mode (0-based), or null
21
23
  * @returns {Promise<{ status: number, data?: object, errorMessage?: string }>}
22
24
  */
23
- export async function handleFileUpload({ service, store, siteId, pageUrl, elementName, file }) {
25
+ export async function handleFileUpload({
26
+ service,
27
+ store,
28
+ siteId,
29
+ pageUrl,
30
+ elementName,
31
+ file,
32
+ mode = "single", // "single" | "multipleThingsDraft" | "multipleThingsEdit"
33
+ index = null // numeric index for edit mode
34
+ }) {
35
+
24
36
  try {
25
37
  // Validate essentials
26
38
  // Early exit if key things are missing
@@ -187,23 +199,56 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
187
199
 
188
200
  // ✅ Success
189
201
  // Store the file metadata in the session store
190
- // unneeded handle of `Attachment` at the end
191
- // dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
192
- dataLayer.storePageDataElement(store, siteId, pageUrl, elementName, {
202
+ const metadata = {
193
203
  sha256: response.Data.sha256,
194
204
  fileId: response.Data.fileId,
195
- });
196
- logger.debug("File upload successful", response.Data);
197
- logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
198
- return {
199
- status: 200,
200
- data: {
205
+ fileName: response.Data.fileName || file.originalname || "",
206
+ mimeType: response.Data.contentType || file.mimetype || "",
207
+ fileSize: response.Data.fileSize || file.size || 0,
208
+ };
209
+
210
+ if (mode === "multipleThingsDraft") {
211
+ // Store in draft object
212
+ let draft = dataLayer.getMultipleDraft(store, siteId, pageUrl);
213
+ if (!draft) draft = {};
214
+ draft[elementName] = {
201
215
  sha256: response.Data.sha256,
202
- filename: response.Data.fileName || '',
203
216
  fileId: response.Data.fileId,
204
- mimeType: response.Data.contentType || '',
205
- fileSize: response.Data?.fileSize || ''
217
+ };
218
+ dataLayer.setMultipleDraft(store, siteId, pageUrl, draft);
219
+ logger.debug(`Stored file metadata in draft for ${siteId}/${pageUrl}`, metadata);
220
+ }
221
+ else if (mode === "multipleThingsEdit") {
222
+ // Store in item array
223
+ let items = dataLayer.getPageData(store, siteId, pageUrl);
224
+ if (!Array.isArray(items)) items = [];
225
+ if (index !== null && index >= 0 && index < items.length) {
226
+ items[index][elementName] = {
227
+ sha256: response.Data.sha256,
228
+ fileId: response.Data.fileId,
229
+ };
230
+ dataLayer.storePageData(store, siteId, pageUrl, items);
231
+ logger.debug(`Stored file metadata in item index=${index} for ${siteId}/${pageUrl}`, metadata);
232
+ } else {
233
+ return {
234
+ status: 400,
235
+ dataStatus: 412,
236
+ errorMessage: `Invalid index for multipleThingsEdit (index=${index})`
237
+ };
206
238
  }
239
+ }
240
+ else {
241
+ // Default: single-page behaviour
242
+ dataLayer.storePageDataElement(store, siteId, pageUrl, elementName, {
243
+ sha256: response.Data.sha256,
244
+ fileId: response.Data.fileId,
245
+ });
246
+ logger.debug(`Stored file metadata in single mode for ${siteId}/${pageUrl}`, metadata);
247
+ }
248
+
249
+ return {
250
+ status: 200,
251
+ data: metadata
207
252
  };
208
253
 
209
254
  } catch (err) {
@@ -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
+ }