@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.
@@ -59,19 +59,33 @@ export function initializeSiteData(store, siteId, pageUrl = null) {
59
59
  * @param {string} pageUrl The page url
60
60
  * @param {object} validationErrors The validation errors
61
61
  * @param {object} formData The form data that produced the errors
62
+ * @param {string} key The key to store the errors under. Used for multiple items. Defaults to null
62
63
  */
63
- export function storePageValidationErrors(store, siteId, pageUrl, validationErrors, formData) {
64
- // Ensure session structure is initialized
65
- initializeSiteData(store, siteId, pageUrl);
66
-
67
- // Store the validation errors
64
+ export function storePageValidationErrors(store, siteId, pageUrl, validationErrors, formData, key = null) {
65
+ // Ensure session structure is initialized
66
+ initializeSiteData(store, siteId, pageUrl);
67
+
68
+ // Build the error object
69
+ const errorObj = {
70
+ errors: validationErrors,
71
+ formData: formData,
72
+ errorSummary: []
73
+ };
74
+
75
+ // If a key is provided (e.g., "add" or "2"), store under that key
76
+ if (key !== null) {
77
+ const existing = store.siteData[siteId].inputData[pageUrl]["validationErrors"] || {};
68
78
  store.siteData[siteId].inputData[pageUrl]["validationErrors"] = {
69
- errors: validationErrors,
70
- formData: formData,
71
- errorSummary: []
79
+ ...existing,
80
+ [key]: errorObj
72
81
  };
82
+ } else {
83
+ // Normal page (no key)
84
+ store.siteData[siteId].inputData[pageUrl]["validationErrors"] = errorObj;
85
+ }
73
86
  }
74
87
 
88
+
75
89
  /**
76
90
  * Stores the page's form data in the data layer
77
91
  *
@@ -378,10 +392,43 @@ export function getSiteSubmissionData(store, siteId) {
378
392
  * @param {string} elementName The element name
379
393
  * @returns The value of the form data for the element or an empty string if none exist.
380
394
  */
381
- export function getFormDataValue(store, siteId, pageUrl, elementName) {
382
- return store?.siteData?.[siteId]?.inputData?.[pageUrl]?.formData?.[elementName] || "";
395
+ export function getFormDataValue(store, siteId, pageUrl, elementName, index = null) {
396
+ const pageData = store?.siteData?.[siteId]?.inputData?.[pageUrl];
397
+ if (!pageData) return "";
398
+
399
+ // Case 1: formData is an array (multipleThings edit)
400
+ if (Array.isArray(pageData.formData) && index !== null) {
401
+ return pageData?.formData[index]?.[elementName] || "";
402
+ }
403
+
404
+ // Case 2: formData is a flat object (single page or multipleThings add seed)
405
+ if (pageData.formData && !Array.isArray(pageData.formData)) {
406
+ const val = pageData.formData[elementName];
407
+ // If the flat value exists and is non-empty, prefer it.
408
+ const hasNonEmptyFlat =
409
+ val !== undefined &&
410
+ val !== "" &&
411
+ !(typeof val === "object" && val !== null && Object.keys(val).length === 0);
412
+ if (hasNonEmptyFlat) return val;
413
+
414
+ // Otherwise, fall back to multipleDraft (used in add flow) if present.
415
+ if (pageData.multipleDraft && typeof pageData.multipleDraft === "object") {
416
+ const draftVal = pageData.multipleDraft[elementName];
417
+ if (draftVal !== undefined && draftVal !== "") return draftVal;
418
+ }
419
+ // If neither exists, return an empty string
420
+ return "";
421
+ }
422
+
423
+ // Case 3: no flat formData; fall back to multipleDraft (used in add flow)
424
+ if (pageData.multipleDraft && typeof pageData.multipleDraft === "object") {
425
+ return pageData?.multipleDraft?.[elementName] || "";
426
+ }
427
+
428
+ return "";
383
429
  }
384
430
 
431
+
385
432
  /**
386
433
  * Get the user object from the session store
387
434
  *
@@ -389,13 +436,51 @@ export function getFormDataValue(store, siteId, pageUrl, elementName) {
389
436
  * @returns The user object from the store or null if it doesn't exist.
390
437
  */
391
438
  export function getUser(store) {
392
- return store.user || null;
439
+ return store?.user || null;
393
440
  }
394
441
 
395
442
  export function clearSiteData(store, siteId) {
396
443
  delete store?.siteData[siteId];
397
444
  }
398
445
 
446
+ /**
447
+ * Get multiple things draft data while adding a new multiple thing
448
+ *
449
+ * @param {object} store The session store
450
+ * @param {string} siteId The site id
451
+ * @param {string} pageUrl The page url
452
+ * @returns The multiple things draft data while adding a new multiple thing.
453
+ */
454
+ export function getMultipleDraft(store, siteId, pageUrl) {
455
+ return store?.siteData?.[siteId]?.inputData?.[pageUrl]?.multipleDraft || null;
456
+ }
457
+
458
+ /**
459
+ * Store multiple things draft data used while adding a new multiple thing
460
+ *
461
+ * @param {object} store The session store
462
+ * @param {string} siteId The site id
463
+ * @param {string} pageUrl The page url
464
+ * @param {*} obj The multiple things draft data to be stored
465
+ */
466
+ export function setMultipleDraft(store, siteId, pageUrl, obj) {
467
+ initializeSiteData(store, siteId, pageUrl);
468
+ store.siteData[siteId].inputData[pageUrl].multipleDraft = obj || {};
469
+ }
470
+
471
+ /**
472
+ * Clear multiple things draft data used while adding a new multiple thing
473
+ *
474
+ * @param {object} store The session store
475
+ * @param {string} siteId The site id
476
+ * @param {string} pageUrl The page url
477
+ */
478
+ export function clearMultipleDraft(store, siteId, pageUrl) {
479
+ if (store?.siteData?.[siteId]?.inputData?.[pageUrl]) {
480
+ store.siteData[siteId].inputData[pageUrl].multipleDraft = null;
481
+ }
482
+ }
483
+
399
484
  /**
400
485
  * Check if a file reference is used in more than one place (field) across the site's inputData.
401
486
  *
@@ -431,34 +516,55 @@ export function isFileUsedInSiteInputDataAgain(store, siteId, { fileId, sha256 }
431
516
 
432
517
  // Loop all pages under the site
433
518
  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;
519
+ const pageData = site[pageKey];
520
+ if (!pageData) continue;
521
+
522
+ // Helper to scan an object for file matches
523
+ const scanObject = (obj) => {
524
+ for (const value of Object.values(obj)) {
525
+ if (value == null) continue;
526
+ const candidates = Array.isArray(value) ? value : [value];
527
+ for (const candidate of candidates) {
528
+ if (
529
+ candidate &&
530
+ typeof candidate === "object" &&
531
+ "fileId" in candidate &&
532
+ "sha256" in candidate
533
+ ) {
534
+ const idMatches = fileId ? candidate.fileId === fileId : true;
535
+ const shaMatches = sha256 ? candidate.sha256 === sha256 : true;
536
+ if (idMatches && shaMatches) {
537
+ hits += 1;
538
+ if (hits > 1) return true;
539
+ }
459
540
  }
460
541
  }
461
542
  }
543
+ return false;
544
+ };
545
+
546
+ // Case 1: flat formData object
547
+ if (pageData.formData && !Array.isArray(pageData.formData)) {
548
+ if (scanObject(pageData.formData)) return true;
549
+ }
550
+
551
+ // Case 2: multipleDraft
552
+ if (pageData.multipleDraft) {
553
+ if (scanObject(pageData.multipleDraft)) return true;
554
+ }
555
+
556
+ // Case 3: formData as array (multiple items)
557
+ if (Array.isArray(pageData.formData)) {
558
+ for (const item of pageData.formData) {
559
+ if (scanObject(item)) return true;
560
+ }
561
+ }
562
+
563
+ // Case 4: formData.multipleItems array (your current design)
564
+ if (Array.isArray(pageData.formData?.multipleItems)) {
565
+ for (const item of pageData.formData.multipleItems) {
566
+ if (scanObject(item)) return true;
567
+ }
462
568
  }
463
569
  }
464
570
 
@@ -498,6 +604,7 @@ export function removeAllFilesFromSite(
498
604
  ) {
499
605
  // Ensure session structure is initialized
500
606
  initializeSiteData(store, siteId);
607
+
501
608
  // --- Guard rails ---------------------------------------------------------
502
609
 
503
610
  // Nothing to remove if neither identifier is provided.
@@ -534,43 +641,77 @@ export function removeAllFilesFromSite(
534
641
  // --- Main traversal over all pages --------------------------------------
535
642
 
536
643
  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;
644
+ if (!page || typeof page !== "object") continue;
645
+
646
+ // --- Case 1: flat formData object -----------------------------------
647
+ if (page.formData && !Array.isArray(page.formData)) {
648
+ const formData = page.formData;
649
+ for (const key of Object.keys(formData)) {
650
+ const val = formData[key];
651
+
652
+ // Case A: a single file object → replace with "" if it matches.
653
+ if (isMatch(val)) {
654
+ formData[key] = "";
655
+ continue;
656
+ }
657
+
658
+ // Case B: an array → replace ONLY the matching items with "".
659
+ if (Array.isArray(val)) {
660
+ let changed = false;
661
+ const mapped = val.map((item) => {
662
+ if (isMatch(item)) {
663
+ changed = true;
664
+ return "";
665
+ }
666
+ return item;
667
+ });
668
+ if (changed) formData[key] = mapped;
669
+ }
556
670
  }
671
+ }
557
672
 
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 "";
673
+ // --- Case 2: formData as array (multiple items) ---------------------
674
+ if (Array.isArray(page.formData)) {
675
+ for (const item of page.formData) {
676
+ if (!item || typeof item !== "object") continue;
677
+ for (const key of Object.keys(item)) {
678
+ const val = item[key];
679
+ if (isMatch(val)) {
680
+ item[key] = "";
681
+ continue;
682
+ }
683
+ if (Array.isArray(val)) {
684
+ let changed = false;
685
+ const mapped = val.map((sub) =>
686
+ isMatch(sub) ? "" : sub
687
+ );
688
+ if (changed) item[key] = mapped;
565
689
  }
566
- return item;
567
- });
568
- if (changed) formData[key] = mapped;
690
+ }
569
691
  }
692
+ }
570
693
 
571
- // Note: If you later store file-like objects deeper in nested objects,
572
- // add a recursive visitor here (with cycle protection / max depth).
694
+ // --- Case 3: multipleDraft ------------------------------------------
695
+ if (page.multipleDraft && typeof page.multipleDraft === "object") {
696
+ for (const key of Object.keys(page.multipleDraft)) {
697
+ const val = page.multipleDraft[key];
698
+ if (isMatch(val)) {
699
+ page.multipleDraft[key] = "";
700
+ continue;
701
+ }
702
+ if (Array.isArray(val)) {
703
+ let changed = false;
704
+ const mapped = val.map((sub) =>
705
+ isMatch(sub) ? "" : sub
706
+ );
707
+ if (changed) page.multipleDraft[key] = mapped;
708
+ }
709
+ }
573
710
  }
711
+
712
+ // Note: If you later store file-like objects deeper in nested objects,
713
+ // add a recursive visitor here (with cycle protection / max depth).
574
714
  }
575
715
  }
576
716
 
717
+
@@ -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,43 @@ 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
+ fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
108
+ }
109
+
75
110
  if (fileData) {
76
111
  element.element = "fileView";
77
112
  element.params.fileId = fileData.fileId;
78
113
  element.params.sha256 = fileData.sha256;
79
114
  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}`;
115
+
116
+ // Build base path based on mode
117
+ let basePath = `/${siteId}/${pageUrl}`;
118
+ if (mode === "add") {
119
+ basePath += "/multiple/add";
120
+ } else if (mode === "edit" && index !== null) {
121
+ basePath += `/multiple/edit/${index}`;
122
+ }
123
+
124
+ // View link
125
+ element.params.viewHref = `${basePath}/view-file/${fieldName}`;
82
126
  element.params.viewTarget = "_blank";
83
- element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
127
+ // Delete link (preserve ?route=review if present)
128
+ element.params.deleteHref = `${basePath}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`
129
+
130
+
84
131
  } else {
85
132
  // TODO: Ask Andreas how to handle empty file inputs
86
133
  element.params.value = "";
87
- }
134
+ }
88
135
  fileInputElements[fieldName] = element;
89
- // Handle all other input elements (textInput, checkboxes, radios, etc.)
136
+ // Handle all other input elements (textInput, checkboxes, radios, etc.)
90
137
  } else {
91
138
  element.params.value = theData[fieldName] || "";
92
139
  }
@@ -95,11 +142,11 @@ export function populateFormData(formElements, theData, validationErrors, store
95
142
  if (validationErrors?.errors?.[fieldName]) {
96
143
  element.params.error = validationErrors.errors[fieldName].message;
97
144
  //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
145
+ // const elementId = (element.element === "checkboxes" || element.element === "radios") // if checkboxes or radios
146
+ // ? `${element.params.id}-option-1` // use the id of the first option
147
+ // : (element.element === "dateInput") //if dateInput
148
+ // ? `${element.params.id}_day` // use the id of the day input
149
+ // : element.params.id; // else use the id of the element
103
150
  validationErrors.errorSummary.push({
104
151
  link: `#${elementId}`,
105
152
  text: validationErrors.errors[fieldName].message
@@ -111,8 +158,8 @@ export function populateFormData(formElements, theData, validationErrors, store
111
158
  if (element.element === "radios" && element.params.items) {
112
159
  element.params.items.forEach(item => {
113
160
  if (item.conditionalElements) {
114
- populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
115
-
161
+ populateFormData(item.conditionalElements, theData, validationErrors, store, siteId, pageUrl, lang, fileInputElements, routeParam);
162
+
116
163
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
117
164
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
118
165
  item.conditionalHasErrors = true;
@@ -121,6 +168,25 @@ export function populateFormData(formElements, theData, validationErrors, store
121
168
  });
122
169
  }
123
170
  });
171
+
172
+ // 🔴 Handle _global validation errors (collection-level, not tied to a field)
173
+ if (isRootCall && validationErrors?.errors?._global) {
174
+ validationErrors.errorSummary = validationErrors.errorSummary || [];
175
+
176
+ // Decide where the link should point
177
+ let linkTarget = `#${firstElementId}`; // default anchor at top of the form
178
+ if (validationErrors.errors._global.pageUrl) {
179
+ // If pageUrl is provided (e.g. for max items), point back to hub
180
+ linkTarget = `${validationErrors.errors._global.pageUrl}`;
181
+ }
182
+
183
+ // Push into the error summary
184
+ validationErrors.errorSummary.push({
185
+ link: linkTarget,
186
+ text: validationErrors.errors._global.message
187
+ });
188
+ }
189
+
124
190
  // add file input elements's definition in js object
125
191
  if (isRootCall && Object.keys(fileInputElements).length > 0) {
126
192
  const scriptTag = `
@@ -155,9 +221,10 @@ export function populateFormData(formElements, theData, validationErrors, store
155
221
  * @param {Object} store - The session store .
156
222
  * @param {string} siteId - The site ID .
157
223
  * @param {string} pageUrl - The page URL .
224
+ * @param {number|null} index - The index of the item being edited for multiple items
158
225
  * @returns {Object} filteredData - The filtered form data.
159
226
  */
160
- export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
227
+ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "", index = null) {
161
228
  const filteredData = {};
162
229
  elements.forEach(element => {
163
230
  const { name } = element.params || {};
@@ -174,12 +241,12 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
174
241
  if (item.conditionalElements) {
175
242
  Object.assign(
176
243
  filteredData,
177
- getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
244
+ getFormData(item.conditionalElements, formData, store, siteId, pageUrl, index)
178
245
  );
179
246
  }
180
247
  });
181
248
  }
182
-
249
+
183
250
  }
184
251
  // Handle dateInput
185
252
  else if (element.element === "dateInput") {
@@ -189,13 +256,13 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
189
256
  filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
190
257
  filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
191
258
  filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
192
- // handle fileInput
259
+ // handle fileInput
193
260
  } else if (element.element === "fileInput") {
194
261
  // fileInput elements are already stored in the store when it was uploaded
195
262
  // so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
196
263
  // unneeded handle of `Attachment` at the end
197
264
  // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
198
- const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
265
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name, index);
199
266
  if (fileData) {
200
267
  // unneeded handle of `Attachment` at the end
201
268
  // filteredData[name + "Attachment"] = fileData;
@@ -206,7 +273,7 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
206
273
  // filteredData[name + "Attachment"] = ""; // or handle as needed
207
274
  filteredData[name] = ""; // or handle as needed
208
275
  }
209
- // Handle other elements (e.g., textInput, textArea, datePicker)
276
+ // Handle other elements (e.g., textInput, textArea, datePicker)
210
277
  } else {
211
278
  filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
212
279
  }
@@ -216,9 +283,9 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
216
283
  // Object.assign(filteredData, getFormData(element.conditionalElements, formData));
217
284
  // }
218
285
  }
219
-
286
+
220
287
  });
221
-
288
+
222
289
 
223
290
  return filteredData;
224
- }
291
+ }
@@ -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) {