@gov-cy/govcy-express-services 1.0.0-alpha.7 → 1.0.0-alpha.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
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
@@ -25,6 +25,7 @@ import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
25
25
  import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
26
26
  import { govcyLoadSubmissionData } from './middleware/govcyLoadSubmissionData.mjs';
27
27
  import { govcyUploadMiddleware } from './middleware/govcyUpload.mjs';
28
+ import { govcyDeleteFilePageHandler, govcyDeleteFilePostHandler } from './middleware/govcyDeleteFileHandler.mjs';
28
29
  import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
29
30
  import { logger } from "./utils/govcyLogger.mjs";
30
31
 
@@ -150,9 +151,15 @@ export default function initializeGovCyExpressService(){
150
151
  // ✅ -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
151
152
  app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
152
153
 
154
+ // ❌🗃️ -- ROUTE: Delete file (BEFORE the dynamic route)
155
+ app.get('/:siteId/:pageUrl/:elementName/delete-file', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyDeleteFilePageHandler(), renderGovcyPage());
156
+
153
157
  // 📝 -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
154
158
  app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
155
159
 
160
+ // ❌🗃️📥 -- ROUTE: Handle POST requests for delete file
161
+ app.post('/:siteId/:pageUrl/:elementName/delete-file', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyDeleteFilePostHandler());
162
+
156
163
  // 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
157
164
  app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
158
165
 
@@ -0,0 +1,234 @@
1
+ import * as govcyResources from "../resources/govcyResources.mjs";
2
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
3
+ import { logger } from "../utils/govcyLogger.mjs";
4
+ import { pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
5
+ import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
6
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
7
+ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
8
+ import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
9
+ import { URL } from "url";
10
+
11
+
12
+ /**
13
+ * Middleware to handle the delete file page.
14
+ * This middleware processes the delete file page, populates the question, and shows validation errors.
15
+ */
16
+ export function govcyDeleteFilePageHandler() {
17
+ return (req, res, next) => {
18
+ try {
19
+ const { siteId, pageUrl, elementName } = req.params;
20
+
21
+ // Create a deep copy of the service to avoid modifying the original
22
+ let serviceCopy = req.serviceData;
23
+
24
+ // ⤵️ Find the current page based on the URL
25
+ const page = getPageConfigData(serviceCopy, pageUrl);
26
+
27
+ // deep copy the page template to avoid modifying the original
28
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
29
+
30
+ // ----- Conditional logic comes here
31
+ // ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
32
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
33
+ if (conditionResult.result === false) {
34
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
35
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
36
+ }
37
+
38
+ // Validate the field: Only allow upload if the page contains a fileInput with the given name
39
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
40
+ if (!fileInputElement) {
41
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
42
+ }
43
+
44
+ // Validate if the file input has a label
45
+ if (!fileInputElement?.params?.label) {
46
+ return handleMiddlewareError(`File input [${elementName}] does not have a label`, 404, next);
47
+ }
48
+
49
+ //get element data
50
+ const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
51
+
52
+ // If the element data is not found, return an error response
53
+ if (!elementData) {
54
+ return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
55
+ }
56
+
57
+ // Deep copy page title (so we don’t mutate template)
58
+ const pageTitle = JSON.parse(JSON.stringify(govcyResources.staticResources.text.deleteFileTitle));
59
+
60
+ // Replace label placeholders on page title
61
+ for (const lang of Object.keys(pageTitle)) {
62
+ const labelForLang = fileInputElement.params.label[lang] || fileInputElement.params.label.el || "";
63
+ pageTitle[lang] = pageTitle[lang].replace("{{file}}", labelForLang);
64
+ }
65
+
66
+
67
+ // Deep copy renderer pageData from
68
+ let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
69
+
70
+ // Handle isTesting
71
+ pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
72
+
73
+ // Base page template structure
74
+ let pageTemplate = {
75
+ sections: [
76
+ {
77
+ name: "beforeMain",
78
+ elements: [govcyResources.staticResources.elements.backLink]
79
+ }
80
+ ]
81
+ };
82
+
83
+ // Construct page title
84
+ const pageRadios = {
85
+ element: "radios",
86
+ params: {
87
+ id: "deleteFile",
88
+ name: "deleteFile",
89
+ legend: pageTitle,
90
+ isPageHeading: true,
91
+ classes: "govcy-mb-6",
92
+ items:[
93
+ {
94
+ value: "yes",
95
+ text: govcyResources.staticResources.text.deleteYesOption
96
+ },
97
+ {
98
+ value: "no",
99
+ text: govcyResources.staticResources.text.deleteNoOption
100
+ }
101
+ ]
102
+
103
+ }
104
+ };
105
+ //-------------
106
+
107
+ // Construct submit button
108
+ const formElement = {
109
+ element: "form",
110
+ params: {
111
+ action: govcyResources.constructPageUrl(siteId, `${pageUrl}/${elementName}/delete-file`,(req.query.route === "review" ? "review" : "")),
112
+ method: "POST",
113
+ elements: [
114
+ pageRadios,
115
+ {
116
+ element: "button",
117
+ params: {
118
+ type: "submit",
119
+ text: govcyResources.staticResources.text.continue
120
+ }
121
+ },
122
+ govcyResources.csrfTokenInput(req.csrfToken())
123
+ ]
124
+ }
125
+ };
126
+
127
+ // --------- Handle Validation Errors ---------
128
+ // Check if validation errors exist in the request
129
+ const validationErrors = [];
130
+ let mainElements = [];
131
+ if (req?.query?.hasError ) {
132
+ validationErrors.push({
133
+ link: '#deleteFile-option-1',
134
+ text: govcyResources.staticResources.text.deleteFileValidationError
135
+ });
136
+ mainElements.push(govcyResources.errorSummary(validationErrors));
137
+ formElement.params.elements[0].params.error = govcyResources.staticResources.text.deleteFileValidationError;
138
+ }
139
+ //--------- End Handle Validation Errors ---------
140
+
141
+ // Add elements to the main section, the H1, summary list, the submit button and the JS
142
+ mainElements.push(formElement, govcyResources.staticResources.elements["govcyFormsJs"]);
143
+ // Append generated summary list to the page template
144
+ pageTemplate.sections.push({ name: "main", elements: mainElements });
145
+
146
+ //if user is logged in add he user bane section in the page template
147
+ if (dataLayer.getUser(req.session)) {
148
+ pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
149
+ }
150
+
151
+ //prepare pageData
152
+ pageData.site = serviceCopy.site;
153
+ pageData.pageData.title = pageTitle;
154
+
155
+ // Attach processed page data to the request
156
+ req.processedPage = {
157
+ pageData: pageData,
158
+ pageTemplate: pageTemplate
159
+ };
160
+ logger.debug("Processed delete file page data:", req.processedPage);
161
+ next();
162
+ } catch (error) {
163
+ return next(error); // Pass error to govcyHttpErrorHandler
164
+ }
165
+ };
166
+ }
167
+
168
+
169
+ /**
170
+ * Middleware to handle delete file post form processing
171
+ * This middleware processes the post, validates the form and handles the file data layer
172
+ */
173
+ export function govcyDeleteFilePostHandler() {
174
+ return (req, res, next) => {
175
+ try {
176
+ // Extract siteId and pageUrl from request
177
+ let { siteId, pageUrl, elementName } = req.params;
178
+
179
+ // get service data
180
+ let serviceCopy = req.serviceData;
181
+
182
+ // 🔍 Find the page by pageUrl
183
+ const page = getPageConfigData(serviceCopy, pageUrl);
184
+
185
+ // Deep copy pageTemplate to avoid modifying the original
186
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
187
+
188
+ // ----- Conditional logic comes here
189
+ // Check if the page has conditions and apply logic
190
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
191
+ if (conditionResult.result === false) {
192
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
193
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
194
+ }
195
+
196
+
197
+ // the page base return url
198
+ const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${pageUrl}`;
199
+
200
+ //check if input `deleteFile` has a value
201
+ if (!req?.body?.deleteFile ||
202
+ (req.body.deleteFile !== "yes" && req.body.deleteFile !== "no")) {
203
+ logger.debug("⛔️ No deleteFile value provided on POST — skipping form save and redirecting:", req.body);
204
+ //construct the page url with error
205
+ let myUrl = new URL(pageBaseReturnUrl+`/${elementName}/delete-file`);
206
+ //check if the route is review
207
+ if (req.query.route === "review") {
208
+ myUrl.searchParams.set("route", "review");
209
+ }
210
+ //set the error flag
211
+ myUrl.searchParams.set("hasError", "1");
212
+
213
+ //redirect to the same page with error summary (relative path)
214
+ return res.redirect(govcyResources.constructErrorSummaryUrl(myUrl.pathname + myUrl.search));
215
+ }
216
+
217
+ //if no validation errors
218
+ if (req.body.deleteFile === "yes") {
219
+ dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, "");
220
+ }
221
+ // construct the page url
222
+ let myUrl = new URL(pageBaseReturnUrl);
223
+ //check if the route is review
224
+ if (req.query.route === "review") {
225
+ myUrl.searchParams.set("route", "review");
226
+ }
227
+
228
+ // redirect to the page (relative path)
229
+ res.redirect(myUrl.pathname + myUrl.search);
230
+ } catch (error) {
231
+ return next(error); // Pass error to govcyHttpErrorHandler
232
+ }
233
+ };
234
+ }
@@ -91,7 +91,7 @@ export function govcyPageHandler() {
91
91
  }
92
92
  //--------- End of Handle Validation Errors ---------
93
93
 
94
- populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang);
94
+ populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang, null, req.query.route);
95
95
  // if there are validation errors, add an error summary
96
96
  if (validationErrors?.errorSummary?.length > 0) {
97
97
  element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
@@ -120,3 +120,4 @@ export function govcyPageHandler() {
120
120
  }
121
121
  };
122
122
  }
123
+
@@ -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.status, result.errorMessage || 'File upload failed'));
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,152 +1,196 @@
1
1
  // 🔍 Select all file inputs that have the .govcy-file-upload class
2
- const fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
2
+ var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
3
3
 
4
4
  // 🔁 Loop over each file input and attach a change event listener
5
- fileInputs.forEach(input => {
6
- input.addEventListener('change', async (event) => {
7
- const messages = {
8
- "uploadSuccesful": {
9
- "el": "Το αρχείο ανεβαστηκε",
10
- "en": "File uploaded successfully",
11
- "tr": "File uploaded successfully"
12
- },
13
- "uploadFailed": {
14
- "el": "Αποτυχια ανεβασης",
15
- "en": "File upload failed",
16
- "tr": "File upload failed"
17
- }
18
- };
19
- // 🔐 Get the CSRF token from a hidden input field (generated by your backend)
20
- const csrfToken = document.querySelector('input[type="hidden"][name="_csrf"]')?.value;
21
- // 🔧 Define siteId and pageUrl (you can dynamically extract these later)
22
- const siteId = window._govcySiteId || "";
23
- const pageUrl = window._govcyPageUrl || "";
24
- const lang = window._govcyLang || "el";
25
- // 📦 Grab the selected file
26
- const file = event.target.files[0];
27
- const elementName = input.name; // Form field's `name` attribute
28
-
29
- if (!file) return; // Exit if no file was selected
30
-
31
- // 🧵 Prepare form-data payload for the API
32
- const formData = new FormData();
33
- formData.append('file', file); // Attach the actual file
34
- formData.append('elementName', elementName); // Attach the field name for backend lookup
35
-
36
- try {
37
- // 🚀 Send file to the backend upload API
38
- const response = await axios.post(`/apis/${siteId}/${pageUrl}/upload`, formData, {
39
- headers: {
40
- 'X-CSRF-Token': csrfToken // 🔐 Pass CSRF token in custom header
41
- }
42
- });
43
-
44
- const { sha256, fileId } = response.data.Data;
45
-
46
- // 📝 Store returned metadata in hidden fields for submission with the form
47
- document.querySelector(`[name="${elementName}Attachment[fileId]"`).value = fileId;
48
- document.querySelector(`[name="${elementName}Attachment[sha256]"`).value = sha256;
49
-
50
- // Success
51
- // Create an instance of GovcyFrontendRendererBrowser
52
- const renderer = new GovcyFrontendRendererBrowser();
53
- // Define the input data
54
- const inputData =
55
- {
56
- "site": {
57
- "lang": lang
58
- }
59
- };
60
-
61
- const fileInputMap = window._govcyFileInputs || {};
62
- let fileElement = fileInputMap[elementName];
63
- fileElement.element = "fileView";
64
- fileElement.params.fileId = fileId;
65
- fileElement.params.sha256 = sha256;
66
- fileElement.params.visuallyHiddenText = fileElement.params.label;
67
- fileElement.params.error = null;
68
- // TODO: Also need to set the `view` and `download` URLs
69
- fileElement.params.viewHref = "#viewHref";
70
- fileElement.params.deleteHref = "#deleteHref";
71
- // Construct the JSONTemplate
72
- const JSONTemplate = {
73
- "elements": [fileElement]
74
- };
75
-
76
- //render HTML into string
77
- let renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
78
- // look for element with id `${elementName}-outer-control`
79
- // if not found look for element with id `${elementName}-input-control`
80
- // if not found look for element with id `${elementName}-view-control`
81
- var outerElement = document.getElementById(`${elementName}-outer-control`)
82
- || document.getElementById(`${elementName}-input-control`)
83
- || document.getElementById(`${elementName}-view-control`);
84
-
85
- if (outerElement) {
86
- //remove all classes from outerElement
87
- outerElement.className = "";
88
- //set the id of the outerElement to `${elementName}-outer-control`
89
- outerElement.id = `${elementName}-outer-control`;
90
- //update DOM and initialize the JS components
91
- renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
92
- }
93
- // ✅ Update ARIA live region with success message
94
- const statusRegion = document.getElementById('_govcy-upload-status');
95
- if (statusRegion) {
96
- statusRegion.textContent = messages.uploadSuccesful[lang];
97
- setTimeout(() => {
98
- statusRegion.textContent = '';
99
- }, 10000);
100
- }
101
- // alert('✅ File uploaded successfully');
102
-
103
- } catch (err) {
104
- // Create an instance of GovcyFrontendRendererBrowser
105
- const renderer = new GovcyFrontendRendererBrowser();
106
- const lang = window._govcyLang || "el";
107
- // Define the input data
108
- const inputData =
109
- {
110
- "site": {
111
- "lang": lang
112
- }
113
- };
114
- const fileInputMap = window._govcyFileInputs || {};
115
- let fileElement = fileInputMap[elementName];
116
- fileElement.element = "fileInput";
117
- fileElement.params.fileId = "";
118
- fileElement.params.sha256 = ""
119
- fileElement.params.error = messages.uploadFailed;
120
-
121
- // Construct the JSONTemplate
122
- const JSONTemplate = {
123
- "elements": [fileElement]
124
- };
125
- //render HTML into string
126
- let renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
127
- var outerElement = document.getElementById(`${elementName}-outer-control`)
128
- || document.getElementById(`${elementName}-input-control`)
129
- || document.getElementById(`${elementName}-view-control`);
130
-
131
- if (outerElement) {
132
- //remove all classes from outerElement
133
- outerElement.className = "";
134
- //set the id of the outerElement to `${elementName}-outer-control`
135
- outerElement.id = `${elementName}-outer-control`;
136
- //update DOM and initialize the JS components
137
- renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
138
- //TODO: Kamran need to figure a way to re register the DOM event on change
5
+ fileInputs.forEach(function(input) {
6
+ input.addEventListener('change', _uploadFileEventHandler);
7
+ });
8
+
9
+
10
+ /**
11
+ * Handles the upload of a file event
12
+ *
13
+ * @param {object} event The event
14
+ */
15
+ 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
+
50
+ // 🔐 Get the CSRF token from a hidden input field (generated by your backend)
51
+ var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
52
+ var csrfToken = csrfEl ? csrfEl.value : '';
53
+
54
+ // 🔧 Define siteId and pageUrl (you can dynamically extract these later)
55
+ var siteId = window._govcySiteId || "";
56
+ var pageUrl = window._govcyPageUrl || "";
57
+ var lang = window._govcyLang || "el";
58
+
59
+ // 📦 Grab the selected file
60
+ var file = event.target.files[0];
61
+ var elementName = input.name; // Form field's `name` attribute
62
+ var elementId = input.id; // Form field's `id` attribute
63
+
64
+ if (!file) return; // Exit if no file was selected
65
+
66
+ // 🧵 Prepare form-data payload for the API
67
+ var formData = new FormData();
68
+ formData.append('file', file); // Attach the actual file
69
+ formData.append('elementName', elementName); // Attach the field name for backend lookup
70
+
71
+ // 🚀 CHANGED: using fetch instead of axios.post
72
+ fetch(`/apis/${siteId}/${pageUrl}/upload`, {
73
+ method: "POST",
74
+ headers: {
75
+ "X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
76
+ },
77
+ body: formData
78
+ })
79
+ .then(function(response) {
80
+ // 🚀 CHANGED: fetch does not auto-throw on error codes check manually
81
+ if (!response.ok) {
82
+ return response.json().then(function(errData) {
83
+ throw { response: { data: errData } };
84
+ });
139
85
  }
140
- // ✅ Update ARIA live region with success message
141
- const statusRegion = document.getElementById('_govcy-upload-error');
86
+ return response.json();
87
+ })
88
+ .then(function(data) {
89
+ // ✅ Success response
90
+ var sha256 = data.Data.sha256;
91
+ var fileId = data.Data.fileId;
92
+
93
+ // 📝 Store returned metadata in hidden fields if needed
94
+ // document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
95
+ // document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
96
+
97
+ // Render the file view
98
+ _renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
99
+
100
+ // Accessibility: Update ARIA live region with success message
101
+ var statusRegion = document.getElementById('_govcy-upload-status');
142
102
  if (statusRegion) {
143
- statusRegion.textContent = messages.uploadFailed[lang];
144
- setTimeout(() => {
103
+ setTimeout(function() {
104
+ statusRegion.textContent = messages.uploadSuccesful[lang];
105
+ }, 200);
106
+ setTimeout(function() {
145
107
  statusRegion.textContent = '';
146
- }, 10000);
108
+ }, 5000);
109
+ }
110
+ })
111
+ .catch(function(err) {
112
+ // ⚠️ Show an error message if upload fails
113
+ var errorMessage = messages.uploadFailed;
114
+ var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
115
+
116
+ if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
117
+ errorMessage = messages["uploadFailed" + errorCode];
118
+ }
119
+
120
+ // Render the file input with error
121
+ _renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
122
+
123
+ // Re-bind the file input's change handler
124
+ var newInput = document.getElementById(elementId);
125
+ if (newInput) {
126
+ newInput.addEventListener('change', _uploadFileEventHandler);
147
127
  }
148
- // // ⚠️ Show an error message if upload fails
149
- // alert('❌ Upload failed: ' + (err.response?.data?.error || err.message));
128
+
129
+ // Accessibility: Focus on the form field
130
+ document.getElementById(elementId)?.focus();
131
+ });
132
+ }
133
+
134
+
135
+ /**
136
+ * Renders a file element in the DOM
137
+ *
138
+ * @param {string} elementState The element state. Can be "fileInput" or "fileView"
139
+ * @param {string} elementId The element id
140
+ * @param {string} elementName The element name
141
+ * @param {string} fileId The file id
142
+ * @param {string} sha256 The sha256
143
+ * @param {object} errorMessage The error message in all supported languages
144
+ */
145
+ function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
146
+
147
+ // Grab the query string part (?foo=bar&route=something)
148
+ var queryString = window.location.search;
149
+ // Parse it
150
+ var params = new URLSearchParams(queryString);
151
+ // Get the "route" value (null if not present)
152
+ var route = params.get("route");
153
+
154
+ // Create an instance of GovcyFrontendRendererBrowser
155
+ var renderer = new GovcyFrontendRendererBrowser();
156
+ var lang = window._govcyLang || "el";
157
+ // Define the input data
158
+ var inputData =
159
+ {
160
+ "site": {
161
+ "lang": lang
150
162
  }
151
- });
152
- });
163
+ };
164
+ var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
165
+ var fileElement = fileInputMap[elementName];
166
+ fileElement.element = elementState;
167
+ if (errorMessage != null) fileElement.params.error = errorMessage;
168
+ if (fileId != null) fileElement.params.fileId = fileId;
169
+ if (sha256 != null) fileElement.params.sha256 = sha256;
170
+ if (elementState == "fileView") {
171
+ fileElement.params.visuallyHiddenText = fileElement.params.label;
172
+ // TODO: Also need to set the `view` and `download` URLs
173
+ fileElement.params.viewHref = "#viewHref";
174
+ fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/" + elementName + "/delete-file"
175
+ + (route !== null ? "?route=" + encodeURIComponent(route) : "");
176
+ }
177
+ // Construct the JSONTemplate
178
+ var JSONTemplate = {
179
+ "elements": [fileElement]
180
+ };
181
+
182
+ //render HTML into string
183
+ var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
184
+ var outerElement = document.getElementById(`${elementId}-outer-control`)
185
+ || document.getElementById(`${elementId}-input-control`)
186
+ || document.getElementById(`${elementId}-view-control`);
187
+
188
+ if (outerElement) {
189
+ //remove all classes from outerElement
190
+ outerElement.className = "";
191
+ //set the id of the outerElement to `${elementId}-outer-control`
192
+ outerElement.id = `${elementId}-outer-control`;
193
+ //update DOM and initialize the JS components
194
+ renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
195
+ }
196
+ }
@@ -1,20 +1,31 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
2
  // --- Show conditionals for checked radios ---
3
- document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked').forEach(radio => {
4
- const targetId = radio.getAttribute('data-aria-controls');
5
- const targetElement = document.getElementById(targetId);
3
+ // CHANGED: NodeList.prototype.forEach is not in IE11 → use Array.prototype.forEach.call
4
+ // CHANGED: Arrow function → function
5
+ Array.prototype.forEach.call(
6
+ document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked'),
7
+ function (radio) { // CHANGED: arrow → function
8
+ // CHANGED: const → var (ES5)
9
+ var targetId = radio.getAttribute('data-aria-controls');
10
+ var targetElement = document.getElementById(targetId);
6
11
 
7
- if (targetElement) {
8
- targetElement.classList.remove('govcy-radio__conditional--hidden');
12
+ if (targetElement) {
13
+ targetElement.classList.remove('govcy-radio__conditional--hidden');
14
+ }
9
15
  }
10
- });
16
+ );
17
+
11
18
  // --- Disable submit button after form submission ---
12
- document.querySelectorAll('form').forEach(form => {
19
+ // CHANGED: NodeList.forEach Array.prototype.forEach.call
20
+ Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) { // CHANGED
13
21
  form.addEventListener('submit', function (e) {
14
- const submitButton = form.querySelector('[type="submit"]');
22
+ // CHANGED: const var (ES5)
23
+ var submitButton = form.querySelector('[type="submit"]');
15
24
  if (submitButton) {
16
25
  submitButton.disabled = true;
17
26
  submitButton.setAttribute('aria-disabled', 'true');
27
+ // (Optional) announce busy state for AT:
28
+ // submitButton.setAttribute('aria-busy', 'true'); // CHANGED: optional a11y improvement
18
29
  }
19
30
  });
20
31
  });
@@ -6,6 +6,11 @@ export const staticResources = {
6
6
  el: "Υποβολή",
7
7
  tr: "Gönder"
8
8
  },
9
+ continue: {
10
+ en: "Continue",
11
+ el: "Συνέχεια",
12
+ tr: "Continue"
13
+ },
9
14
  cancel: {
10
15
  en: "Cancel",
11
16
  el: "Ακύρωση",
@@ -100,6 +105,41 @@ export const staticResources = {
100
105
  en: "We have received your request. ",
101
106
  el: "Έχουμε λάβει την αίτησή σας. ",
102
107
  tr: "We have received your request. "
108
+ },
109
+ fileUploaded : {
110
+ en: "File uploaded",
111
+ el: "Το αρχείο ανεβάστηκε",
112
+ tr: "File uploaded"
113
+ },
114
+ fileNotUploaded : {
115
+ en: "File has not been uploaded. ",
116
+ el: "Το αρχείο δεν ανεβάστηκε. ",
117
+ tr: "File has not been uploaded. "
118
+ },
119
+ fileYouHaveUploaded : {
120
+ en: "You have uploaded the file for \"{{file}}\"",
121
+ el: "Έχετε ανεβάσει το αρχείο \"{{file}}\"",
122
+ tr: "You have uploaded the file for \"{{file}}\""
123
+ },
124
+ deleteFileTitle : {
125
+ en: "Are you sure you want to delete the file \"{{file}}\"? ",
126
+ el: "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
127
+ tr: "Are you sure you want to delete the file \"{{file}}\"? "
128
+ },
129
+ deleteYesOption: {
130
+ el:"Ναι, θέλω να διαγράψω το αρχείο",
131
+ en:"Yes, I want to delete this file",
132
+ tr:"Yes, I want to delete this file"
133
+ },
134
+ deleteNoOption: {
135
+ el:"Όχι, δεν θέλω να διαγράψω το αρχείο",
136
+ en:"No, I don't want to delete this file",
137
+ tr:"No, I don't want to delete this file"
138
+ },
139
+ deleteFileValidationError: {
140
+ en: "Select if you want to delete the file",
141
+ el: "Επιλέξτε αν θέλετε να διαγράψετε το αρχείο",
142
+ tr: "Select if you want to delete the filez"
103
143
  }
104
144
  },
105
145
  //remderer sections
@@ -113,9 +153,9 @@ export const staticResources = {
113
153
  element: "htmlElement",
114
154
  params: {
115
155
  text: {
116
- 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>`,
117
- 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>`,
118
- 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>`
156
+ 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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
157
+ 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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
158
+ 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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
119
159
  }
120
160
  }
121
161
  },
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
8
8
  import * as dataLayer from "./govcyDataLayer.mjs";
9
+ import * as govcyResources from "../resources/govcyResources.mjs";
9
10
 
10
11
 
11
12
  /**
@@ -17,10 +18,15 @@ import * as dataLayer from "./govcyDataLayer.mjs";
17
18
  * @param {string} siteId The site ID
18
19
  * @param {string} pageUrl The page URL
19
20
  * @param {string} lang The language
21
+ * @param {Object} fileInputElements The file input elements
22
+ * @param {string} routeParam The route parameter
20
23
  */
21
- export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el") {
24
+ export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
22
25
  const inputElements = ALLOWED_FORM_ELEMENTS;
23
- let fileInputElements = {};
26
+ const isRootCall = !fileInputElements;
27
+ if (isRootCall) {
28
+ fileInputElements = {};
29
+ }
24
30
  // Recursively populate form data with session data
25
31
  formElements.forEach(element => {
26
32
  if (inputElements.includes(element.element)) {
@@ -62,7 +68,9 @@ export function populateFormData(formElements, theData, validationErrors, store
62
68
  } else if (element.element === "fileInput") {
63
69
  // For fileInput, we change the element.element to "fileView" and set the
64
70
  // fileId and sha256 from the session store
65
- const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
71
+ // unneeded handle of `Attachment` at the end
72
+ // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
73
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
66
74
  // TODO: Ask Andreas how to handle empty file inputs
67
75
  if (fileData) {
68
76
  element.element = "fileView";
@@ -71,7 +79,7 @@ export function populateFormData(formElements, theData, validationErrors, store
71
79
  element.params.visuallyHiddenText = element.params.label;
72
80
  // TODO: Also need to set the `view` and `download` URLs
73
81
  element.params.viewHref = "#viewHref";
74
- element.params.deleteHref = "#deleteHref";
82
+ element.params.deleteHref = `/${siteId}/${pageUrl}/${fieldName}/delete-file${(routeParam) ? `?route=${routeParam}` : ''}`;
75
83
  } else {
76
84
  // TODO: Ask Andreas how to handle empty file inputs
77
85
  element.params.value = "";
@@ -102,7 +110,7 @@ export function populateFormData(formElements, theData, validationErrors, store
102
110
  if (element.element === "radios" && element.params.items) {
103
111
  element.params.items.forEach(item => {
104
112
  if (item.conditionalElements) {
105
- populateFormData(item.conditionalElements, theData, validationErrors);
113
+ populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
106
114
 
107
115
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
108
116
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
@@ -113,7 +121,7 @@ export function populateFormData(formElements, theData, validationErrors, store
113
121
  }
114
122
  });
115
123
  // add file input elements's definition in js object
116
- if (fileInputElements != {}) {
124
+ if (isRootCall && Object.keys(fileInputElements).length > 0) {
117
125
  const scriptTag = `
118
126
  <script type="text/javascript">
119
127
  window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
@@ -121,7 +129,7 @@ export function populateFormData(formElements, theData, validationErrors, store
121
129
  window._govcyPageUrl = "${pageUrl}";
122
130
  window._govcyLang = "${lang}";
123
131
  </script>
124
- <div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="polite"></div>
132
+ <div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="assertive"></div>
125
133
  <div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
126
134
  `.trim();
127
135
  formElements.push({
@@ -165,7 +173,7 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
165
173
  if (item.conditionalElements) {
166
174
  Object.assign(
167
175
  filteredData,
168
- getFormData(item.conditionalElements, formData)
176
+ getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
169
177
  );
170
178
  }
171
179
  });
@@ -184,12 +192,18 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
184
192
  } else if (element.element === "fileInput") {
185
193
  // fileInput elements are already stored in the store when it was uploaded
186
194
  // so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
187
- const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
195
+ // unneeded handle of `Attachment` at the end
196
+ // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
197
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
188
198
  if (fileData) {
189
- filteredData[name + "Attachment"] = fileData;
199
+ // unneeded handle of `Attachment` at the end
200
+ // filteredData[name + "Attachment"] = fileData;
201
+ filteredData[name] = fileData;
190
202
  } else {
191
203
  //TODO: Ask Andreas how to handle empty file inputs
192
- filteredData[name + "Attachment"] = ""; // or handle as needed
204
+ // unneeded handle of `Attachment` at the end
205
+ // filteredData[name + "Attachment"] = ""; // or handle as needed
206
+ filteredData[name] = ""; // or handle as needed
193
207
  }
194
208
  // Handle other elements (e.g., textInput, textArea, datePicker)
195
209
  } else {
@@ -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,13 +180,16 @@ 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
177
189
  // Store the file metadata in the session store
178
- dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
190
+ // unneeded handle of `Attachment` at the end
191
+ // dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
192
+ dataLayer.storePageDataElement(store, siteId, pageUrl, elementName, {
179
193
  sha256: response.Data.sha256,
180
194
  fileId: response.Data.fileId,
181
195
  });
@@ -195,6 +209,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
195
209
  } catch (err) {
196
210
  return {
197
211
  status: 500,
212
+ dataStatus: 500,
198
213
  errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
199
214
  };
200
215
  }
@@ -219,11 +234,12 @@ function containsFileInput(elements = [], targetName) {
219
234
  for (const el of elements) {
220
235
  // ✅ Direct file input match
221
236
  if (el.element === 'fileInput' && el.params?.name === targetName) {
222
- return true;
237
+ return el;
223
238
  }
224
239
  // 🔁 Recurse into nested elements (e.g. groups, conditionals)
225
240
  if (Array.isArray(el?.params?.elements)) {
226
- if (containsFileInput(el.params.elements, targetName)) return true;
241
+ const nestedMatch = containsFileInput(el.params.elements, targetName);
242
+ if (nestedMatch) return nestedMatch; // ← propagate the found element
227
243
  }
228
244
 
229
245
  // 🎯 Special case: conditional radios/checkboxes
@@ -233,7 +249,8 @@ function containsFileInput(elements = [], targetName) {
233
249
  ) {
234
250
  for (const item of el.params.items) {
235
251
  if (Array.isArray(item?.conditionalElements)) {
236
- if (containsFileInput(item.conditionalElements, targetName)) return true;
252
+ const match = containsFileInput(item.conditionalElements, targetName);
253
+ if (match) return match; // ← propagate the found element
237
254
  }
238
255
  }
239
256
  }
@@ -249,16 +266,24 @@ function containsFileInput(elements = [], targetName) {
249
266
  * @param {string} elementName The name of the element to check
250
267
  * @return {boolean} True if a fileInput exists, false otherwise
251
268
  */
252
- function pageContainsFileInput(pageTemplate, elementName) {
269
+ export function pageContainsFileInput(pageTemplate, elementName) {
253
270
  const sections = pageTemplate?.sections || [];
254
- return sections.some(section =>
255
- section?.elements?.some(el =>
256
- el.element === 'form' &&
257
- containsFileInput(el.params?.elements, elementName)
258
- )
259
- );
271
+
272
+ for (const section of sections) {
273
+ for (const el of section?.elements || []) {
274
+ if (el.element === 'form') {
275
+ const match = containsFileInput(el.params?.elements, elementName);
276
+ if (match) {
277
+ return match; // ← return the actual element
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ return null; // no match found
260
284
  }
261
285
 
286
+
262
287
  /**
263
288
  * Validates magic bytes against expected mimetype
264
289
  * @param {Buffer} buffer
@@ -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,8 +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
- : (field.element === "fileInput") // Handle fileInput
267
- ? formData[`${field.params.name}Attachment`] || ""
266
+ // unneeded handle of `Attachment` at the end
267
+ // : (field.element === "fileInput") // Handle fileInput
268
+ // ? formData[`${field.params.name}Attachment`] || ""
268
269
  : formData[field.params.name] || ""; // Get submitted value
269
270
 
270
271
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
@@ -313,7 +314,9 @@ export function validateFormElements(elements, formData, pageUrl) {
313
314
  .filter(Boolean) // Remove empty values
314
315
  .join("-") // Join remaining parts
315
316
  : (conditionalElement.element === "fileInput") // Handle fileInput
316
- ? formData[`${conditionalElement.params.name}Attachment`] || ""
317
+ // unneeded handle of `Attachment` at the end
318
+ // ? formData[`${conditionalElement.params.name}Attachment`] || ""
319
+ ? formData[`${conditionalElement.params.name}`] || ""
317
320
  : formData[conditionalElement.params.name] || ""; // Get submitted value
318
321
 
319
322
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`