@gov-cy/govcy-express-services 1.0.0-alpha.6 β†’ 1.0.0-alpha.7

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.7",
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));
@@ -1,16 +1,27 @@
1
1
  // πŸ” Select all file inputs that have the .govcy-file-upload class
2
2
  const 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;
6
-
7
- // πŸ”§ Define siteId and pageUrl (you can dynamically extract these later)
8
- const siteId = 'test';
9
- const pageUrl = 'data-entry-all';
10
-
11
4
  // πŸ” Loop over each file input and attach a change event listener
12
5
  fileInputs.forEach(input => {
13
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";
14
25
  // πŸ“¦ Grab the selected file
15
26
  const file = event.target.files[0];
16
27
  const elementName = input.name; // Form field's `name` attribute
@@ -24,7 +35,7 @@ fileInputs.forEach(input => {
24
35
 
25
36
  try {
26
37
  // πŸš€ Send file to the backend upload API
27
- const response = await axios.post(`/${siteId}/${pageUrl}/upload`, formData, {
38
+ const response = await axios.post(`/apis/${siteId}/${pageUrl}/upload`, formData, {
28
39
  headers: {
29
40
  'X-CSRF-Token': csrfToken // πŸ” Pass CSRF token in custom header
30
41
  }
@@ -36,11 +47,106 @@ fileInputs.forEach(input => {
36
47
  document.querySelector(`[name="${elementName}Attachment[fileId]"`).value = fileId;
37
48
  document.querySelector(`[name="${elementName}Attachment[sha256]"`).value = sha256;
38
49
 
39
- alert('βœ… File uploaded successfully');
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');
40
102
 
41
103
  } catch (err) {
42
- // ⚠️ Show an error message if upload fails
43
- alert('❌ Upload failed: ' + (err.response?.data?.error || err.message));
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
139
+ }
140
+ // βœ… Update ARIA live region with success message
141
+ const statusRegion = document.getElementById('_govcy-upload-error');
142
+ if (statusRegion) {
143
+ statusRegion.textContent = messages.uploadFailed[lang];
144
+ setTimeout(() => {
145
+ statusRegion.textContent = '';
146
+ }, 10000);
147
+ }
148
+ // // ⚠️ Show an error message if upload fails
149
+ // alert('❌ Upload failed: ' + (err.response?.data?.error || err.message));
44
150
  }
45
151
  });
46
152
  });
@@ -113,9 +113,9 @@ export const staticResources = {
113
113
  element: "htmlElement",
114
114
  params: {
115
115
  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>`
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>`
119
119
  }
120
120
  }
121
121
  },
@@ -192,6 +192,27 @@ export function csrfTokenInput(csrfToken) {
192
192
  };
193
193
  }
194
194
 
195
+ /**
196
+ * Get the site and page input elements
197
+ * @param {string} siteId The site id
198
+ * @param {string} pageUrl The page url
199
+ * @param {string} lang The page language
200
+ * @returns {object} htmlElement with the site and page inputs
201
+ */
202
+ export function siteAndPageInput(siteId, pageUrl, lang = "el") {
203
+ const siteAndPageInputs = `<input type="hidden" name="_siteId" value="${siteId}"><input type="hidden" name="_pageUrl" value="${pageUrl}"><input type="hidden" name="_lang" value="${lang}">`;
204
+ return {
205
+ element: "htmlElement",
206
+ params: {
207
+ text: {
208
+ en: siteAndPageInputs,
209
+ el: siteAndPageInputs,
210
+ tr: siteAndPageInputs
211
+ }
212
+ }
213
+ };
214
+ }
215
+
195
216
  /**
196
217
  * Error page template
197
218
  * @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,22 @@
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") {
16
22
  const inputElements = ALLOWED_FORM_ELEMENTS;
17
-
23
+ let fileInputElements = {};
18
24
  // Recursively populate form data with session data
19
25
  formElements.forEach(element => {
20
26
  if (inputElements.includes(element.element)) {
@@ -53,6 +59,24 @@ export function populateFormData(formElements, theData, validationErrors) {
53
59
  // Invalid format (not matching D/M/YYYY or DD/MM/YYYY)
54
60
  element.params.value = "";
55
61
  }
62
+ } else if (element.element === "fileInput") {
63
+ // For fileInput, we change the element.element to "fileView" and set the
64
+ // fileId and sha256 from the session store
65
+ const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
66
+ // TODO: Ask Andreas how to handle empty file inputs
67
+ if (fileData) {
68
+ element.element = "fileView";
69
+ element.params.fileId = fileData.fileId;
70
+ element.params.sha256 = fileData.sha256;
71
+ element.params.visuallyHiddenText = element.params.label;
72
+ // TODO: Also need to set the `view` and `download` URLs
73
+ element.params.viewHref = "#viewHref";
74
+ element.params.deleteHref = "#deleteHref";
75
+ } else {
76
+ // TODO: Ask Andreas how to handle empty file inputs
77
+ element.params.value = "";
78
+ }
79
+ fileInputElements[fieldName] = element;
56
80
  // Handle all other input elements (textInput, checkboxes, radios, etc.)
57
81
  } else {
58
82
  element.params.value = theData[fieldName] || "";
@@ -88,6 +112,29 @@ export function populateFormData(formElements, theData, validationErrors) {
88
112
  });
89
113
  }
90
114
  });
115
+ // add file input elements's definition in js object
116
+ if (fileInputElements != {}) {
117
+ const scriptTag = `
118
+ <script type="text/javascript">
119
+ window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
120
+ window._govcySiteId = "${siteId}";
121
+ window._govcyPageUrl = "${pageUrl}";
122
+ window._govcyLang = "${lang}";
123
+ </script>
124
+ <div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="polite"></div>
125
+ <div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
126
+ `.trim();
127
+ formElements.push({
128
+ element: 'htmlElement',
129
+ params: {
130
+ text: {
131
+ en: scriptTag,
132
+ el: scriptTag,
133
+ tr: scriptTag
134
+ }
135
+ }
136
+ });
137
+ }
91
138
  }
92
139
 
93
140
 
@@ -96,9 +143,12 @@ export function populateFormData(formElements, theData, validationErrors) {
96
143
  *
97
144
  * @param {Array} elements - The form elements (including conditional ones).
98
145
  * @param {Object} formData - The submitted form data.
146
+ * @param {Object} store - The session store .
147
+ * @param {string} siteId - The site ID .
148
+ * @param {string} pageUrl - The page URL .
99
149
  * @returns {Object} filteredData - The filtered form data.
100
150
  */
101
- export function getFormData(elements, formData) {
151
+ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
102
152
  const filteredData = {};
103
153
  elements.forEach(element => {
104
154
  const { name } = element.params || {};
@@ -130,6 +180,17 @@ export function getFormData(elements, formData) {
130
180
  filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
131
181
  filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
132
182
  filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
183
+ // handle fileInput
184
+ } else if (element.element === "fileInput") {
185
+ // fileInput elements are already stored in the store when it was uploaded
186
+ // 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");
188
+ if (fileData) {
189
+ filteredData[name + "Attachment"] = fileData;
190
+ } else {
191
+ //TODO: Ask Andreas how to handle empty file inputs
192
+ filteredData[name + "Attachment"] = ""; // or handle as needed
193
+ }
133
194
  // Handle other elements (e.g., textInput, textArea, datePicker)
134
195
  } else {
135
196
  filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
@@ -174,6 +174,11 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
174
174
  }
175
175
 
176
176
  // βœ… Success
177
+ // Store the file metadata in the session store
178
+ dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
179
+ sha256: response.Data.sha256,
180
+ fileId: response.Data.fileId,
181
+ });
177
182
  logger.debug("File upload successful", response.Data);
178
183
  logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
179
184
  return {
@@ -263,6 +263,8 @@ 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
268
  : formData[field.params.name] || ""; // Get submitted value
267
269
 
268
270
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
@@ -310,6 +312,8 @@ export function validateFormElements(elements, formData, pageUrl) {
310
312
  formData[`${conditionalElement.params.name}_day`]]
311
313
  .filter(Boolean) // Remove empty values
312
314
  .join("-") // Join remaining parts
315
+ : (conditionalElement.element === "fileInput") // Handle fileInput
316
+ ? formData[`${conditionalElement.params.name}Attachment`] || ""
313
317
  : formData[conditionalElement.params.name] || ""; // Get submitted value
314
318
 
315
319
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`