@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.
@@ -1,13 +1,14 @@
1
1
  import * as govcyResources from "../resources/govcyResources.mjs";
2
- import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
2
+ import { validateFormElements } from "../utils/govcyValidator.mjs"; // Import your validator
3
3
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
4
4
  import { logger } from "../utils/govcyLogger.mjs";
5
- import {prepareSubmissionData, prepareSubmissionDataAPI, generateSubmitEmail } from "../utils/govcySubmitData.mjs";
5
+ import { prepareSubmissionData, prepareSubmissionDataAPI, generateSubmitEmail } from "../utils/govcySubmitData.mjs";
6
6
  import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
7
7
  import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
8
8
  import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
9
9
  import { sendEmail } from "../utils/govcyNotification.mjs"
10
10
  import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
11
+ import { validateMultipleThings } from "../utils/govcyMultipleThingsValidation.mjs";
11
12
 
12
13
  /**
13
14
  * Middleware to handle review page form submission
@@ -17,7 +18,7 @@ export function govcyReviewPostHandler() {
17
18
  return async (req, res, next) => {
18
19
  try {
19
20
  const { siteId } = req.params;
20
-
21
+
21
22
  // ✅ Load service and check if it exists
22
23
  const service = req.serviceData;
23
24
  let validationErrors = {};
@@ -26,7 +27,7 @@ export function govcyReviewPostHandler() {
26
27
  for (const page of service.pages) {
27
28
  //get page url
28
29
  const pageUrl = page.pageData.url;
29
-
30
+
30
31
  // ----- Conditional logic comes here
31
32
  // ✅ Skip validation if page is conditionally excluded
32
33
  const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
@@ -46,9 +47,32 @@ export function govcyReviewPostHandler() {
46
47
 
47
48
  // Get stored form data for this page (or default to empty)
48
49
  const formData = dataLayer.getPageData(req.session, siteId, pageUrl) || {};
49
-
50
- // Run validations
51
- const errors = validateFormElements(formElement.params.elements, formData, pageUrl);
50
+
51
+ let errors = {};
52
+ // ----- MultipleThings hub handling -----
53
+ if (page.multipleThings) {
54
+ // Use multiple things validator
55
+ const items = Array.isArray(formData) ? formData : [];
56
+ const mtErrors = validateMultipleThings(page, items, service.site.lang);
57
+
58
+ if (Object.keys(mtErrors).length > 0) {
59
+ errors[pageUrl] = {
60
+ type: "multipleThings", // ✅ mark it
61
+ hub: { errors: mtErrors } // keep hub-style structure
62
+ };
63
+ }
64
+ } else { // ----- Normal form handling -----
65
+ // Normal page validation
66
+ const v = validateFormElements(formElement.params.elements, formData, pageUrl);
67
+ if (Object.keys(v).length > 0) {
68
+ errors[pageUrl] = {
69
+ type: "normal", // ✅ mark it
70
+ ...v
71
+ };
72
+ }
73
+ // // Run validations
74
+ // errors = validateFormElements(formElement.params.elements, formData, pageUrl);
75
+ }
52
76
 
53
77
  // Add errors to the validationErrors object
54
78
  validationErrors = { ...validationErrors, ...errors };
@@ -68,7 +92,7 @@ export function govcyReviewPostHandler() {
68
92
  const clientKey = getEnvVariable(service?.site?.submissionAPIEndpoint?.clientKey || "", false);
69
93
  const serviceId = getEnvVariable(service?.site?.submissionAPIEndpoint?.serviceId || "", false);
70
94
  const dsfGtwApiKey = getEnvVariable(service?.site?.submissionAPIEndpoint?.dsfgtwApiKey || "", "");
71
- const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES",false) ; // Default to false if not set
95
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false); // Default to false if not set
72
96
  if (!submissionUrl) {
73
97
  return handleMiddlewareError("🚨 Submission API endpoint URL is missing", 500, next);
74
98
  }
@@ -78,13 +102,13 @@ export function govcyReviewPostHandler() {
78
102
  if (!serviceId) {
79
103
  return handleMiddlewareError("🚨 Submission API serviceId is missing", 500, next);
80
104
  }
81
-
105
+
82
106
  // Prepare submission data
83
107
  const submissionData = prepareSubmissionData(req, siteId, service);
84
108
 
85
109
  // Prepare submission data for API
86
110
  const submissionDataAPI = prepareSubmissionDataAPI(submissionData);
87
-
111
+
88
112
  logger.debug("Prepared submission data for API:", submissionDataAPI);
89
113
 
90
114
  // Call the API to submit the data
@@ -94,7 +118,7 @@ export function govcyReviewPostHandler() {
94
118
  submissionDataAPI, // Pass the prepared submission data
95
119
  true, // Use access token authentication
96
120
  dataLayer.getUser(req.session), // Get the user from the session
97
- {
121
+ {
98
122
  accept: "text/plain", // Set Accept header to text/plain
99
123
  "client-key": clientKey, // Set the client key header
100
124
  "service-id": serviceId, // Set the service ID header
@@ -103,44 +127,44 @@ export function govcyReviewPostHandler() {
103
127
  3,
104
128
  allowSelfSignedCerts
105
129
  );
106
-
130
+
107
131
  // Check if the response is successful
108
132
  if (response.Succeeded) {
109
133
  let referenceNo = response?.Data?.referenceValue || "";
110
134
  // Add the reference number to the submission data
111
- submissionData.referenceNumber = referenceNo;
135
+ submissionData.referenceNumber = referenceNo;
112
136
  logger.info("✅ Data submitted", siteId, referenceNo);
113
137
  // handle data layer submission
114
138
  dataLayer.storeSiteSubmissionData(
115
139
  req.session,
116
- siteId,
140
+ siteId,
117
141
  submissionData);
118
-
142
+
119
143
  //-- Send email to user
120
144
  // Generate the email body
121
145
  let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
122
146
  logger.debug("Email generated:", emailBody);
123
147
  // Send the email
124
- sendEmail(service.site.title[service.site.lang],emailBody,[dataLayer.getUser(req.session).email], "eMail").catch(err => {
148
+ sendEmail(service.site.title[service.site.lang], emailBody, [dataLayer.getUser(req.session).email], "eMail").catch(err => {
125
149
  logger.error("Email sending failed (async):", err);
126
150
  });
127
151
  // --- End of email sending
128
-
129
- logger.debug("🔄 Redirecting to success page:", req);
152
+
153
+ logger.debug("🔄 Redirecting to success page:", req);
130
154
  // redirect to success
131
155
  return res.redirect(govcyResources.constructPageUrl(siteId, `success`));
132
-
156
+
133
157
  // logger.debug("The submission data prepared:", printFriendlyData);
134
158
  // let reviewSummary = generateReviewSummary(printFriendlyData,req, siteId, false);
135
159
  // res.send(emailBody);
136
-
160
+
137
161
  // // Clear any existing submission errors from the session
138
162
  // dataLayer.clearSiteSubmissionErrors(req.session, siteId);
139
163
  } else {
140
164
  // Handle submission failure
141
165
  const errorCode = response.ErrorCode;
142
166
  const errorPage = service.site?.submissionAPIEndpoint?.response?.errorResponse?.[errorCode]?.page;
143
-
167
+
144
168
  if (errorPage) {
145
169
  logger.info("🚨 Submission returned failed:", response.ErrorCode);
146
170
  return res.redirect(errorPage);
@@ -148,7 +172,7 @@ export function govcyReviewPostHandler() {
148
172
  return handleMiddlewareError("🚨 Unknown error code received from API.", 500, next);
149
173
  }
150
174
  }
151
-
175
+
152
176
  }
153
177
 
154
178
  // Proceed to final submission if no errors
@@ -159,5 +183,5 @@ export function govcyReviewPostHandler() {
159
183
  return next(error);
160
184
  }
161
185
  };
162
-
186
+
163
187
  }
@@ -45,7 +45,7 @@ export function govcyRoutePageHandler(req, res, next) {
45
45
  pageData.site.lang = req.globalLang;
46
46
  //if user is logged in add he user bane section in the page template
47
47
  if (dataLayer.getUser(req.session)) {
48
- pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
48
+ pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
49
49
  }
50
50
  const renderer = new govcyFrontendRenderer();
51
51
  const html = renderer.renderFromJSON(pageTemplate, pageData);
@@ -65,7 +65,13 @@ export function govcySuccessPageHandler(isPDF = false) {
65
65
  const successPanel = {
66
66
  element: "panel",
67
67
  params: {
68
- header: govcyResources.staticResources.text.submissionSuccessTitle,
68
+ // if serviceCopy has site.successPageHeader use it otherwise use the static resource. it should test if serviceCopy.site.successPageHeader[req.globalLang] exists
69
+ header: (
70
+ serviceCopy?.site?.successPageHeader?.[req.globalLang]
71
+ ? serviceCopy.site.successPageHeader
72
+ : govcyResources.staticResources.text.submissionSuccessTitle
73
+ ),
74
+ // header: govcyResources.staticResources.text.submissionSuccessTitle,
69
75
  body: govcyResources.staticResources.text.yourSubmissionId,
70
76
  referenceNumber: govcyResources.getSameMultilingualObject(serviceCopy.site.languages,submissionData.referenceNumber)
71
77
  }
@@ -92,11 +98,6 @@ export function govcySuccessPageHandler(isPDF = false) {
92
98
  // Append generated summary list to the page template
93
99
  pageTemplate.sections.push({ name: "main", elements: mainElements });
94
100
 
95
- //if user is logged in add he user bane section in the page template
96
- if (dataLayer.getUser(req.session)) {
97
- pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
98
- }
99
-
100
101
  //prepare pageData
101
102
  pageData.site = serviceCopy.site;
102
103
  pageData.pageData.title = govcyResources.staticResources.text.submissionSuccessTitle;
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg"
2
+ width="24" height="24" viewBox="0 0 24 24" fill="none">
3
+ <path d="M12 5v14M5 12h14"
4
+ stroke="#31576F"
5
+ stroke-width="2"
6
+ stroke-linecap="round"
7
+ stroke-linejoin="round"/>
8
+ </svg>
@@ -10,7 +10,7 @@ var _govcyPrevFocus = null;
10
10
  var _govcyDisabledEls = [];
11
11
 
12
12
  // 🔁 Loop over each file input and attach a change event listener
13
- fileInputs.forEach(function(input) {
13
+ fileInputs.forEach(function (input) {
14
14
  input.addEventListener('change', _uploadFileEventHandler);
15
15
  });
16
16
 
@@ -19,19 +19,19 @@ fileInputs.forEach(function(input) {
19
19
  * @param {*} root The root element whose focusable children will be disabled
20
20
  */
