@gov-cy/govcy-express-services 1.0.0-alpha.1 → 1.0.0-alpha.10

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.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The successResponse function creates a standardized success response object.
3
+ *
4
+ * @param {*} data - The data to be included in the response.
5
+ * @returns {Object} - The success response object.
6
+ */
7
+ export function successResponse(data = null) {
8
+ return {
9
+ Succeeded: true,
10
+ ErrorCode: 0,
11
+ ErrorMessage: '',
12
+ Data: data
13
+ };
14
+ }
15
+
16
+ /**
17
+ * The errorResponse function creates a standardized error response object.
18
+ *
19
+ * @param {int} code - The error code to be included in the response.
20
+ * @param {string} message - The error message to be included in the response.
21
+ * @param {Object} data - Additional data to be included in the response.
22
+ * @returns {Object} - The error response object.
23
+ */
24
+ export function errorResponse(code = 1, message = 'Unknown error', data = null) {
25
+ return {
26
+ Succeeded: false,
27
+ ErrorCode: code,
28
+ ErrorMessage: message,
29
+ Data: data
30
+ };
31
+ }
@@ -1,4 +1,8 @@
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
+ export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
6
+ export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
7
+ export const ALLOWED_FILE_SIZE_MB = 5; // Maximum file size in MB
8
+ export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // 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
  * *
@@ -328,6 +336,20 @@ export function getSiteLoadData(store, siteId) {
328
336
  return null;
329
337
  }
330
338
 
339
+ /**
340
+ * Get the site's reference number from load data from the store
341
+ *
342
+ * @param {object} store The session store
343
+ * @param {string} siteId The site ID
344
+ * @returns {string|null} The reference number or null if not available
345
+ */
346
+ export function getSiteLoadDataReferenceNumber(store, siteId) {
347
+ const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
348
+
349
+ return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
350
+ }
351
+
352
+
331
353
  /**
332
354
  * Get the site's input data from the store
333
355
  *
@@ -124,7 +124,7 @@ export function evaluateExpressionWithFlattening(expression, object, prefix = ''
124
124
  * @returns {{ result: true } | { result: false, redirect: string }}
125
125
  * An object indicating whether to render the page or redirect
126
126
  */
