@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.0.0-alpha.6",
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.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,46 +1,175 @@
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
- // 🔐 Get the CSRF token from a hidden input field (generated by your backend)
5
- const csrfToken = document.querySelector('input[type="hidden"][name="_csrf"]')?.value;
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
- // 🔁 Loop over each file input and attach a change event listener
12
- fileInputs.forEach(input => {
13
- input.addEventListener('change', async (event) => {
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
- const file = event.target.files[0];
16
- const elementName = input.name; // Form field's `name` attribute
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
- const formData = new FormData();
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
- const response = await axios.post(`/${siteId}/${pageUrl}/upload`, formData, {
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
- const { sha256, fileId } = response.data.Data;
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(`[name="${elementName}Attachment[fileId]"`).value = fileId;
37
- document.querySelector(`[name="${elementName}Attachment[sha256]"`).value = sha256;
81
+ // document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
82
+ // document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
38
83
 
39
- alert('✅ File uploaded successfully');
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
- alert('❌ Upload failed: ' + (err.response?.data?.error || err.message));
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 = /^\/[^/]+\/[^/]+\/(upload|download)$/;
13
+ const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+\/(upload|download)$/;
14
14
  const isStructuredApiUrl = apiUrlPattern.test(req.originalUrl || req.url);
15
15
 
16
16
  return acceptJson || isStructuredApiUrl;
@@ -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`