@gov-cy/govcy-express-services 1.0.0-alpha.9 → 1.1.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.
@@ -79,7 +79,10 @@ export function govcyReviewPageHandler() {
79
79
  //--------- End Handle Validation Errors ---------
80
80
 
81
81
  // Add elements to the main section, the H1, summary list, the submit button and the JS
82
- mainElements.push(pageH1, summaryList, submitButton, govcyResources.staticResources.elements["govcyFormsJs"]);
82
+ mainElements.push(pageH1,
83
+ summaryList,
84
+ submitButton
85
+ );
83
86
  // Append generated summary list to the page template
84
87
  pageTemplate.sections.push({ name: "main", elements: mainElements });
85
88
 
@@ -118,7 +118,7 @@ export function govcyReviewPostHandler() {
118
118
 
119
119
  //-- Send email to user
120
120
  // Generate the email body
121
- let emailBody = generateSubmitEmail(service, submissionData.print_friendly_data, referenceNo, req);
121
+ let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
122
122
  logger.debug("Email generated:", emailBody);
123
123
  // Send the email
124
124
  sendEmail(service.site.title[service.site.lang],emailBody,[dataLayer.getUser(req.session).email], "eMail").catch(err => {
@@ -78,7 +78,7 @@ export function govcySuccessPageHandler(isPDF = false) {
78
78
  }
79
79
  }
80
80
 
81
- let summaryList = submissionData.renderer_data;
81
+ let summaryList = submissionData.rendererData;
82
82
 
83
83
  let mainElements = [];
84
84
  // Add elements to the main section
@@ -87,8 +87,7 @@ export function govcySuccessPageHandler(isPDF = false) {
87
87
  weHaveSendYouAnEmail,
88
88
  pdfLink,
89
89
  theDataFromYourRequest,
90
- summaryList,
91
- govcyResources.staticResources.elements["govcyFormsJs"]
90
+ summaryList
92
91
  );
93
92
  // Append generated summary list to the page template
94
93
  pageTemplate.sections.push({ name: "main", elements: mainElements });
@@ -1,11 +1,105 @@
1
1
  // 🔍 Select all file inputs that have the .govcy-file-upload class
2
2
  var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
3
3
 
4
+ // select overlay and app root elements
5
+ var _govcyOverlay = document.getElementById("govcy--loadingOverlay");
6
+ var _govcyAppRoot = document.getElementById("govcy--body");
7
+
8
+ // Accessibility: Keep track of previously focused element and disabled elements
9
+ var _govcyPrevFocus = null;
10
+ var _govcyDisabledEls = [];
11
+
4
12
  // 🔁 Loop over each file input and attach a change event listener
5
13
  fileInputs.forEach(function(input) {
6
14
  input.addEventListener('change', _uploadFileEventHandler);
7
15
  });
8
16
 
17
+ /**
18
+ * Disables all focusable elements within a given root element
19
+ * @param {*} root The root element whose focusable children will be disabled
20
+ */
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');
35
+ }
36
+
37
+ /**
38
+ * Restores all focusable elements within a given root element
39
+ * @param {*} root The root element whose focusable children will be restored
40
+ */
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');
51
+ }
52
+
53
+ /**
54
+ * Traps tab key navigation within the overlay
55
+ * @param {*} e The event
56
+ * @returns
57
+ */
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(); }
65
+ }
66
+
67
+ /**
68
+ * Shows the loading spinner overlay and traps focus within it
69
+ */
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();
84
+ }
85
+
86
+ /**
87
+ * Hides the loading spinner overlay and restores focus to the previously focused element
88
+ */
89
+ function hideLoadingSpinner() {
90
+ _govcyOverlay.style.display = 'none';
91
+ _govcyOverlay.setAttribute('aria-hidden', 'true');
92
+ document.documentElement.style.overflow = '';
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();
101
+ }
102
+
9
103
 
