@gov-cy/govcy-express-services 1.0.0-alpha.8 → 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 +1 -1
- package/src/index.mjs +7 -0
- package/src/middleware/govcyDeleteFileHandler.mjs +234 -0
- package/src/middleware/govcyPageHandler.mjs +2 -1
- package/src/public/js/govcyFiles.js +97 -76
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +36 -6
- package/src/utils/govcyFormHandling.mjs +6 -3
- package/src/utils/govcyHandleFiles.mjs +20 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.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
|
+
|
|
@@ -9,94 +9,106 @@ fileInputs.forEach(function(input) {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Handles the upload of a file event
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @param {object} event The event
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
function _uploadFileEventHandler(event) {
|
|
16
16
|
var input = event.target;
|
|
17
17
|
var messages = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
});
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
var lang = window._govcyLang || "el";
|
|
57
|
-
// 📦 Grab the selected file
|
|
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
|
|
61
|
-
|
|
62
|
-
if (!file) return; // Exit if no file was selected
|
|
63
|
-
|
|
64
|
-
// 🧵 Prepare form-data payload for the API
|
|
65
|
-
var formData = new FormData();
|
|
66
|
-
formData.append('file', file); // Attach the actual file
|
|
67
|
-
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
// 🚀 Send file to the backend upload API
|
|
71
|
-
var response = await axios.post(`/apis/${siteId}/${pageUrl}/upload`, formData, {
|
|
72
|
-
headers: {
|
|
73
|
-
'X-CSRF-Token': csrfToken // 🔐 Pass CSRF token in custom header
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
var sha256 = response.data.Data.sha256;
|
|
78
|
-
var fileId = response.data.Data.fileId;
|
|
79
|
-
|
|
80
|
-
// 📝 Store returned metadata in hidden fields for submission with the form
|
|
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
|
|
81
94
|
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
|
|
82
95
|
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
|
|
83
96
|
|
|
84
97
|
// Render the file view
|
|
85
98
|
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
// Accessibility: Update ARIA live region with success message
|
|
88
101
|
var statusRegion = document.getElementById('_govcy-upload-status');
|
|
89
102
|
if (statusRegion) {
|
|
90
103
|
setTimeout(function() {
|
|
91
104
|
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
92
|
-
}, 200)
|
|
105
|
+
}, 200);
|
|
93
106
|
setTimeout(function() {
|
|
94
107
|
statusRegion.textContent = '';
|
|
95
|
-
}, 5000);
|
|
108
|
+
}, 5000);
|
|
96
109
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} catch (err) {
|
|
110
|
+
})
|
|
111
|
+
.catch(function(err) {
|
|
100
112
|
// ⚠️ Show an error message if upload fails
|
|
101
113
|
var errorMessage = messages.uploadFailed;
|
|
102
114
|
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
|
|
@@ -106,8 +118,8 @@ async function _uploadFileEventHandler(event) {
|
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
// Render the file input with error
|
|
109
|
-
_renderFileElement("fileInput", elementId, elementName, "","", errorMessage);
|
|
110
|
-
|
|
121
|
+
_renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
|
|
122
|
+
|
|
111
123
|
// Re-bind the file input's change handler
|
|
112
124
|
var newInput = document.getElementById(elementId);
|
|
113
125
|
if (newInput) {
|
|
@@ -116,10 +128,10 @@ async function _uploadFileEventHandler(event) {
|
|
|
116
128
|
|
|
117
129
|
// Accessibility: Focus on the form field
|
|
118
130
|
document.getElementById(elementId)?.focus();
|
|
119
|
-
|
|
120
|
-
}
|
|
131
|
+
});
|
|
121
132
|
}
|
|
122
133
|
|
|
134
|
+
|
|
123
135
|
/**
|
|
124
136
|
* Renders a file element in the DOM
|
|
125
137
|
*
|
|
@@ -131,6 +143,14 @@ async function _uploadFileEventHandler(event) {
|
|
|
131
143
|
* @param {object} errorMessage The error message in all supported languages
|
|
132
144
|
*/
|
|
133
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
|
+
|
|
134
154
|
// Create an instance of GovcyFrontendRendererBrowser
|
|
135
155
|
var renderer = new GovcyFrontendRendererBrowser();
|
|
136
156
|
var lang = window._govcyLang || "el";
|
|
@@ -141,7 +161,7 @@ function _renderFileElement(elementState, elementId, elementName, fileId, sha256
|
|
|
141
161
|
"lang": lang
|
|
142
162
|
}
|
|
143
163
|
};
|
|
144
|
-
var fileInputMap =
|
|
164
|
+
var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
|
|
145
165
|
var fileElement = fileInputMap[elementName];
|
|
146
166
|
fileElement.element = elementState;
|
|
147
167
|
if (errorMessage != null) fileElement.params.error = errorMessage;
|
|
@@ -151,7 +171,8 @@ function _renderFileElement(elementState, elementId, elementName, fileId, sha256
|
|
|
151
171
|
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
152
172
|
// TODO: Also need to set the `view` and `download` URLs
|
|
153
173
|
fileElement.params.viewHref = "#viewHref";
|
|
154
|
-
fileElement.params.deleteHref = "
|
|
174
|
+
fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/" + elementName + "/delete-file"
|
|
175
|
+
+ (route !== null ? "?route=" + encodeURIComponent(route) : "");
|
|
155
176
|
}
|
|
156
177
|
// Construct the JSONTemplate
|
|
157
178
|
var JSONTemplate = {
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", function () {
|
|
2
2
|
// --- Show conditionals for checked radios ---
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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: "Ακύρωση",
|
|
@@ -102,14 +107,39 @@ export const staticResources = {
|
|
|
102
107
|
tr: "We have received your request. "
|
|
103
108
|
},
|
|
104
109
|
fileUploaded : {
|
|
105
|
-
en: "File
|
|
106
|
-
el: "Το αρχείο
|
|
107
|
-
tr: "File
|
|
110
|
+
en: "File uploaded",
|
|
111
|
+
el: "Το αρχείο ανεβάστηκε",
|
|
112
|
+
tr: "File uploaded"
|
|
108
113
|
},
|
|
109
114
|
fileNotUploaded : {
|
|
110
115
|
en: "File has not been uploaded. ",
|
|
111
116
|
el: "Το αρχείο δεν ανεβάστηκε. ",
|
|
112
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"
|
|
113
143
|
}
|
|
114
144
|
},
|
|
115
145
|
//remderer sections
|
|
@@ -123,9 +153,9 @@ export const staticResources = {
|
|
|
123
153
|
element: "htmlElement",
|
|
124
154
|
params: {
|
|
125
155
|
text: {
|
|
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>`
|
|
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>`
|
|
129
159
|
}
|
|
130
160
|
}
|
|
131
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,8 +18,10 @@ 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", fileInputElements = null) {
|
|
24
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
|
|
22
25
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
23
26
|
const isRootCall = !fileInputElements;
|
|
24
27
|
if (isRootCall) {
|
|
@@ -76,7 +79,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
76
79
|
element.params.visuallyHiddenText = element.params.label;
|
|
77
80
|
// TODO: Also need to set the `view` and `download` URLs
|
|
78
81
|
element.params.viewHref = "#viewHref";
|
|
79
|
-
element.params.deleteHref =
|
|
82
|
+
element.params.deleteHref = `/${siteId}/${pageUrl}/${fieldName}/delete-file${(routeParam) ? `?route=${routeParam}` : ''}`;
|
|
80
83
|
} else {
|
|
81
84
|
// TODO: Ask Andreas how to handle empty file inputs
|
|
82
85
|
element.params.value = "";
|
|
@@ -107,7 +110,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
107
110
|
if (element.element === "radios" && element.params.items) {
|
|
108
111
|
element.params.items.forEach(item => {
|
|
109
112
|
if (item.conditionalElements) {
|
|
110
|
-
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements);
|
|
113
|
+
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
|
|
111
114
|
|
|
112
115
|
// Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
|
|
113
116
|
if (item.conditionalElements.some(condEl => condEl.params?.error)) {
|
|
@@ -234,11 +234,12 @@ function containsFileInput(elements = [], targetName) {
|
|
|
234
234
|
for (const el of elements) {
|
|
235
235
|
// ✅ Direct file input match
|
|
236
236
|
if (el.element === 'fileInput' && el.params?.name === targetName) {
|
|
237
|
-
return
|
|
237
|
+
return el;
|
|
238
238
|
}
|
|
239
239
|
// 🔁 Recurse into nested elements (e.g. groups, conditionals)
|
|
240
240
|
if (Array.isArray(el?.params?.elements)) {
|
|
241
|
-
|
|
241
|
+
const nestedMatch = containsFileInput(el.params.elements, targetName);
|
|
242
|
+
if (nestedMatch) return nestedMatch; // ← propagate the found element
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
// 🎯 Special case: conditional radios/checkboxes
|
|
@@ -248,7 +249,8 @@ function containsFileInput(elements = [], targetName) {
|
|
|
248
249
|
) {
|
|
249
250
|
for (const item of el.params.items) {
|
|
250
251
|
if (Array.isArray(item?.conditionalElements)) {
|
|
251
|
-
|
|
252
|
+
const match = containsFileInput(item.conditionalElements, targetName);
|
|
253
|
+
if (match) return match; // ← propagate the found element
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
}
|
|
@@ -264,16 +266,24 @@ function containsFileInput(elements = [], targetName) {
|
|
|
264
266
|
* @param {string} elementName The name of the element to check
|
|
265
267
|
* @return {boolean} True if a fileInput exists, false otherwise
|
|
266
268
|
*/
|
|
267
|
-
function pageContainsFileInput(pageTemplate, elementName) {
|
|
269
|
+
export function pageContainsFileInput(pageTemplate, elementName) {
|
|
268
270
|
const sections = pageTemplate?.sections || [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
275
284
|
}
|
|
276
285
|
|
|
286
|
+
|
|
277
287
|
/**
|
|
278
288
|
* Validates magic bytes against expected mimetype
|
|
279
289
|
* @param {Buffer} buffer
|