@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.
- package/README.md +1033 -79
- package/package.json +9 -3
- package/src/auth/cyLoginAuth.mjs +2 -1
- package/src/index.mjs +9 -5
- package/src/middleware/cyLoginAuth.mjs +3 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
- package/src/middleware/{govcyUpload.mjs → govcyFileUpload.mjs} +1 -1
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyPDFRender.mjs +3 -1
- package/src/middleware/govcyPageHandler.mjs +1 -5
- package/src/middleware/govcyPageRender.mjs +10 -0
- package/src/middleware/govcyReviewPageHandler.mjs +4 -1
- package/src/middleware/govcyReviewPostHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +2 -3
- package/src/public/js/govcyFiles.js +108 -5
- package/src/resources/govcyResources.mjs +26 -6
- package/src/utils/govcyConstants.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +192 -14
- package/src/utils/govcyFormHandling.mjs +3 -2
- package/src/utils/govcyHandleFiles.mjs +3 -3
- package/src/utils/govcySubmitData.mjs +162 -109
- package/src/middleware/govcyDeleteFileHandler.mjs +0 -234
|
@@ -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,
|
|
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.
|
|
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.
|
|
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": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από
|
|
45
|
-
"en": "The selected file must be smaller than
|
|
46
|
-
"tr": "The selected file must be smaller than
|
|
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 = "
|
|
174
|
-
fileElement.params.
|
|
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
|
|
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: "
|
|
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
|
|
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/
|
|
157
|
-
el: `<script src="https://cdn.jsdelivr.net/
|
|
158
|
-
tr: `<script src="https://cdn.jsdelivr.net/
|
|
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 =
|
|
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
|
-
|
|
347
|
+
const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
|
|
348
348
|
|
|
349
|
-
|
|
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 =
|
|
82
|
-
element.params.
|
|
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
|
-
|
|
201
|
+
sha256: response.Data.sha256,
|
|
202
202
|
filename: response.Data.fileName || '',
|
|
203
203
|
fileId: response.Data.fileId,
|
|
204
204
|
mimeType: response.Data.contentType || '',
|
|
205
|
-
|
|
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
|