10
104
  /**
11
105
  * Handles the upload of a file event
@@ -41,9 +135,9 @@ function _uploadFileEventHandler(event) {
41
135
  "tr": "The selected file must be a JPG, JPEG, PNG or PDF"
42
136
  },
43
137
  "uploadFailed409": {
44
- "el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 5MB",
45
- "en": "The selected file must be smaller than 5MB",
46
- "tr": "The selected file must be smaller than 5MB"
138
+ "el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 4MB",
139
+ "en": "The selected file must be smaller than 4MB",
140
+ "tr": "The selected file must be smaller than 4MB"
47
141
  }
48
142
  };
49
143
 
@@ -63,6 +157,9 @@ function _uploadFileEventHandler(event) {
63
157
 
64
158
  if (!file) return; // Exit if no file was selected
65
159
 
160
+ // Show loading spinner
161
+ showLoadingSpinner();
162
+
66
163
  // 🧵 Prepare form-data payload for the API
67
164
  var formData = new FormData();
68
165
  formData.append('file', file); // Attach the actual file
@@ -93,6 +190,9 @@ function _uploadFileEventHandler(event) {
93
190
  // 📝 Store returned metadata in hidden fields if needed
94
191
  // document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
95
192
  // document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
193
+
194
+ // Hide loading spinner
195
+ hideLoadingSpinner();
96
196
 
97
197
  // Render the file view
98
198
  _renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
@@ -117,6 +217,8 @@ function _uploadFileEventHandler(event) {
117
217
  errorMessage = messages["uploadFailed" + errorCode];
118
218
  }
119
219
 
220
+ // Hide loading spinner
221
+ hideLoadingSpinner();
120
222
  // Render the file input with error
121
223
  _renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
122
224
 
@@ -170,8 +272,9 @@ function _renderFileElement(elementState, elementId, elementName, fileId, sha256
170
272
  if (elementState == "fileView") {
171
273
  fileElement.params.visuallyHiddenText = fileElement.params.label;
172
274
  // TODO: Also need to set the `view` and `download` URLs
173
- fileElement.params.viewHref = "#viewHref";
174
- fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/" + elementName + "/delete-file"
275
+ fileElement.params.viewHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/view-file/" + elementName;
276
+ fileElement.params.viewTarget = "_blank";
277
+ fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/delete-file/" + elementName
175
278
  + (route !== null ? "?route=" + encodeURIComponent(route) : "");
176
279
  }
177
280
  // Construct the JSONTemplate
@@ -59,7 +59,7 @@ export const staticResources = {
59
59
  errorPage403NaturalOnlyPolicyBody: {
60
60
  el: "<p>Η πρόσβαση επιτρέπεται μόνο σε φυσικά πρόσωπα με επιβεβαιωμένο προφίλ. <a href=\"/logout\">Αποσυνδεθείτε</a> και δοκιμάστε ξανά αργότερα.</p>",
61
61
  en: "<p>Access is only allowed to individuals with a verified profile.<a href=\"/logout\">Sign out</a> and try again later.</p>",
62
- tr: "<p>Access is only allowed to individuals with a confirmed profile.<a href=\"/logout\">Giriş yapmadan</a> sonra tekrar deneyiniz.</p>"
62
+ tr: "<p>Access is only allowed to individuals with a verified profile.<a href=\"/logout\">Giriş yapmadan</a> sonra tekrar deneyiniz.</p>"
63
63
  },
64
64
  errorPage500Title: {
65
65
  el: "Λυπούμαστε, υπάρχει πρόβλημα με την υπηρεσία",
@@ -123,7 +123,7 @@ export const staticResources = {
123
123
  },
124
124
  deleteFileTitle : {
125
125
  en: "Are you sure you want to delete the file \"{{file}}\"? ",
126
- el: "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
126
+ el: "Σίγουρα θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
127
127
  tr: "Are you sure you want to delete the file \"{{file}}\"? "
128
128
  },
129
129
  deleteYesOption: {
@@ -139,7 +139,17 @@ export const staticResources = {
139
139
  deleteFileValidationError: {
140
140
  en: "Select if you want to delete the file",
141
141
  el: "Επιλέξτε αν θέλετε να διαγράψετε το αρχείο",
142
- tr: "Select if you want to delete the filez"
142
+ tr: "Select if you want to delete the file"
143
+ },
144
+ viewFile: {
145
+ en: "View file",
146
+ el: "Προβολή αρχείου",
147
+ tr: "View file"
148
+ },
149
+ deleteSameFileWarning: {
150
+ 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
+ el: "Έχετε ανεβάσει το αρχείο αυτό και σε άλλα σημεία της αίτησης. Αν το διαγράψετε, θα διαγραφεί από όλα τα σημεία.",
152
+ 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."
143
153
  }
144
154
  },
145
155
  //remderer sections
@@ -153,9 +163,19 @@ export const staticResources = {
153
163
  element: "htmlElement",
154
164
  params: {
155
165
  text: {
156
- en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
157
- el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
158
- tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
166
+ en: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
167
+ el: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
168
+ tr: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
169
+ }
170
+ }
171
+ },
172
+ govcyLoadingOverlay: {
173
+ element: "htmlElement",
174
+ params: {
175
+ text: {
176
+ en: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Loading...</span></div></div></div>`,
177
+ el: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Φόρτωση...</span></div></div></div>`,
178
+ tr: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Loading...</span></div></div></div>`
159
179
  }
160
180
  }
161
181
  },
@@ -4,5 +4,5 @@
4
4
  export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput","fileInput","fileView"];
5
5
  export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
6
6
  export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
7
- export const ALLOWED_FILE_SIZE_MB = 5; // Maximum file size in MB
7
+ export const ALLOWED_FILE_SIZE_MB = 4; // Maximum file size in MB
8
8
  export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
@@ -102,7 +102,7 @@ export function storePageData(store, siteId, pageUrl, formData) {
102
102
  export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
103
103
  // Ensure session structure is initialized
104
104
  initializeSiteData(store, siteId, pageUrl);
105
-
105
+
106
106
  // Store the element value
107
107
  store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
108
108
  }
@@ -191,11 +191,11 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
191
191
  // let rawData = getSiteInputData(store, siteId);
192
192
  // Store the submission data
193
193
  store.siteData[siteId].submissionData = submissionData;
194
-
194
+
195
195
  // Clear validation errors from the session
196
196
  store.siteData[siteId].inputData = {};
197
197
  // Clear presaved/temporary save data
198
- store.siteData[siteId].loadData = {};
198
+ store.siteData[siteId].loadData = {};
199
199
 
200
200
  }
201
201
 
@@ -208,7 +208,7 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
208
208
  * @param {object} result - API response
209
209
  */