127
- export function evaluatePageConditions(page, store, siteKey, req) {
127
+ export function evaluatePageConditions(page, store, siteKey, req = {}) {
128
128
  // Get conditions array from nested page structure
129
129
  const conditions = page?.pageData?.conditions;
130
130
 
@@ -5,16 +5,28 @@
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";
9
+ import * as govcyResources from "../resources/govcyResources.mjs";
8
10
 
9
11
 
10
12
  /**
11
13
  * Helper function to populate form data with session data
12
14
  * @param {Array} formElements The form elements
13
15
  * @param {*} theData The data either from session or request
16
+ * @param {Object} validationErrors The validation errors
17
+ * @param {Object} store The session store
18
+ * @param {string} siteId The site ID
19
+ * @param {string} pageUrl The page URL
20
+ * @param {string} lang The language
21
+ * @param {Object} fileInputElements The file input elements
22
+ * @param {string} routeParam The route parameter
14
23
  */
15
- export function populateFormData(formElements, theData, validationErrors) {
24
+ export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
16
25
  const inputElements = ALLOWED_FORM_ELEMENTS;
17
-
26
+ const isRootCall = !fileInputElements;
27
+ if (isRootCall) {
28
+ fileInputElements = {};
29
+ }
18
30
  // Recursively populate form data with session data
19
31
  formElements.forEach(element => {
20
32
  if (inputElements.includes(element.element)) {
@@ -53,6 +65,27 @@ export function populateFormData(formElements, theData, validationErrors) {
53
65
  // Invalid format (not matching D/M/YYYY or DD/MM/YYYY)
54
66
  element.params.value = "";
55
67
  }
68
+ } else if (element.element === "fileInput") {
69
+ // For fileInput, we change the element.element to "fileView" and set the
70
+ // fileId and sha256 from the session store
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);
74
+ // TODO: Ask Andreas how to handle empty file inputs
75
+ if (fileData) {
76
+ element.element = "fileView";
77
+ element.params.fileId = fileData.fileId;
78
+ element.params.sha256 = fileData.sha256;
79
+ element.params.visuallyHiddenText = element.params.label;
80
+ // TODO: Also need to set the `view` and `download` URLs
81
+ element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
82
+ element.params.viewTarget = "_blank";
83
+ element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
84
+ } else {
85
+ // TODO: Ask Andreas how to handle empty file inputs
86
+ element.params.value = "";
87
+ }
88
+ fileInputElements[fieldName] = element;
56
89
  // Handle all other input elements (textInput, checkboxes, radios, etc.)
57
90
  } else {
58
91
  element.params.value = theData[fieldName] || "";
@@ -78,7 +111,7 @@ export function populateFormData(formElements, theData, validationErrors) {
78
111
  if (element.element === "radios" && element.params.items) {
79
112
  element.params.items.forEach(item => {
80
113
  if (item.conditionalElements) {
81
- populateFormData(item.conditionalElements, theData, validationErrors);
114
+ populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
82
115
 
83
116
  // Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
84
117
  if (item.conditionalElements.some(condEl => condEl.params?.error)) {
@@ -88,6 +121,29 @@ export function populateFormData(formElements, theData, validationErrors) {
88
121
  });
89
122
  }
90
123
  });
124
+ // add file input elements's definition in js object
125
+ if (isRootCall && Object.keys(fileInputElements).length > 0) {
126
+ const scriptTag = `
127
+ <script type="text/javascript">
128
+ window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
129
+ window._govcySiteId = "${siteId}";
130
+ window._govcyPageUrl = "${pageUrl}";
131
+ window._govcyLang = "${lang}";
132
+ </script>
133
+ <div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="assertive"></div>
134
+ <div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
135
+ `.trim();
136
+ formElements.push({
137
+ element: 'htmlElement',
138
+ params: {
139
+ text: {
140
+ en: scriptTag,
141
+ el: scriptTag,
142
+ tr: scriptTag
143
+ }
144
+ }
145
+ });
146
+ }
91
147
  }
92
148
 
93
149
 
@@ -96,9 +152,12 @@ export function populateFormData(formElements, theData, validationErrors) {
96
152
  *
97
153
  * @param {Array} elements - The form elements (including conditional ones).
98
154
  * @param {Object} formData - The submitted form data.
155
+ * @param {Object} store - The session store .
156
+ * @param {string} siteId - The site ID .
157
+ * @param {string} pageUrl - The page URL .
99
158
  * @returns {Object} filteredData - The filtered form data.
100
159
  */
101
- export function getFormData(elements, formData) {
160
+ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
102
161
  const filteredData = {};
103
162
  elements.forEach(element => {
104
163
  const { name } = element.params || {};
@@ -115,7 +174,7 @@ export function getFormData(elements, formData) {
115
174
  if (item.conditionalElements) {
116
175
  Object.assign(
117
176
  filteredData,
118
- getFormData(item.conditionalElements, formData)
177
+ getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
119
178
  );
120
179
  }
121
180
  });
@@ -130,6 +189,23 @@ export function getFormData(elements, formData) {
130
189
  filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
131
190
  filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
132
191
  filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
192
+ // handle fileInput
193
+ } else if (element.element === "fileInput") {
194
+ // fileInput elements are already stored in the store when it was uploaded
195
+ // so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
196
+ // unneeded handle of `Attachment` at the end
197
+ // const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
198
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
199
+ if (fileData) {
200
+ // unneeded handle of `Attachment` at the end
201
+ // filteredData[name + "Attachment"] = fileData;
202
+ filteredData[name] = fileData;
203
+ } else {
204
+ //TODO: Ask Andreas how to handle empty file inputs
205
+ // unneeded handle of `Attachment` at the end
206
+ // filteredData[name + "Attachment"] = ""; // or handle as needed
207
+ filteredData[name] = ""; // or handle as needed
208
+ }
133
209
  // Handle other elements (e.g., textInput, textArea, datePicker)
134
210
  } else {
135
211
  filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
@@ -0,0 +1,307 @@
1
+ import FormData from 'form-data';
2
+ import { getPageConfigData } from "./govcyLoadConfigData.mjs";
3
+ import { evaluatePageConditions } from "./govcyExpressions.mjs";
4
+ import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
5
+ import { ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_SIZE_MB } from "./govcyConstants.mjs";
6
+ import { govcyApiRequest } from "./govcyApiRequest.mjs";
7
+ import * as dataLayer from "./govcyDataLayer.mjs";
8
+ import { logger } from './govcyLogger.mjs';
9
+
10
+ /**
11
+ * Handles the logic for uploading a file to the configured Upload API.
12
+ * Does not send a response — just returns a standard object to be handled by middleware.
13
+ *
14
+ * @param {object} opts - Input parameters
15
+ * @param {object} opts.service - The service config object
16
+ * @param {object} opts.store - Session store (req.session)
17
+ * @param {string} opts.siteId - Site ID
18
+ * @param {string} opts.pageUrl - Page URL
19
+ * @param {string} opts.elementName - Name of file input
20
+ * @param {object} opts.file - File object from multer (req.file)
21
+ * @returns {Promise<{ status: number, data?: object, errorMessage?: string }>}
22
+ */
23
+ export async function handleFileUpload({ service, store, siteId, pageUrl, elementName, file }) {
24
+ try {
25
+ // Validate essentials
26
+ // Early exit if key things are missing
27
+ if (!file || !elementName) {
28
+ return {
29
+ status: 400,
30
+ dataStatus: 400,
31
+ errorMessage: 'Missing file or element name'
32
+ };
33
+ }
34
+
35
+ // Get the upload configuration
36
+ const uploadCfg = service?.site?.fileUploadAPIEndpoint;
37
+ // Check if upload configuration is available
38
+ if (!uploadCfg?.url || !uploadCfg?.clientKey || !uploadCfg?.serviceId) {
39
+ return {
40
+ status: 400,
41
+ dataStatus: 401,
42
+ errorMessage: 'Missing upload configuration'
43
+ };
44
+ }
45
+
46
+ // Environment vars
47
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
48
+ let url = getEnvVariable(uploadCfg.url || "", false);
49
+ const clientKey = getEnvVariable(uploadCfg.clientKey || "", false);
50
+ const serviceId = getEnvVariable(uploadCfg.serviceId || "", false);
51
+ const dsfGtwKey = getEnvVariable(uploadCfg?.dsfgtwApiKey || "", "");
52
+ const method = (uploadCfg?.method || "PUT").toLowerCase();
53
+
54
+ // Check if the upload API is configured correctly
55
+ if (!url || !clientKey) {
56
+ return {
57
+ status: 400,
58
+ dataStatus: 402,
59
+ errorMessage: 'Missing environment variables for upload'
60
+ };
61
+ }
62
+
63
+ // Construct the URL with tag being the elementName
64
+ const tag = encodeURIComponent(elementName.trim());
65
+ url += `/${tag}`;
66
+
67
+ // Get the page configuration using utility (safely extracts the correct page)
68
+ const page = getPageConfigData(service, pageUrl);
69
+ // Check if the page template is valid
70
+ if (!page?.pageTemplate) {
71
+ return {
72
+ status: 400,
73
+ dataStatus: 403,
74
+ errorMessage: 'Invalid page configuration'
75
+ };
76
+ }
77
+
78
+ // ----- Conditional logic comes here
79
+ // Respect conditional logic: If the page is skipped due to conditions, abort
80
+ const conditionResult = evaluatePageConditions(page, store, siteId);
81
+ if (conditionResult.result === false) {
82
+ return {
83
+ status: 403,
84
+ dataStatus: 404,
85
+ errorMessage: 'This page is skipped by conditional logic'
86
+ };
87
+ }
88
+
89
+ // deep copy the page template to avoid modifying the original
90
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
91
+ // Validate the field: Only allow upload if the page contains a fileInput with the given name
92
+ const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
93
+ if (!isAllowed) {
94
+ return {
95
+ status: 403,
96
+ dataStatus: 405,
97
+ errorMessage: `File input [${elementName}] not allowed on this page`
98
+ };
99
+ }
100
+
101
+ // Empty file check
102
+ if (file.size === 0) {
103
+ return {
104
+ status: 400,
105
+ dataStatus: 406,
106
+ errorMessage: 'Uploaded file is empty'
107
+ };
108
+ }
109
+
110
+ // file type checks
111
+ // 1. Check declared mimetype
112
+ if (!ALLOWED_FILE_MIME_TYPES.includes(file.mimetype)) {
113
+ return {
114
+ status: 400,
115
+ dataStatus: 407,
116
+ errorMessage: 'Invalid file type (MIME not allowed)'
117
+ };
118
+ }
119
+
120
+ // 2. Check actual file content (magic bytes) matches claimed MIME type
121
+ if (!isMagicByteValid(file.buffer, file.mimetype)) {
122
+ return {
123
+ status: 400,
124
+ dataStatus: 408,
125
+ errorMessage: 'Invalid file type (magic byte mismatch)'
126
+ };
127
+ }
128
+
129
+ // File size check
130
+ if (file.size > ALLOWED_FILE_SIZE_MB * 1024 * 1024) {
131
+ return {
132
+ status: 400,
133
+ dataStatus: 409,
134
+ errorMessage: 'File exceeds allowed size'
135
+ };
136
+ }
137
+
138
+ // Prepare FormData
139
+ const form = new FormData();
140
+ form.append('file', file.buffer, {
141
+ filename: file.originalname,
142
+ contentType: file.mimetype,
143
+ });
144
+
145
+ logger.debug("Prepared FormData with file:", {
146
+ filename: file.originalname,
147
+ mimetype: file.mimetype,
148
+ size: file.size
149
+ });
150
+
151
+ // Get the user
152
+ const user = dataLayer.getUser(store);
153
+ // Perform the upload request
154
+ const response = await govcyApiRequest(
155
+ method,
156
+ url,
157
+ form,
158
+ true,
159
+ user,
160
+ {
161
+ accept: "text/plain",
162
+ "client-key": clientKey,
163
+ "service-id": serviceId,
164
+ ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
165
+ },
166
+ 3,
167
+ allowSelfSignedCerts
168
+ );
169
+
170
+ // If not succeeded, handle error
171
+ if (!response?.Succeeded) {
172
+ return {
173
+ status: 500,
174
+ dataStatus: 410,
175
+ errorMessage: `${response?.ErrorCode} - ${response?.ErrorMessage} - fileUploadAPIEndpoint returned succeeded false`
176
+ };
177
+ }
178
+
179
+ // Check if the response contains the expected data
180
+ if (!response?.Data?.fileId || !response?.Data?.sha256) {
181
+ return {
182
+ status: 500,
183
+ dataStatus: 411,
184
+ errorMessage: 'Missing fileId or sha256 in response'
185
+ };
186
+ }
187
+
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
+ });
196
+ logger.debug("File upload successful", response.Data);
197
+ logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
198
+ return {
199
+ status: 200,
200
+ data: {
201
+ sha256: response.Data.sha256,
202
+ filename: response.Data.fileName || '',
203
+ fileId: response.Data.fileId,
204
+ mimeType: response.Data.contentType || '',
205
+ fileSize: response.Data?.fileSize || ''
206
+ }
207
+ };
208
+
209
+ } catch (err) {
210
+ return {
211
+ status: 500,
212
+ dataStatus: 500,
213
+ errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
214
+ };
215
+ }
216
+ }
217
+
218
+ //--------------------------------------------------------------------------
219
+ // Helper Functions
220
+ /**
221
+ * Recursively checks whether any element (or its children) is a fileInput
222
+ * with the matching elementName.
223
+ *
224
+ * Supports:
225
+ * - Top-level fileInput
226
+ * - Nested `params.elements` (used in groups, conditionals, etc.)
227
+ * - Conditional radios/checkboxes with `items[].conditionalElements`
228
+ *
229
+ * @param {Array} elements - The array of elements to search
230
+ * @param {string} targetName - The name of the file input to check
231
+ * @returns {boolean} True if a matching fileInput is found, false otherwise
232
+ */
233
+ function containsFileInput(elements = [], targetName) {
234
+ for (const el of elements) {
235
+ // ✅ Direct file input match
236
+ if (el.element === 'fileInput' && el.params?.name === targetName) {
237
+ return el;
238
+ }
239
+ // 🔁 Recurse into nested elements (e.g. groups, conditionals)
240
+ if (Array.isArray(el?.params?.elements)) {
241
+ const nestedMatch = containsFileInput(el.params.elements, targetName);
242
+ if (nestedMatch) return nestedMatch; // ← propagate the found element
243
+ }
244
+
245
+ // 🎯 Special case: conditional radios/checkboxes
246
+ if (
247
+ (el.element === 'radios' || el.element === 'checkboxes') &&
248
+ Array.isArray(el?.params?.items)
249
+ ) {
250
+ for (const item of el.params.items) {
251
+ if (Array.isArray(item?.conditionalElements)) {
252
+ const match = containsFileInput(item.conditionalElements, targetName);
253
+ if (match) return match; // ← propagate the found element
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return false;
259
+ }
260
+
261
+ /**
262
+ * Checks whether the specified page contains a valid fileInput for this element ID
263
+ * under any <form> element in its sections
264
+ *
265
+ * @param {object} pageTemplate The page template object
266
+ * @param {string} elementName The name of the element to check
267
+ * @return {boolean} True if a fileInput exists, false otherwise
268
+ */
269
+ export function pageContainsFileInput(pageTemplate, elementName) {
270
+ const sections = pageTemplate?.sections || [];
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
284
+ }
285
+
286
+
287
+ /**
288
+ * Validates magic bytes against expected mimetype
289
+ * @param {Buffer} buffer
290
+ * @param {string} mimetype
291
+ * @returns {boolean}
292
+ */
293
+ export function isMagicByteValid(buffer, mimetype) {
294
+ const signatures = {
295
+ 'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
296
+ 'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG
297
+ 'image/jpeg': [0xFF, 0xD8, 0xFF], // JPG/JPEG
298
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [0x50, 0x4B, 0x03, 0x04], // DOCX
299
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [0x50, 0x4B, 0x03, 0x04], // XLSX
300
+ };
301
+
302
+ const expected = signatures[mimetype];
303
+ if (!expected) return false; // unknown type
304
+
305
+ const actual = Array.from(buffer.slice(0, expected.length));
306
+ return expected.every((byte, i) => actual[i] === byte);
307
+ }
@@ -73,7 +73,8 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
73
73
  ...(getCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": getCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
74
74
  },
75
75
  3,
76
- allowSelfSignedCerts
76
+ allowSelfSignedCerts,
77
+ [200, 404] // Allowed HTTP status codes
77
78
  );
78
79
 
79
80
  // If not succeeded, handle error
@@ -88,7 +89,9 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
88
89
  dataLayer.storeSiteLoadData(store, siteId, getResponse.Data);
89
90
 
90
91
  try {
91
- const parsed = JSON.parse(getResponse.Data.submissionData || "{}");
92
+ const parsed = JSON.parse(getResponse.Data.submissionData
93
+ || getResponse.Data.submission_data
94
+ || "{}");
92
95
  if (parsed && typeof parsed === "object") {
93
96
  dataLayer.storeSiteInputData(store, siteId, parsed);
94
97
  logger.debug(`💾 Input data restored from saved submission for siteId: ${siteId}`);
@@ -99,11 +102,15 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
99
102
 
100
103
  // if not call the PUT submission API
101
104
  } else {
105
+ const tempPutPayload = {
106
+ // submission_data: JSON.stringify({}),
107
+ submissionData: JSON.stringify({})
108
+ };
102
109
  // If no data, call the PUT submission API to create it
103
110
  const putResponse = await govcyApiRequest(
104
111
  putCfgMethod,
105
112
  putCfgUrl,
106
- putCfgParams,
113
+ tempPutPayload,
107
114
  true, // use access token auth
108
115
  user,
109
116
  {
@@ -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
  }
@@ -410,6 +437,33 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
410
437
  };
411
438
  }
412
439
 
440
+ /**
441
+ * Helper function to create a summary list item for file links.
442
+ * @param {object} key the key of multilingual object
443
+ * @param {string} value the value
444
+ * @param {string} siteId the site id
445
+ * @param {string} pageUrl the page url
446
+ * @param {string} elementName the element name
447
+ * @returns {object} the summary list item with file link
448
+ */
449
+ function createSummaryListItemFileLink(key, value, siteId, pageUrl, elementName) {
450
+ return {
451
+ "key": key,
452
+ "value": [
453
+ {
454
+ "element": "htmlElement",
455
+ "params": {
456
+ "text": {
457
+ "en": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.en}<span class="govcy-visually-hidden"> ${key?.en || ""}</span></a>`,
458
+ "el": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.el}<span class="govcy-visually-hidden"> ${key?.el || ""}</span></a>`,
459
+ "tr": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.tr}<span class="govcy-visually-hidden"> ${key?.tr || ""}</span></a>`
460
+ }
461
+ }
462
+ }
463
+ ]
464
+ };
465
+ }
466
+
413
467
 
414
468
 
415
469
 
@@ -425,8 +479,14 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
425
479
  for (const field of fields) {
426
480
  const label = field.label;
427
481
  const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
428
- // add the field to the summary entry
429
- summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
482
+ // --- HACK --- to see if this is a file element
483
+ // check if field.value is an object with `sha256` and `fileId` properties
484
+ if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
485
+ summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
486
+ } else {
487
+ // add the field to the summary entry
488
+ summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
489
+ }
430
490
  }
431
491
 
432
492
  // Add inner summary list to the main summary list