21
21
  function disableFocusables(root) {
22
- var sel = 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
23
- var nodes = root.querySelectorAll(sel);
24
- _govcyDisabledEls = [];
25
- for (var i = 0; i < nodes.length; i++) {
26
- var el = nodes[i];
27
- if (_govcyOverlay.contains(el)) continue; // don’t disable overlay itself
28
- var prev = el.getAttribute('tabindex');
29
- el.setAttribute('data-prev-tabindex', prev === null ? '' : prev);
30
- el.setAttribute('tabindex', '-1');
31
- _govcyDisabledEls.push(el);
32
- }
33
- root.setAttribute('aria-hidden', 'true'); // hide from AT on fallback
34
- root.setAttribute('aria-busy', 'true');
22
+ var sel = 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
23
+ var nodes = root.querySelectorAll(sel);
24
+ _govcyDisabledEls = [];
25
+ for (var i = 0; i < nodes.length; i++) {
26
+ var el = nodes[i];
27
+ if (_govcyOverlay.contains(el)) continue; // don’t disable overlay itself
28
+ var prev = el.getAttribute('tabindex');
29
+ el.setAttribute('data-prev-tabindex', prev === null ? '' : prev);
30
+ el.setAttribute('tabindex', '-1');
31
+ _govcyDisabledEls.push(el);
32
+ }
33
+ root.setAttribute('aria-hidden', 'true'); // hide from AT on fallback
34
+ root.setAttribute('aria-busy', 'true');
35
35
  }
36
36
 
37
37
  /**
@@ -39,15 +39,15 @@ function disableFocusables(root) {
39
39
  * @param {*} root The root element whose focusable children will be restored
40
40
  */
41
41
  function restoreFocusables(root) {
42
- for (var i = 0; i < _govcyDisabledEls.length; i++) {
43
- var el = _govcyDisabledEls[i];
44
- var prev = el.getAttribute('data-prev-tabindex');
45
- if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev);
46
- el.removeAttribute('data-prev-tabindex');
47
- }
48
- _govcyDisabledEls = [];
49
- root.removeAttribute('aria-hidden');
50
- root.removeAttribute('aria-busy');
42
+ for (var i = 0; i < _govcyDisabledEls.length; i++) {
43
+ var el = _govcyDisabledEls[i];
44
+ var prev = el.getAttribute('data-prev-tabindex');
45
+ if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev);
46
+ el.removeAttribute('data-prev-tabindex');
47
+ }
48
+ _govcyDisabledEls = [];
49
+ root.removeAttribute('aria-hidden');
50
+ root.removeAttribute('aria-busy');
51
51
  }
52
52
 
53
53
  /**
@@ -56,48 +56,48 @@ function restoreFocusables(root) {
56
56
  * @returns
57
57
  */
58
58
  function trapTab(e) {
59
- if (e.key !== 'Tab') return;
60
- var focusables = _govcyOverlay.querySelectorAll('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
61
- if (focusables.length === 0) { e.preventDefault(); _govcyOverlay.focus(); return; }
62
- var first = focusables[0], last = focusables[focusables.length - 1];
63
- if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
64
- else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
59
+ if (e.key !== 'Tab') return;
60
+ var focusables = _govcyOverlay.querySelectorAll('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
61
+ if (focusables.length === 0) { e.preventDefault(); _govcyOverlay.focus(); return; }
62
+ var first = focusables[0], last = focusables[focusables.length - 1];
63
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
64
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
65
65
  }
66
66
 
67
67
  /**
68
68
  * Shows the loading spinner overlay and traps focus within it
69
69
  */
70
70
  function showLoadingSpinner() {
71
- _govcyPrevFocus = document.activeElement;
72
- _govcyOverlay.setAttribute('aria-hidden', 'false');
73
- _govcyOverlay.setAttribute('tabindex', '-1');
74
- _govcyOverlay.style.display = 'flex';
75
- document.documentElement.style.overflow = 'hidden';
76
-
77
- if ('inert' in HTMLElement.prototype) { // progressive enhancement
78
- _govcyAppRoot.inert = true;
79
- } else {
80
- disableFocusables(_govcyAppRoot);
81
- document.addEventListener('keydown', trapTab, true);
82
- }
83
- _govcyOverlay.focus();
71
+ _govcyPrevFocus = document.activeElement;
72
+ _govcyOverlay.setAttribute('aria-hidden', 'false');
73
+ _govcyOverlay.setAttribute('tabindex', '-1');
74
+ _govcyOverlay.style.display = 'flex';
75
+ document.documentElement.style.overflow = 'hidden';
76
+
77
+ if ('inert' in HTMLElement.prototype) { // progressive enhancement
78
+ _govcyAppRoot.inert = true;
79
+ } else {
80
+ disableFocusables(_govcyAppRoot);
81
+ document.addEventListener('keydown', trapTab, true);
82
+ }
83
+ _govcyOverlay.focus();
84
84
  }
85
85
 
86
86
  /**
87
87
  * Hides the loading spinner overlay and restores focus to the previously focused element
88
88
  */
89
89
  function hideLoadingSpinner() {
90
- _govcyOverlay.style.display = 'none';
91
- _govcyOverlay.setAttribute('aria-hidden', 'true');
92
- document.documentElement.style.overflow = '';
90
+ _govcyOverlay.style.display = 'none';
91
+ _govcyOverlay.setAttribute('aria-hidden', 'true');
92
+ document.documentElement.style.overflow = '';
93
93
 
94
- if ('inert' in HTMLElement.prototype) {
95
- _govcyAppRoot.inert = false;
96
- } else {
97
- restoreFocusables(_govcyAppRoot);
98
- document.removeEventListener('keydown', trapTab, true);
99
- }
100
- if (_govcyPrevFocus && _govcyPrevFocus.focus) _govcyPrevFocus.focus();
94
+ if ('inert' in HTMLElement.prototype) {
95
+ _govcyAppRoot.inert = false;
96
+ } else {
97
+ restoreFocusables(_govcyAppRoot);
98
+ document.removeEventListener('keydown', trapTab, true);
99
+ }
100
+ if (_govcyPrevFocus && _govcyPrevFocus.focus) _govcyPrevFocus.focus();
101
101
  }
102
102
 
103
103
 
@@ -165,24 +165,40 @@ function _uploadFileEventHandler(event) {
165
165
  formData.append('file', file); // Attach the actual file
166
166
  formData.append('elementName', elementName); // Attach the field name for backend lookup
167
167
 
168
- // 🚀 CHANGED: using fetch instead of axios.post
169
- fetch(`/apis/${siteId}/${pageUrl}/upload`, {
168
+ // 🚀 Build upload URL depending on mode (single / multiple/add / multiple/edit/:index)
169
+ var pathname = window.location.pathname;
170
+ var uploadUrl;
171
+
172
+ if (/\/multiple\/add\/?$/.test(pathname)) {
173
+ uploadUrl = `/apis/${siteId}/${pageUrl}/multiple/add/upload`;
174
+ } else {
175
+ var editMatch = pathname.match(/\/multiple\/edit\/(\d+)(?:\/|$)/);
176
+ if (editMatch) {
177
+ var idx = editMatch[1];
178
+ uploadUrl = `/apis/${siteId}/${pageUrl}/multiple/edit/${idx}/upload`;
179
+ } else {
180
+ uploadUrl = `/apis/${siteId}/${pageUrl}/upload`;
181
+ }
182
+ }
183
+
184
+ fetch(uploadUrl, {
170
185
  method: "POST",
171
186
  headers: {
172
187
  "X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
173
188
  },
174
189
  body: formData
175
190
  })
176
- .then(function(response) {
191
+
192
+ .then(function (response) {
177
193
  // 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
178
194
  if (!response.ok) {
179
- return response.json().then(function(errData) {
195
+ return response.json().then(function (errData) {
180
196
  throw { response: { data: errData } };
181
197
  });
182
198
  }
183
199
  return response.json();
184
200
  })
185
- .then(function(data) {
201
+ .then(function (data) {
186
202
  // ✅ Success response
187
203
  var sha256 = data.Data.sha256;
188
204
  var fileId = data.Data.fileId;
@@ -190,7 +206,7 @@ function _uploadFileEventHandler(event) {
190
206
  // 📝 Store returned metadata in hidden fields if needed
191
207
  // document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
192
208
  // document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
193
-
209
+
194
210
  // Hide loading spinner
195
211
  hideLoadingSpinner();
196
212
 
@@ -200,15 +216,15 @@ function _uploadFileEventHandler(event) {
200
216
  // Accessibility: Update ARIA live region with success message
201
217
  var statusRegion = document.getElementById('_govcy-upload-status');
202
218
  if (statusRegion) {
203
- setTimeout(function() {
219
+ setTimeout(function () {
204
220
  statusRegion.textContent = messages.uploadSuccesful[lang];
205
221
  }, 200);
206
- setTimeout(function() {
222
+ setTimeout(function () {
207
223
  statusRegion.textContent = '';
208
224
  }, 5000);
209
225
  }
210
226
  })
211
- .catch(function(err) {
227
+ .catch(function (err) {
212
228
  // ⚠️ Show an error message if upload fails
213
229
  var errorMessage = messages.uploadFailed;
214
230
  var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
@@ -245,7 +261,7 @@ function _uploadFileEventHandler(event) {
245
261
  * @param {object} errorMessage The error message in all supported languages
246
262
  */
247
263
  function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
248
-
264
+
249
265
  // Grab the query string part (?foo=bar&route=something)
250
266
  var queryString = window.location.search;
251
267
  // Parse it
@@ -263,7 +279,7 @@ function _renderFileElement(elementState, elementId, elementName, fileId, sha256
263
279
  "lang": lang
264
280
  }
265
281
  };
266
- var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
282
+ var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
267
283
  var fileElement = fileInputMap[elementName];
268
284
  fileElement.element = elementState;
269
285
  if (errorMessage != null) fileElement.params.error = errorMessage;
@@ -271,22 +287,27 @@ function _renderFileElement(elementState, elementId, elementName, fileId, sha256
271
287
  if (sha256 != null) fileElement.params.sha256 = sha256;
272
288
  if (elementState == "fileView") {
273
289
  fileElement.params.visuallyHiddenText = fileElement.params.label;
274
- // TODO: Also need to set the `view` and `download` URLs
275
- fileElement.params.viewHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/view-file/" + elementName;
290
+ // Use the actual current path (works for single, draft, edit)
291
+ var basePath = window.location.pathname.replace(/\/$/, "");
292
+
293
+ // View link
294
+ fileElement.params.viewHref = basePath + "/view-file/" + elementName;
276
295
  fileElement.params.viewTarget = "_blank";
277
- fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/delete-file/" + elementName
296
+
297
+ // Delete link (preserve ?route=review if present)
298
+ fileElement.params.deleteHref = basePath + "/delete-file/" + elementName
278
299
  + (route !== null ? "?route=" + encodeURIComponent(route) : "");
279
300
  }
280
301
  // Construct the JSONTemplate
281
302
  var JSONTemplate = {
282
303
  "elements": [fileElement]
283
304
  };
284
-
305
+
285
306
  //render HTML into string
286
- var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
287
- var outerElement = document.getElementById(`${elementId}-outer-control`)
288
- || document.getElementById(`${elementId}-input-control`)
289
- || document.getElementById(`${elementId}-view-control`);
307
+ var renderedHtml = renderer.renderFromJSON(JSONTemplate, inputData);
308
+ var outerElement = document.getElementById(`${elementId}-outer-control`)
309
+ || document.getElementById(`${elementId}-input-control`)
310
+ || document.getElementById(`${elementId}-view-control`);
290
311
 
291
312
  if (outerElement) {
292
313
  //remove all classes from outerElement
@@ -26,6 +26,16 @@ export const staticResources = {
26
26
  el: "Αλλαγή",
27
27
  tr: "Değişiklik"
28
28
  },
29
+ delete : {
30
+ en: "Delete",
31
+ el: "Διαγραφή",
32
+ tr: "Delete"
33
+ },
34
+ untitled : {
35
+ en: "Untitled",
36
+ el: "Χωρίς τίτλο",
37
+ tr: "Untitled"
38
+ },
29
39
  formSuccess: {
30
40
  en: "Your form has been submitted!",
31
41
  el: "Η φόρμα σας έχει υποβληθεί!" ,
@@ -150,6 +160,66 @@ export const staticResources = {
150
160
  en: "Υou have uploaded the same file more than once in this application. If you delete it, it will be deleted from all places in the application.",
151
161
  el: "Έχετε ανεβάσει το αρχείο αυτό και σε άλλα σημεία της αίτησης. Αν το διαγράψετε, θα διαγραφεί από όλα τα σημεία.",
152
162
  tr: "Υou have uploaded the same file more than once in this application. If you delete it, it will be deleted from all places in the application."
163
+ },
164
+ multipleThingsEnptyState: {
165
+ en: "You did not add any entries.",
166
+ el: "Δεν έχετε προσθέσει ακόμη κάποια καταχώρηση.",
167
+ tr: "You did not add any entries."
168
+ },
169
+ multipleThingsEmptyStateReview: {
170
+ en: "You did not add any entries.",
171
+ el: "Δεν έχετε προσθέσει κάποια καταχώρηση.",
172
+ tr: "You did not add any entries yet."
173
+ },
174
+ multipleThingsAddEntry: {
175
+ en: "➕ Add new entry",
176
+ el: "➕ Προσθήκη νέας καταχώρησης",
177
+ tr: "➕ Add new entry"
178
+ },
179
+ multipleThingsDedupeMessage: {
180
+ en: "This entry already exists",
181
+ el: "Αυτή η καταχώριση υπάρχει ήδη",
182
+ tr: "This entry already exists"
183
+ },
184
+ multipleThingsMaxMessage: {
185
+ en: "You have reached the maximum number of entries. You can only add up to {{max}} entries",
186
+ el: "Έχετε φτάσει το μέγιστο αριθμό καταχωρίσεων. Μπορείτε να προσθέσετε μόνο έως {{max}} καταχωρίσεις",
187
+ tr: "You have reached the maximum number of entries. You can only add up to {{max}} entries"
188
+ },
189
+ multipleThingsMinMessage: {
190
+ en: "You have not added the minimum number of entries. You must add at least {{min}} entries",
191
+ el: "Δεν έχετε προσθέσει τον ελάχιστο αριθμό καταχωρίσεων. Πρέπει να προσθέσετε τουλάχιστον {{min}} καταχωρίσεις",
192
+ tr: "You have not added the minimum number of entries. You must add at least {{min}} entries"
193
+ },
194
+ multipleThingsItemsValidationPrefix: {
195
+ en: "Entry {{index}} - ",
196
+ el: "Καταχώρηση {{index}} - ",
197
+ tr: "Entry {{index}} - "
198
+ },
199
+ multipleThingsAddSuffix: {
200
+ en: " (Add)",
201
+ el: " (Προσθήκη)",
202
+ tr: " (Add)"
203
+ },
204
+ multipleThingsEditSuffix: {
205
+ en: " (Change)",
206
+ el: " (Αλλαγή)",
207
+ tr: " (Change)"
208
+ },
209
+ multipleThingsDeleteTitle: {
210
+ en: "Are you sure you want to delete the item \"{{item}}\"",
211
+ el: "Σίγουρα θέλετε να διαγράψετε την καταχώρηση \"{{item}}\"",
212
+ tr: "Are you sure you want to delete the item \"{{item}}\""
213
+ },
214
+ multipleThingsDeleteValidationError: {
215
+ en: "Select if you want to delete this item",
216
+ el: "Επιλέξτε αν θέλετε να διαγράψετε αυτή την καταχώρηση",
217
+ tr: "Select if you want to delete the item"
218
+ },
219
+ multipleThingsEntries: {
220
+ en: "Entries",
221
+ el: "Καταχωρήσεις",
222
+ tr: "Entries"
153
223
  }
154
224
  },
155
225
  //remderer sections
@@ -475,6 +545,7 @@ export function getMultilingualObject(el, en, tr) {
475
545
  */
476
546
  export function getSameMultilingualObject(languages, value) {
477
547
  const obj = {};
548
+ if (!Array.isArray(languages)) return {el: value, en: value, tr: value};
478
549
  for (const lang of languages) {
479
550
  obj[lang.code] = value || "";
480
551
  }
@@ -510,4 +581,61 @@ export function getEmailObject( subject, preHeader, header, username, body, foot
510
581
  footerText: getLocalizeContent(footer, usedLang)
511
582
  }
512
583
  }
584
+ }
585
+
586
+ /**
587
+ * Get the link for multiple things hub and add/edit/delete pages
588
+ * @param {string} linkType The type of link. Can be `add`, `edit`, `delete`
589
+ * @param {string} siteId The site id
590
+ * @param {string} pageUrl The page url
591
+ * @param {string} lang The page language
592
+ * @param {string} entryKey The entry key. If not provided, it will be set to an empty string.
593
+ * @param {string} route Whether it comes from the `review` route
594
+ * @param {string} linkText The link text. If not provided, it will be set to an empty string.
595
+ * @param {number} count The current count of entries. If not provided, it will be set to null.
596
+ * @returns {string} The link htmlElement govcy-frontend-renderer object
597
+ */
598
+ export function getMultipleThingsLink(linkType, siteId, pageUrl, lang , entryKey = "", route = "", linkText = "", count = null) {
599
+ // Generate the action part of the URL based on the linkType
600
+ let actionPart = "";
601
+ let linkTextString = "";
602
+ switch (linkType) {
603
+ case "add":
604
+ actionPart = `multiple/add`;
605
+ // if linkText is not provided, use the default text from staticResources
606
+ linkTextString = (linkText
607
+ ? linkText
608
+ : staticResources.text.multipleThingsAddEntry[lang] || staticResources.text.multipleThingsAddEntry["el"]
609
+ );
610
+ break;
611
+ case "edit":
612
+ actionPart = `multiple/edit/${entryKey}`;
613
+ linkTextString = staticResources.text.change[lang] || staticResources.text.change["el"];
614
+ break;
615
+ case "delete":
616
+ actionPart = `multiple/delete/${entryKey}`;
617
+ linkTextString = staticResources.text.delete[lang] || staticResources.text.delete["el"];
618
+ break;
619
+ default:
620
+ actionPart = `multiple/add`;
621
+ // if linkText is not provided, use the default text from staticResources
622
+ linkTextString = (linkText
623
+ ? linkText
624
+ : staticResources.text.multipleThingsAddEntry[lang] || staticResources.text.multipleThingsAddEntry["el"]
625
+ );
626
+ }
627
+ // full part of the URL
628
+ const fullPath = `/${siteId}${pageUrl ? `/${pageUrl}` : ""}/${actionPart}/${route ? `?route=${route}` : ""}`;
629
+ // return the link htmlElement govcy-frontend-renderer object
630
+ return {
631
+ element: "htmlElement",
632
+ params: {
633
+ text: {
634
+ en: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
635
+ el: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
636
+ tr: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`
637
+ }
638
+ }
639
+ };
640
+
513
641
  }
@@ -10,7 +10,7 @@
10
10
  export function isApiRequest(req) {
11
11
  const acceptJson = (req.headers?.accept || "").toLowerCase().includes("application/json");
12
12
 
13
- const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+\/(upload|download)$/;
13
+ const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+(?:\/.*)?\/(upload|download)$/;
14
14
  const isStructuredApiUrl = apiUrlPattern.test(req.originalUrl || req.url);
15
15
 
16
16
  return acceptJson || isStructuredApiUrl;