210
210
  export function storeSiteEligibilityResult(store, siteId, endpointKey, result) {
211
-
211
+
212
212
  initializeSiteData(store, siteId); // Ensure the structure exists
213
213
 
214
214
  if (!store.siteData[siteId].eligibility) store.siteData[siteId].eligibility = {};
@@ -244,7 +244,7 @@ export function getSiteEligibilityResult(store, siteId, endpointKey, maxAgeMs =
244
244
  */
245
245
  export function getPageValidationErrors(store, siteId, pageUrl) {
246
246
  const validationErrors = store?.siteData?.[siteId]?.inputData?.[pageUrl]?.validationErrors || null;
247
-
247
+
248
248
  if (validationErrors) {
249
249
  // Clear validation errors from the session
250
250
  delete store.siteData[siteId].inputData[pageUrl].validationErrors;
@@ -275,7 +275,7 @@ export function getPageData(store, siteId, pageUrl) {
275
275
  */
276
276
  export function getSiteSubmissionErrors(store, siteId) {
277
277
  const validationErrors = store?.siteData?.[siteId]?.submissionErrors || null;
278
-
278
+
279
279
  if (validationErrors) {
280
280
  // Clear validation errors from the session
281
281
  delete store.siteData[siteId].submissionErrors;
@@ -294,7 +294,7 @@ export function getSiteSubmissionErrors(store, siteId) {
294
294
  */
295
295
  export function getSiteData(store, siteId) {
296
296
  const inputData = store?.siteData?.[siteId] || {};
297
-
297
+
298
298
  if (inputData) {
299
299
  return inputData;
300
300
  }
@@ -311,7 +311,7 @@ export function getSiteData(store, siteId) {
311
311
  */
312
312
  export function getSiteInputData(store, siteId) {
313
313
  const inputData = store?.siteData?.[siteId]?.inputData || {};
314
-
314
+
315
315
  if (inputData) {
316
316
  return inputData;
317
317
  }
@@ -328,7 +328,7 @@ export function getSiteInputData(store, siteId) {
328
328
  */
329
329
  export function getSiteLoadData(store, siteId) {
330
330
  const loadData = store?.siteData?.[siteId]?.loadData || {};
331
-
331
+
332
332
  if (loadData) {
333
333
  return loadData;
334
334
  }
@@ -344,9 +344,9 @@ export function getSiteLoadData(store, siteId) {
344
344
  * @returns {string|null} The reference number or null if not available
345
345
  */
346
346
  export function getSiteLoadDataReferenceNumber(store, siteId) {
347
- const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
347
+ const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
348
348
 
349
- return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
349
+ return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
350
350
  }
351
351
 
352
352
 
@@ -359,9 +359,9 @@ export function getSiteLoadDataReferenceNumber(store, siteId) {
359
359
  */
360
360
  export function getSiteSubmissionData(store, siteId) {
361
361
  initializeSiteData(store, siteId); // Ensure the structure exists
362
-
362
+
363
363
  const submission = store?.siteData?.[siteId]?.submissionData || {};
364
-
364
+
365
365
  if (submission) {
366
366
  return submission;
367
367
  }
@@ -388,7 +388,7 @@ export function getFormDataValue(store, siteId, pageUrl, elementName) {
388
388
  * @param {object} store The session store
389
389
  * @returns The user object from the store or null if it doesn't exist.
390
390
  */
391
- export function getUser(store){
391
+ export function getUser(store) {
392
392
  return store.user || null;
393
393
  }
394
394
 
@@ -396,3 +396,181 @@ export function clearSiteData(store, siteId) {
396
396
  delete store?.siteData[siteId];
397
397
  }
398
398
 
399
+ /**
400
+ * Check if a file reference is used in more than one place (field) across the site's inputData.
401
+ *
402
+ * A "file reference" is an object like:
403
+ * { sha256: "abc...", fileId: "xyz..." }
404
+ *
405
+ * Matching rules:
406
+ * - If both fileId and sha256 are provided, both must match.
407
+ * - If only one is provided, we match by that single property.
408
+ *
409
+ * Notes:
410
+ * - Does NOT mutate the session.
411
+ * - Safely handles missing site/pages.
412
+ * - If a form field is an array (e.g., multiple file inputs), each item is checked.
413
+ *
414
+ * @param {object} store The session object (e.g., req.session)
415
+ * @param {string} siteId The site identifier
416
+ * @param {object} params { fileId?: string, sha256?: string }
417
+ * @returns {boolean} true if the file is found in more than one place, else false
418
+ */
419
+ export function isFileUsedInSiteInputDataAgain(store, siteId, { fileId, sha256 } = {}) {
420
+ // If neither identifier is provided, we cannot match anything
421
+ if (!fileId && !sha256) return false;
422
+
423
+ // Ensure session structure is initialized
424
+ initializeSiteData(store, siteId);
425
+
426
+ // Site input data: session.siteData[siteId].inputData
427
+ const site = store?.siteData?.[siteId]?.inputData;
428
+ if (!site || typeof site !== 'object') return false;
429
+
430
+ let hits = 0; // how many fields across the site reference this file
431
+
432
+ // Loop all pages under the site
433
+ for (const pageKey of Object.keys(site)) {
434
+ const formData = site[pageKey]?.formData;
435
+ if (!formData || typeof formData !== 'object') continue;
436
+
437
+ // Loop all fields on the page
438
+ for (const [_, value] of Object.entries(formData)) {
439
+ if (value == null) continue;
440
+
441
+ // Normalize to an array to also support multi-value fields (e.g., multiple file inputs)
442
+ const candidates = Array.isArray(value) ? value : [value];
443
+
444
+ for (const candidate of candidates) {
445
+ // We only consider objects that look like file references
446
+ if (
447
+ candidate &&
448
+ typeof candidate === 'object' &&
449
+ 'fileId' in candidate &&
450
+ 'sha256' in candidate
451
+ ) {
452
+ const idMatches = fileId ? candidate.fileId === fileId : true;
453
+ const shaMatches = sha256 ? candidate.sha256 === sha256 : true;
454
+
455
+ if (idMatches && shaMatches) {
456
+ hits += 1;
457
+ // As soon as we see it in more than one place, we can answer true
458
+ if (hits > 1) return true;
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ // If we get here, it was used 0 or 1 times
466
+ return false;
467
+ }
468
+
469
+
470
+ /**
471
+ * Remove (replace with "") file values across ALL pages of a site,
472
+ * matching a specific fileId and/or sha256.
473
+ *
474
+ * Matching rules:
475
+ * - If BOTH fileId and sha256 are provided, a file object must match BOTH.
476
+ * - If ONLY fileId is provided, match on fileId.
477
+ * - If ONLY sha256 is provided, match on sha256.
478
+ *
479
+ * Scope:
480
+ * - Operates on every page under store.siteData[siteId].inputData.
481
+ * - Shallow traversal of formData:
482
+ * • Direct fields: formData[elementId] = { fileId, sha256, ... }
483
+ * • Arrays: [ {fileId...}, {sha256...}, "other" ] → ["", "", "other"] (for matches)
484
+ *
485
+ * Side effects:
486
+ * - Mutates store.siteData[siteId].inputData[*].formData in place.
487
+ * - Intentionally returns nothing.
488
+ *
489
+ * @param {object} store - The data-layer store.
490
+ * @param {string} siteId - The site key under store.siteData to modify.
491
+ * @param {{ fileId?: string|null, sha256?: string|null }} match - Identifiers to match.
492
+ * Provide at least one of fileId/sha256. If both are given, both must match.
493
+ */
494
+ export function removeAllFilesFromSite(
495
+ store,
496
+ siteId,
497
+ { fileId = null, sha256 = null } = {}
498
+ ) {
499
+ // Ensure session structure is initialized
500
+ initializeSiteData(store, siteId);
501
+ // --- Guard rails ---------------------------------------------------------
502
+
503
+ // Nothing to remove if neither identifier is provided.
504
+ if (!fileId && !sha256) return;
505
+
506
+ // Per your structure: dig under .inputData for the site's pages.
507
+ const site = store?.siteData?.[siteId]?.inputData;
508
+ if (!site || typeof site !== "object") return;
509
+
510
+ // --- Helpers -------------------------------------------------------------
511
+
512
+ // Is this value a "file-like" object (has fileId and/or sha256)?
513
+ const isFileLike = (v) =>
514
+ v &&
515
+ typeof v === "object" &&
516
+ (Object.prototype.hasOwnProperty.call(v, "fileId") ||
517
+ Object.prototype.hasOwnProperty.call(v, "sha256"));
518
+
519
+ // Does a file-like object match the provided criteria?
520
+ const isMatch = (obj) => {
521
+ if (!isFileLike(obj)) return false;
522
+
523
+ // Strict when both are given
524
+ if (fileId && sha256) {
525
+ return obj.fileId === fileId && obj.sha256 === sha256;
526
+ }
527
+ // Otherwise match whichever was provided
528
+ if (fileId) return obj.fileId === fileId;
529
+ if (sha256) return obj.sha256 === sha256;
530
+
531
+ return false;
532
+ };
533
+
534
+ // --- Main traversal over all pages --------------------------------------
535
+
536
+ for (const page of Object.values(site)) {
537
+ // Each page should be an object with a formData object
538
+ const formData =
539
+ page &&
540
+ typeof page === "object" &&
541
+ page.formData &&
542
+ typeof page.formData === "object"
543
+ ? page.formData
544
+ : null;
545
+
546
+ if (!formData) continue; // skip content-only pages, etc.
547
+
548
+ // For each field on this page…
549
+ for (const key of Object.keys(formData)) {
550
+ const val = formData[key];
551
+
552
+ // Case A: a single file object → replace with "" if it matches.
553
+ if (isMatch(val)) {
554
+ formData[key] = "";
555
+ continue;
556
+ }
557
+
558
+ // Case B: an array → replace ONLY the matching items with "".
559
+ if (Array.isArray(val)) {
560
+ let changed = false;
561
+ const mapped = val.map((item) => {
562
+ if (isMatch(item)) {
563
+ changed = true;
564
+ return "";
565
+ }
566
+ return item;
567
+ });
568
+ if (changed) formData[key] = mapped;
569
+ }
570
+
571
+ // Note: If you later store file-like objects deeper in nested objects,
572
+ // add a recursive visitor here (with cycle protection / max depth).
573
+ }
574
+ }
575
+ }
576
+
@@ -78,8 +78,9 @@ export function populateFormData(formElements, theData, validationErrors, store
78
78
  element.params.sha256 = fileData.sha256;
79
79
  element.params.visuallyHiddenText = element.params.label;
80
80
  // TODO: Also need to set the `view` and `download` URLs
81
- element.params.viewHref = "#viewHref";
82
- element.params.deleteHref = `/${siteId}/${pageUrl}/${fieldName}/delete-file${(routeParam) ? `?route=${routeParam}` : ''}`;
81
+ element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
82
+ element.params.viewTarget = "_blank";
83
+ element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
83
84
  } else {
84
85
  // TODO: Ask Andreas how to handle empty file inputs
85
86
  element.params.value = "";
@@ -198,11 +198,11 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
198
198
  return {
199
199
  status: 200,
200
200
  data: {
201
- sha: response.Data.sha256,
201
+ sha256: response.Data.sha256,
202
202
  filename: response.Data.fileName || '',
203
203
  fileId: response.Data.fileId,
204
204
  mimeType: response.Data.contentType || '',
205
- sha256: response.Data.fileSize || ''
205
+ fileSize: response.Data?.fileSize || ''
206
206
  }
207
207
  };
208
208
 
@@ -290,7 +290,7 @@ export function pageContainsFileInput(pageTemplate, elementName) {
290
290
  * @param {string} mimetype
291
291
  * @returns {boolean}
292
292
  */
293
- function isMagicByteValid(buffer, mimetype) {
293
+ export function isMagicByteValid(buffer, mimetype) {
294
294
  const signatures = {
295
295
  'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
296
296
  'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG