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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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" ? ` class="govcy-add-new-item" id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
635
+ el: `<p><a${(count !== null && linkType === "add" ? ` class="govcy-add-new-item" id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
636
+ tr: `<p><a${(count !== null && linkType === "add" ? ` class="govcy-add-new-item" 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;
@@ -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
+