@gov-cy/govcy-express-services 1.2.0 → 1.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +3 -2
- package/src/index.mjs +190 -49
- package/src/middleware/govcyConfigSiteData.mjs +4 -0
- package/src/middleware/govcyFileDeleteHandler.mjs +41 -19
- package/src/middleware/govcyFileUpload.mjs +14 -1
- package/src/middleware/govcyFileViewHandler.mjs +20 -1
- package/src/middleware/govcyFormsPostHandler.mjs +76 -47
- package/src/middleware/govcyMultipleThingsDeleteHandler.mjs +233 -0
- package/src/middleware/govcyMultipleThingsHubHandler.mjs +247 -0
- package/src/middleware/govcyMultipleThingsItemPage.mjs +460 -0
- package/src/middleware/govcyPageHandler.mjs +17 -5
- package/src/middleware/govcyPageRender.mjs +6 -0
- package/src/middleware/govcyReviewPageHandler.mjs +58 -32
- package/src/middleware/govcyReviewPostHandler.mjs +47 -23
- package/src/middleware/govcyRoutePageHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +0 -5
- package/src/public/img/Plus_24x24.svg +8 -0
- package/src/public/js/govcyFiles.js +92 -71
- package/src/resources/govcyResources.mjs +128 -0
- package/src/utils/govcyApiDetection.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +208 -67
- package/src/utils/govcyFormHandling.mjs +94 -27
- package/src/utils/govcyHandleFiles.mjs +58 -13
- package/src/utils/govcyMultipleThingsValidation.mjs +132 -0
- package/src/utils/govcySubmitData.mjs +221 -88
- package/src/utils/govcyValidator.mjs +19 -7
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
435
|
-
if (!
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
567
|
-
});
|
|
568
|
-
if (changed) formData[key] = mapped;
|
|
690
|
+
}
|
|
569
691
|
}
|
|
692
|
+
}
|
|
570
693
|
|
|
571
|
-
|
|
572
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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) {
|