@gov-cy/govcy-express-services 1.0.0-alpha.6 → 1.0.0-alpha.8
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/package.json +1 -1
- package/src/index.mjs +8 -8
- package/src/middleware/govcyFormsPostHandler.mjs +1 -1
- package/src/middleware/govcyPageHandler.mjs +4 -1
- package/src/middleware/govcyUpload.mjs +1 -1
- package/src/public/js/govcyFiles.js +149 -20
- package/src/resources/govcyResources.mjs +34 -3
- package/src/utils/govcyApiDetection.mjs +1 -1
- package/src/utils/govcyConstants.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +8 -0
- package/src/utils/govcyFormHandling.mjs +77 -5
- package/src/utils/govcyHandleFiles.mjs +20 -0
- package/src/utils/govcySubmitData.mjs +27 -0
- package/src/utils/govcyValidator.mjs +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
|
|
5
5
|
"author": "DMRID - DSF Team",
|
|
6
6
|
"license": "MIT",
|
package/src/index.mjs
CHANGED
|
@@ -129,6 +129,14 @@ export default function initializeGovCyExpressService(){
|
|
|
129
129
|
|
|
130
130
|
// 📝 -- ROUTE: Serve manifest.json dynamically for each site
|
|
131
131
|
app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
|
|
132
|
+
|
|
133
|
+
// 🗃️ -- ROUTE: Handle POST requests for file uploads for a page.
|
|
134
|
+
app.post('/apis/:siteId/:pageUrl/upload',
|
|
135
|
+
serviceConfigDataMiddleware,
|
|
136
|
+
requireAuth, // UNCOMMENT
|
|
137
|
+
naturalPersonPolicy, // UNCOMMENT
|
|
138
|
+
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
139
|
+
govcyUploadMiddleware);
|
|
132
140
|
|
|
133
141
|
// 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
|
|
134
142
|
app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
|
|
@@ -148,14 +156,6 @@ export default function initializeGovCyExpressService(){
|
|
|
148
156
|
// 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
|
|
149
157
|
app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
|
|
150
158
|
|
|
151
|
-
// 🗃️ -- ROUTE: Handle POST requests for file uploads for a page.
|
|
152
|
-
app.post('/:siteId/:pageUrl/upload',
|
|
153
|
-
serviceConfigDataMiddleware,
|
|
154
|
-
requireAuth, // UNCOMMENT
|
|
155
|
-
naturalPersonPolicy, // UNCOMMENT
|
|
156
|
-
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
157
|
-
govcyUploadMiddleware);
|
|
158
|
-
|
|
159
159
|
// 👀📥 -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
|
|
160
160
|
app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
|
|
161
161
|
|
|
@@ -43,7 +43,7 @@ export function govcyFormsPostHandler() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// const formData = req.body; // Submitted data
|
|
46
|
-
const formData = getFormData(formElement.params.elements, req.body); // Submitted data
|
|
46
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
47
47
|
|
|
48
48
|
// ☑️ Start validation from top-level form elements
|
|
49
49
|
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
@@ -53,6 +53,9 @@ export function govcyPageHandler() {
|
|
|
53
53
|
element.params.method = "POST";
|
|
54
54
|
// ➕ Add CSRF token
|
|
55
55
|
element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
|
|
56
|
+
// // ➕ Add siteId and pageUrl to form data
|
|
57
|
+
// element.params.elements.push(govcyResources.siteAndPageInput(siteId, pageUrl, req.globalLang));
|
|
58
|
+
// ➕ Add govcyFormsJs script to the form
|
|
56
59
|
element.params.elements.push(govcyResources.staticResources.elements["govcyFormsJs"]);
|
|
57
60
|
|
|
58
61
|
// 🔍 Find the first button with `prototypeNavigate`
|
|
@@ -88,7 +91,7 @@ export function govcyPageHandler() {
|
|
|
88
91
|
}
|
|
89
92
|
//--------- End of Handle Validation Errors ---------
|
|
90
93
|
|
|
91
|
-
populateFormData(element.params.elements, theData,validationErrors);
|
|
94
|
+
populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang);
|
|
92
95
|
// if there are validation errors, add an error summary
|
|
93
96
|
if (validationErrors?.errorSummary?.length > 0) {
|
|
94
97
|
element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
@@ -28,7 +28,7 @@ export const govcyUploadMiddleware = [
|
|
|
28
28
|
|
|
29
29
|
if (result.status !== 200) {
|
|
30
30
|
logger.error("Upload failed", result);
|
|
31
|
-
return res.status(result.status).json(errorResponse(result.
|
|
31
|
+
return res.status(result.status).json(errorResponse(result.dataStatus, result.errorMessage || 'File upload failed'));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
return res.json(successResponse(result.data));
|
|
@@ -1,46 +1,175 @@
|
|
|
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
|
-
//
|
|
5
|
-
|
|
4
|
+
// 🔁 Loop over each file input and attach a change event listener
|
|
5
|
+
fileInputs.forEach(function(input) {
|
|
6
|
+
input.addEventListener('change', _uploadFileEventHandler);
|
|
7
|
+
});
|
|
6
8
|
|
|
7
|
-
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
8
|
-
const siteId = 'test';
|
|
9
|
-
const pageUrl = 'data-entry-all';
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Handles the upload of a file event
|
|
12
|
+
*
|
|
13
|
+
* @param {object} event The event
|
|
14
|
+
*/
|
|
15
|
+
async function _uploadFileEventHandler(event) {
|
|
16
|
+
var input = event.target;
|
|
17
|
+
var messages = {
|
|
18
|
+
"uploadSuccesful": {
|
|
19
|
+
"el": "Το αρχείο ανεβαστηκε",
|
|
20
|
+
"en": "File uploaded successfully",
|
|
21
|
+
"tr": "File uploaded successfully"
|
|
22
|
+
},
|
|
23
|
+
"uploadFailed": {
|
|
24
|
+
"el": "Αποτυχια ανεβασης",
|
|
25
|
+
"en": "File upload failed",
|
|
26
|
+
"tr": "File upload failed"
|
|
27
|
+
},
|
|
28
|
+
"uploadFailed406": {
|
|
29
|
+
"el": "Το επιλεγμένο αρχείο είναι κενό",
|
|
30
|
+
"en": "The selected file is empty",
|
|
31
|
+
"tr": "The selected file is empty"
|
|
32
|
+
},
|
|
33
|
+
"uploadFailed407": {
|
|
34
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
35
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
36
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
37
|
+
},
|
|
38
|
+
"uploadFailed408": {
|
|
39
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
40
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
41
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
42
|
+
},
|
|
43
|
+
"uploadFailed409": {
|
|
44
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 5MB",
|
|
45
|
+
"en": "The selected file must be smaller than 5MB",
|
|
46
|
+
"tr": "The selected file must be smaller than 5MB"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
|
|
50
|
+
var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
|
|
51
|
+
var csrfToken = csrfEl ? csrfEl.value : '';
|
|
52
|
+
|
|
53
|
+
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
54
|
+
var siteId = window._govcySiteId || "";
|
|
55
|
+
var pageUrl = window._govcyPageUrl || "";
|
|
56
|
+
var lang = window._govcyLang || "el";
|
|
14
57
|
// 📦 Grab the selected file
|
|
15
|
-
|
|
16
|
-
|
|
58
|
+
var file = event.target.files[0];
|
|
59
|
+
var elementName = input.name; // Form field's `name` attribute
|
|
60
|
+
var elementId = input.id; // Form field's `id` attribute
|
|
17
61
|
|
|
18
62
|
if (!file) return; // Exit if no file was selected
|
|
19
63
|
|
|
20
64
|
// 🧵 Prepare form-data payload for the API
|
|
21
|
-
|
|
65
|
+
var formData = new FormData();
|
|
22
66
|
formData.append('file', file); // Attach the actual file
|
|
23
67
|
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
24
68
|
|
|
25
69
|
try {
|
|
26
70
|
// 🚀 Send file to the backend upload API
|
|
27
|
-
|
|
71
|
+
var response = await axios.post(`/apis/${siteId}/${pageUrl}/upload`, formData, {
|
|
28
72
|
headers: {
|
|
29
73
|
'X-CSRF-Token': csrfToken // 🔐 Pass CSRF token in custom header
|
|
30
74
|
}
|
|
31
75
|
});
|
|
32
76
|
|
|
33
|
-
|
|
77
|
+
var sha256 = response.data.Data.sha256;
|
|
78
|
+
var fileId = response.data.Data.fileId;
|
|
34
79
|
|
|
35
80
|
// 📝 Store returned metadata in hidden fields for submission with the form
|
|
36
|
-
document.querySelector(
|
|
37
|
-
document.querySelector(
|
|
81
|
+
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
|
|
82
|
+
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
|
|
38
83
|
|
|
39
|
-
|
|
84
|
+
// Render the file view
|
|
85
|
+
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
|
|
86
|
+
|
|
87
|
+
// Accessibility: Update ARIA live region with success message
|
|
88
|
+
var statusRegion = document.getElementById('_govcy-upload-status');
|
|
89
|
+
if (statusRegion) {
|
|
90
|
+
setTimeout(function() {
|
|
91
|
+
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
92
|
+
}, 200)
|
|
93
|
+
setTimeout(function() {
|
|
94
|
+
statusRegion.textContent = '';
|
|
95
|
+
}, 5000);
|
|
96
|
+
}
|
|
97
|
+
// alert('✅ File uploaded successfully');
|
|
40
98
|
|
|
41
99
|
} catch (err) {
|
|
42
100
|
// ⚠️ Show an error message if upload fails
|
|
43
|
-
|
|
101
|
+
var errorMessage = messages.uploadFailed;
|
|
102
|
+
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
|
|
103
|
+
|
|
104
|
+
if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
|
|
105
|
+
errorMessage = messages["uploadFailed" + errorCode];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Render the file input with error
|
|
109
|
+
_renderFileElement("fileInput", elementId, elementName, "","", errorMessage);
|
|
110
|
+
|
|
111
|
+
// Re-bind the file input's change handler
|
|
112
|
+
var newInput = document.getElementById(elementId);
|
|
113
|
+
if (newInput) {
|
|
114
|
+
newInput.addEventListener('change', _uploadFileEventHandler);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Accessibility: Focus on the form field
|
|
118
|
+
document.getElementById(elementId)?.focus();
|
|
119
|
+
|
|
44
120
|
}
|
|
45
|
-
|
|
46
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Renders a file element in the DOM
|
|
125
|
+
*
|
|
126
|
+
* @param {string} elementState The element state. Can be "fileInput" or "fileView"
|
|
127
|
+
* @param {string} elementId The element id
|
|
128
|
+
* @param {string} elementName The element name
|
|
129
|
+
* @param {string} fileId The file id
|
|
130
|
+
* @param {string} sha256 The sha256
|
|
131
|
+
* @param {object} errorMessage The error message in all supported languages
|
|
132
|
+
*/
|
|
133
|
+
function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
|
|
134
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
135
|
+
var renderer = new GovcyFrontendRendererBrowser();
|
|
136
|
+
var lang = window._govcyLang || "el";
|
|
137
|
+
// Define the input data
|
|
138
|
+
var inputData =
|
|
139
|
+
{
|
|
140
|
+
"site": {
|
|
141
|
+
"lang": lang
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var fileInputMap = window._govcyFileInputs || {};
|
|
145
|
+
var fileElement = fileInputMap[elementName];
|
|
146
|
+
fileElement.element = elementState;
|
|
147
|
+
if (errorMessage != null) fileElement.params.error = errorMessage;
|
|
148
|
+
if (fileId != null) fileElement.params.fileId = fileId;
|
|
149
|
+
if (sha256 != null) fileElement.params.sha256 = sha256;
|
|
150
|
+
if (elementState == "fileView") {
|
|
151
|
+
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
152
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
153
|
+
fileElement.params.viewHref = "#viewHref";
|
|
154
|
+
fileElement.params.deleteHref = "#deleteHref";
|
|
155
|
+
}
|
|
156
|
+
// Construct the JSONTemplate
|
|
157
|
+
var JSONTemplate = {
|
|
158
|
+
"elements": [fileElement]
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
//render HTML into string
|
|
162
|
+
var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
163
|
+
var outerElement = document.getElementById(`${elementId}-outer-control`)
|
|
164
|
+
|| document.getElementById(`${elementId}-input-control`)
|
|
165
|
+
|| document.getElementById(`${elementId}-view-control`);
|
|
166
|
+
|
|
167
|
+
if (outerElement) {
|
|
168
|
+
//remove all classes from outerElement
|
|
169
|
+
outerElement.className = "";
|
|
170
|
+
//set the id of the outerElement to `${elementId}-outer-control`
|
|
171
|
+
outerElement.id = `${elementId}-outer-control`;
|
|
172
|
+
//update DOM and initialize the JS components
|
|
173
|
+
renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -100,6 +100,16 @@ export const staticResources = {
|
|
|
100
100
|
en: "We have received your request. ",
|
|
101
101
|
el: "Έχουμε λάβει την αίτησή σας. ",
|
|
102
102
|
tr: "We have received your request. "
|
|
103
|
+
},
|
|
104
|
+
fileUploaded : {
|
|
105
|
+
en: "File has been uploaded. ",
|
|
106
|
+
el: "Το αρχείο ανεβάστηκε. ",
|
|
107
|
+
tr: "File has been uploaded. "
|
|
108
|
+
},
|
|
109
|
+
fileNotUploaded : {
|
|
110
|
+
en: "File has not been uploaded. ",
|
|
111
|
+
el: "Το αρχείο δεν ανεβάστηκε. ",
|
|
112
|
+
tr: "File has not been uploaded. "
|
|
103
113
|
}
|
|
104
114
|
},
|
|
105
115
|
//remderer sections
|
|
@@ -113,9 +123,9 @@ export const staticResources = {
|
|
|
113
123
|
element: "htmlElement",
|
|
114
124
|
params: {
|
|
115
125
|
text: {
|
|
116
|
-
en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
117
|
-
el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
118
|
-
tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`
|
|
126
|
+
en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
127
|
+
el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
128
|
+
tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
},
|
|
@@ -192,6 +202,27 @@ export function csrfTokenInput(csrfToken) {
|
|
|
192
202
|
};
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Get the site and page input elements
|
|
207
|
+
* @param {string} siteId The site id
|
|
208
|
+
* @param {string} pageUrl The page url
|
|
209
|
+
* @param {string} lang The page language
|
|
210
|
+
* @returns {object} htmlElement with the site and page inputs
|
|
211
|
+
*/
|
|
212
|
+
export function siteAndPageInput(siteId, pageUrl, lang = "el") {
|
|
213
|
+
const siteAndPageInputs = `<input type="hidden" name="_siteId" value="${siteId}"><input type="hidden" name="_pageUrl" value="${pageUrl}"><input type="hidden" name="_lang" value="${lang}">`;
|
|
214
|
+
return {
|
|
215
|
+
element: "htmlElement",
|
|
216
|
+
params: {
|
|
217
|
+
text: {
|
|
218
|
+
en: siteAndPageInputs,
|
|
219
|
+
el: siteAndPageInputs,
|
|
220
|
+
tr: siteAndPageInputs
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
/**
|
|
196
227
|
* Error page template
|
|
197
228
|
* @param {object} title the title text element
|
|
@@ -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 =
|
|
13
|
+
const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+\/(upload|download)$/;
|
|
14
14
|
const isStructuredApiUrl = apiUrlPattern.test(req.originalUrl || req.url);
|
|
15
15
|
|
|
16
16
|
return acceptJson || isStructuredApiUrl;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared constants for allowed form elements.
|
|
3
3
|
*/
|
|
4
|
-
export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput"];
|
|
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
7
|
export const ALLOWED_FILE_SIZE_MB = 5; // Maximum file size in MB
|
|
@@ -98,6 +98,14 @@ export function storePageData(store, siteId, pageUrl, formData) {
|
|
|
98
98
|
|
|
99
99
|
store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
|
|
103
|
+
// Ensure session structure is initialized
|
|
104
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
105
|
+
|
|
106
|
+
// Store the element value
|
|
107
|
+
store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
|
|
108
|
+
}
|
|
101
109
|
/**
|
|
102
110
|
* Stores the page's input data in the data layer
|
|
103
111
|
* *
|
|
@@ -5,16 +5,25 @@
|
|
|
5
5
|
* and show error summary when there are validation errors.
|
|
6
6
|
*/
|
|
7
7
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
8
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Helper function to populate form data with session data
|
|
12
13
|
* @param {Array} formElements The form elements
|
|
13
14
|
* @param {*} theData The data either from session or request
|
|
15
|
+
* @param {Object} validationErrors The validation errors
|
|
16
|
+
* @param {Object} store The session store
|
|
17
|
+
* @param {string} siteId The site ID
|
|
18
|
+
* @param {string} pageUrl The page URL
|
|
19
|
+
* @param {string} lang The language
|
|
14
20
|
*/
|
|
15
|
-
export function populateFormData(formElements, theData, validationErrors) {
|
|
21
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null) {
|
|
16
22
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
17
|
-
|
|
23
|
+
const isRootCall = !fileInputElements;
|
|
24
|
+
if (isRootCall) {
|
|
25
|
+
fileInputElements = {};
|
|
26
|
+
}
|
|
18
27
|
// Recursively populate form data with session data
|
|
19
28
|
formElements.forEach(element => {
|
|
20
29
|
if (inputElements.includes(element.element)) {
|
|
@@ -53,6 +62,26 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
53
62
|
// Invalid format (not matching D/M/YYYY or DD/MM/YYYY)
|
|
54
63
|
element.params.value = "";
|
|
55
64
|
}
|
|
65
|
+
} else if (element.element === "fileInput") {
|
|
66
|
+
// For fileInput, we change the element.element to "fileView" and set the
|
|
67
|
+
// fileId and sha256 from the session store
|
|
68
|
+
// unneeded handle of `Attachment` at the end
|
|
69
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
|
|
70
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
|
|
71
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
72
|
+
if (fileData) {
|
|
73
|
+
element.element = "fileView";
|
|
74
|
+
element.params.fileId = fileData.fileId;
|
|
75
|
+
element.params.sha256 = fileData.sha256;
|
|
76
|
+
element.params.visuallyHiddenText = element.params.label;
|
|
77
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
78
|
+
element.params.viewHref = "#viewHref";
|
|
79
|
+
element.params.deleteHref = "#deleteHref";
|
|
80
|
+
} else {
|
|
81
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
82
|
+
element.params.value = "";
|
|
83
|
+
}
|
|
84
|
+
fileInputElements[fieldName] = element;
|
|
56
85
|
// Handle all other input elements (textInput, checkboxes, radios, etc.)
|
|
57
86
|
} else {
|
|
58
87
|
element.params.value = theData[fieldName] || "";
|
|
@@ -78,7 +107,7 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
78
107
|
if (element.element === "radios" && element.params.items) {
|
|
79
108
|
element.params.items.forEach(item => {
|
|
80
109
|
if (item.conditionalElements) {
|
|
81
|
-
populateFormData(item.conditionalElements, theData, validationErrors);
|
|
110
|
+
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements);
|
|
82
111
|
|
|
83
112
|
// Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
|
|
84
113
|
if (item.conditionalElements.some(condEl => condEl.params?.error)) {
|
|
@@ -88,6 +117,29 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
88
117
|
});
|
|
89
118
|
}
|
|
90
119
|
});
|
|
120
|
+
// add file input elements's definition in js object
|
|
121
|
+
if (isRootCall && Object.keys(fileInputElements).length > 0) {
|
|
122
|
+
const scriptTag = `
|
|
123
|
+
<script type="text/javascript">
|
|
124
|
+
window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
|
|
125
|
+
window._govcySiteId = "${siteId}";
|
|
126
|
+
window._govcyPageUrl = "${pageUrl}";
|
|
127
|
+
window._govcyLang = "${lang}";
|
|
128
|
+
</script>
|
|
129
|
+
<div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="assertive"></div>
|
|
130
|
+
<div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
|
|
131
|
+
`.trim();
|
|
132
|
+
formElements.push({
|
|
133
|
+
element: 'htmlElement',
|
|
134
|
+
params: {
|
|
135
|
+
text: {
|
|
136
|
+
en: scriptTag,
|
|
137
|
+
el: scriptTag,
|
|
138
|
+
tr: scriptTag
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
91
143
|
}
|
|
92
144
|
|
|
93
145
|
|
|
@@ -96,9 +148,12 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
96
148
|
*
|
|
97
149
|
* @param {Array} elements - The form elements (including conditional ones).
|
|
98
150
|
* @param {Object} formData - The submitted form data.
|
|
151
|
+
* @param {Object} store - The session store .
|
|
152
|
+
* @param {string} siteId - The site ID .
|
|
153
|
+
* @param {string} pageUrl - The page URL .
|
|
99
154
|
* @returns {Object} filteredData - The filtered form data.
|
|
100
155
|
*/
|
|
101
|
-
export function getFormData(elements, formData) {
|
|
156
|
+
export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
|
|
102
157
|
const filteredData = {};
|
|
103
158
|
elements.forEach(element => {
|
|
104
159
|
const { name } = element.params || {};
|
|
@@ -115,7 +170,7 @@ export function getFormData(elements, formData) {
|
|
|
115
170
|
if (item.conditionalElements) {
|
|
116
171
|
Object.assign(
|
|
117
172
|
filteredData,
|
|
118
|
-
getFormData(item.conditionalElements, formData)
|
|
173
|
+
getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
|
|
119
174
|
);
|
|
120
175
|
}
|
|
121
176
|
});
|
|
@@ -130,6 +185,23 @@ export function getFormData(elements, formData) {
|
|
|
130
185
|
filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
|
|
131
186
|
filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
|
|
132
187
|
filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
|
|
188
|
+
// handle fileInput
|
|
189
|
+
} else if (element.element === "fileInput") {
|
|
190
|
+
// fileInput elements are already stored in the store when it was uploaded
|
|
191
|
+
// so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
|
|
192
|
+
// unneeded handle of `Attachment` at the end
|
|
193
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
|
|
194
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
|
|
195
|
+
if (fileData) {
|
|
196
|
+
// unneeded handle of `Attachment` at the end
|
|
197
|
+
// filteredData[name + "Attachment"] = fileData;
|
|
198
|
+
filteredData[name] = fileData;
|
|
199
|
+
} else {
|
|
200
|
+
//TODO: Ask Andreas how to handle empty file inputs
|
|
201
|
+
// unneeded handle of `Attachment` at the end
|
|
202
|
+
// filteredData[name + "Attachment"] = ""; // or handle as needed
|
|
203
|
+
filteredData[name] = ""; // or handle as needed
|
|
204
|
+
}
|
|
133
205
|
// Handle other elements (e.g., textInput, textArea, datePicker)
|
|
134
206
|
} else {
|
|
135
207
|
filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
|
|
@@ -27,6 +27,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
27
27
|
if (!file || !elementName) {
|
|
28
28
|
return {
|
|
29
29
|
status: 400,
|
|
30
|
+
dataStatus: 400,
|
|
30
31
|
errorMessage: 'Missing file or element name'
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -37,6 +38,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
37
38
|
if (!uploadCfg?.url || !uploadCfg?.clientKey || !uploadCfg?.serviceId) {
|
|
38
39
|
return {
|
|
39
40
|
status: 400,
|
|
41
|
+
dataStatus: 401,
|
|
40
42
|
errorMessage: 'Missing upload configuration'
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -53,6 +55,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
53
55
|
if (!url || !clientKey) {
|
|
54
56
|
return {
|
|
55
57
|
status: 400,
|
|
58
|
+
dataStatus: 402,
|
|
56
59
|
errorMessage: 'Missing environment variables for upload'
|
|
57
60
|
};
|
|
58
61
|
}
|
|
@@ -67,6 +70,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
67
70
|
if (!page?.pageTemplate) {
|
|
68
71
|
return {
|
|
69
72
|
status: 400,
|
|
73
|
+
dataStatus: 403,
|
|
70
74
|
errorMessage: 'Invalid page configuration'
|
|
71
75
|
};
|
|
72
76
|
}
|
|
@@ -77,6 +81,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
77
81
|
if (conditionResult.result === false) {
|
|
78
82
|
return {
|
|
79
83
|
status: 403,
|
|
84
|
+
dataStatus: 404,
|
|
80
85
|
errorMessage: 'This page is skipped by conditional logic'
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -88,6 +93,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
88
93
|
if (!isAllowed) {
|
|
89
94
|
return {
|
|
90
95
|
status: 403,
|
|
96
|
+
dataStatus: 405,
|
|
91
97
|
errorMessage: `File input [${elementName}] not allowed on this page`
|
|
92
98
|
};
|
|
93
99
|
}
|
|
@@ -96,6 +102,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
96
102
|
if (file.size === 0) {
|
|
97
103
|
return {
|
|
98
104
|
status: 400,
|
|
105
|
+
dataStatus: 406,
|
|
99
106
|
errorMessage: 'Uploaded file is empty'
|
|
100
107
|
};
|
|
101
108
|
}
|
|
@@ -105,6 +112,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
105
112
|
if (!ALLOWED_FILE_MIME_TYPES.includes(file.mimetype)) {
|
|
106
113
|
return {
|
|
107
114
|
status: 400,
|
|
115
|
+
dataStatus: 407,
|
|
108
116
|
errorMessage: 'Invalid file type (MIME not allowed)'
|
|
109
117
|
};
|
|
110
118
|
}
|
|
@@ -113,6 +121,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
113
121
|
if (!isMagicByteValid(file.buffer, file.mimetype)) {
|
|
114
122
|
return {
|
|
115
123
|
status: 400,
|
|
124
|
+
dataStatus: 408,
|
|
116
125
|
errorMessage: 'Invalid file type (magic byte mismatch)'
|
|
117
126
|
};
|
|
118
127
|
}
|
|
@@ -121,6 +130,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
121
130
|
if (file.size > ALLOWED_FILE_SIZE_MB * 1024 * 1024) {
|
|
122
131
|
return {
|
|
123
132
|
status: 400,
|
|
133
|
+
dataStatus: 409,
|
|
124
134
|
errorMessage: 'File exceeds allowed size'
|
|
125
135
|
};
|
|
126
136
|
}
|
|
@@ -161,6 +171,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
161
171
|
if (!response?.Succeeded) {
|
|
162
172
|
return {
|
|
163
173
|
status: 500,
|
|
174
|
+
dataStatus: 410,
|
|
164
175
|
errorMessage: `${response?.ErrorCode} - ${response?.ErrorMessage} - fileUploadAPIEndpoint returned succeeded false`
|
|
165
176
|
};
|
|
166
177
|
}
|
|
@@ -169,11 +180,19 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
169
180
|
if (!response?.Data?.fileId || !response?.Data?.sha256) {
|
|
170
181
|
return {
|
|
171
182
|
status: 500,
|
|
183
|
+
dataStatus: 411,
|
|
172
184
|
errorMessage: 'Missing fileId or sha256 in response'
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
// ✅ Success
|
|
189
|
+
// 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, {
|
|
193
|
+
sha256: response.Data.sha256,
|
|
194
|
+
fileId: response.Data.fileId,
|
|
195
|
+
});
|
|
177
196
|
logger.debug("File upload successful", response.Data);
|
|
178
197
|
logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
|
|
179
198
|
return {
|
|
@@ -190,6 +209,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
190
209
|
} catch (err) {
|
|
191
210
|
return {
|
|
192
211
|
status: 500,
|
|
212
|
+
dataStatus: 500,
|
|
193
213
|
errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
|
|
194
214
|
};
|
|
195
215
|
}
|
|
@@ -66,6 +66,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
66
66
|
// Store in submissionData
|
|
67
67
|
submissionData[pageUrl].formData[elId] = value;
|
|
68
68
|
|
|
69
|
+
// handle fileInput
|
|
70
|
+
if (elType === "fileInput") {
|
|
71
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
72
|
+
submissionData[pageUrl].formData[elId + "Attachment"] = value;
|
|
73
|
+
delete submissionData[pageUrl].formData[elId];
|
|
74
|
+
}
|
|
69
75
|
|
|
70
76
|
// 🔄 If radios with conditionalElements, walk ALL options
|
|
71
77
|
if (elType === "radios" && Array.isArray(element.params?.items)) {
|
|
@@ -85,6 +91,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
85
91
|
|
|
86
92
|
// Store even if the field was not visible to user
|
|
87
93
|
submissionData[pageUrl].formData[condId] = condValue;
|
|
94
|
+
// handle fileInput
|
|
95
|
+
if (condType === "fileInput") {
|
|
96
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
97
|
+
submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
|
|
98
|
+
delete submissionData[pageUrl].formData[condId];
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
}
|
|
@@ -291,6 +303,10 @@ function getValue(formElement, pageUrl, req, siteId) {
|
|
|
291
303
|
let value = ""
|
|
292
304
|
if (formElement.element === "dateInput") {
|
|
293
305
|
value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
|
|
306
|
+
} else if (formElement.element === "fileInput") {
|
|
307
|
+
// unneeded handle of `Attachment` at the end
|
|
308
|
+
// value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
|
|
309
|
+
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
294
310
|
} else {
|
|
295
311
|
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
296
312
|
}
|
|
@@ -340,6 +356,17 @@ function getValueLabel(formElement, value, pageUrl, req, siteId, service) {
|
|
|
340
356
|
return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
|
|
341
357
|
}
|
|
342
358
|
|
|
359
|
+
// handle fileInput
|
|
360
|
+
if (formElement.element === "fileInput") {
|
|
361
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
362
|
+
if (value) {
|
|
363
|
+
return govcyResources.staticResources.text.fileUploaded;
|
|
364
|
+
} else {
|
|
365
|
+
return govcyResources.getSameMultilingualObject(service.site.languages, "");
|
|
366
|
+
// return govcyResources.staticResources.text.fileNotUploaded;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
// textInput, textArea, etc.
|
|
344
371
|
return govcyResources.getSameMultilingualObject(service.site.languages, value);
|
|
345
372
|
}
|
|
@@ -263,6 +263,9 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
263
263
|
formData[`${field.params.name}_day`]]
|
|
264
264
|
.filter(Boolean) // Remove empty values
|
|
265
265
|
.join("-") // Join remaining parts
|
|
266
|
+
// unneeded handle of `Attachment` at the end
|
|
267
|
+
// : (field.element === "fileInput") // Handle fileInput
|
|
268
|
+
// ? formData[`${field.params.name}Attachment`] || ""
|
|
266
269
|
: formData[field.params.name] || ""; // Get submitted value
|
|
267
270
|
|
|
268
271
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
|
|
@@ -310,6 +313,10 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
310
313
|
formData[`${conditionalElement.params.name}_day`]]
|
|
311
314
|
.filter(Boolean) // Remove empty values
|
|
312
315
|
.join("-") // Join remaining parts
|
|
316
|
+
: (conditionalElement.element === "fileInput") // Handle fileInput
|
|
317
|
+
// unneeded handle of `Attachment` at the end
|
|
318
|
+
// ? formData[`${conditionalElement.params.name}Attachment`] || ""
|
|
319
|
+
? formData[`${conditionalElement.params.name}`] || ""
|
|
313
320
|
: formData[conditionalElement.params.name] || ""; // Get submitted value
|
|
314
321
|
|
|
315
322
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
|