@gov-cy/govcy-express-services 1.0.0-alpha.7 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.mjs +7 -0
- package/src/middleware/govcyDeleteFileHandler.mjs +234 -0
- package/src/middleware/govcyPageHandler.mjs +2 -1
- package/src/middleware/govcyUpload.mjs +1 -1
- package/src/public/js/govcyFiles.js +188 -144
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +43 -3
- package/src/utils/govcyFormHandling.mjs +25 -11
- package/src/utils/govcyHandleFiles.mjs +36 -11
- package/src/utils/govcySubmitData.mjs +27 -0
- package/src/utils/govcyValidator.mjs +6 -3
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
|
+
|
|
@@ -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.
|
|
31
|
+
return res.status(result.status).json(errorResponse(result.dataStatus, result.errorMessage || 'File upload failed'));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
return res.json(successResponse(result.data));
|
|
@@ -1,152 +1,196 @@
|
|
|
1
1
|
// 🔍 Select all file inputs that have the .govcy-file-upload class
|
|
2
|
-
|
|
2
|
+
var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
|
|
3
3
|
|
|
4
4
|
// 🔁 Loop over each file input and attach a change event listener
|
|
5
|
-
fileInputs.forEach(input
|
|
6
|
-
input.addEventListener('change',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (outerElement) {
|
|
86
|
-
//remove all classes from outerElement
|
|
87
|
-
outerElement.className = "";
|
|
88
|
-
//set the id of the outerElement to `${elementName}-outer-control`
|
|
89
|
-
outerElement.id = `${elementName}-outer-control`;
|
|
90
|
-
//update DOM and initialize the JS components
|
|
91
|
-
renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
|
|
92
|
-
}
|
|
93
|
-
// ✅ Update ARIA live region with success message
|
|
94
|
-
const statusRegion = document.getElementById('_govcy-upload-status');
|
|
95
|
-
if (statusRegion) {
|
|
96
|
-
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
97
|
-
setTimeout(() => {
|
|
98
|
-
statusRegion.textContent = '';
|
|
99
|
-
}, 10000);
|
|
100
|
-
}
|
|
101
|
-
// alert('✅ File uploaded successfully');
|
|
102
|
-
|
|
103
|
-
} catch (err) {
|
|
104
|
-
// Create an instance of GovcyFrontendRendererBrowser
|
|
105
|
-
const renderer = new GovcyFrontendRendererBrowser();
|
|
106
|
-
const lang = window._govcyLang || "el";
|
|
107
|
-
// Define the input data
|
|
108
|
-
const inputData =
|
|
109
|
-
{
|
|
110
|
-
"site": {
|
|
111
|
-
"lang": lang
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
const fileInputMap = window._govcyFileInputs || {};
|
|
115
|
-
let fileElement = fileInputMap[elementName];
|
|
116
|
-
fileElement.element = "fileInput";
|
|
117
|
-
fileElement.params.fileId = "";
|
|
118
|
-
fileElement.params.sha256 = ""
|
|
119
|
-
fileElement.params.error = messages.uploadFailed;
|
|
120
|
-
|
|
121
|
-
// Construct the JSONTemplate
|
|
122
|
-
const JSONTemplate = {
|
|
123
|
-
"elements": [fileElement]
|
|
124
|
-
};
|
|
125
|
-
//render HTML into string
|
|
126
|
-
let renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
127
|
-
var outerElement = document.getElementById(`${elementName}-outer-control`)
|
|
128
|
-
|| document.getElementById(`${elementName}-input-control`)
|
|
129
|
-
|| document.getElementById(`${elementName}-view-control`);
|
|
130
|
-
|
|
131
|
-
if (outerElement) {
|
|
132
|
-
//remove all classes from outerElement
|
|
133
|
-
outerElement.className = "";
|
|
134
|
-
//set the id of the outerElement to `${elementName}-outer-control`
|
|
135
|
-
outerElement.id = `${elementName}-outer-control`;
|
|
136
|
-
//update DOM and initialize the JS components
|
|
137
|
-
renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
|
|
138
|
-
//TODO: Kamran need to figure a way to re register the DOM event on change
|
|
5
|
+
fileInputs.forEach(function(input) {
|
|
6
|
+
input.addEventListener('change', _uploadFileEventHandler);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles the upload of a file event
|
|
12
|
+
*
|
|
13
|
+
* @param {object} event The event
|
|
14
|
+
*/
|
|
15
|
+
function _uploadFileEventHandler(event) {
|
|
16
|
+
var input = event.target;
|
|
17
|
+
var messages = {
|
|
18
|
+
"uploadSuccesful": {
|
|
19
|
+
"el": "Το αρχείο ανεβαστηκε",
|
|
20
|
+
"en": "File uploaded successfully",
|
|
21
|
+
"tr": "File uploaded successfully"
|
|
22
|
+
},
|
|
23
|
+
"uploadFailed": {
|
|
24
|
+
"el": "Αποτυχια ανεβασης",
|
|
25
|
+
"en": "File upload failed",
|
|
26
|
+
"tr": "File upload failed"
|
|
27
|
+
},
|
|
28
|
+
"uploadFailed406": {
|
|
29
|
+
"el": "Το επιλεγμένο αρχείο είναι κενό",
|
|
30
|
+
"en": "The selected file is empty",
|
|
31
|
+
"tr": "The selected file is empty"
|
|
32
|
+
},
|
|
33
|
+
"uploadFailed407": {
|
|
34
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
35
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
36
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
37
|
+
},
|
|
38
|
+
"uploadFailed408": {
|
|
39
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
40
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
41
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
42
|
+
},
|
|
43
|
+
"uploadFailed409": {
|
|
44
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 5MB",
|
|
45
|
+
"en": "The selected file must be smaller than 5MB",
|
|
46
|
+
"tr": "The selected file must be smaller than 5MB"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
|
|
51
|
+
var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
|
|
52
|
+
var csrfToken = csrfEl ? csrfEl.value : '';
|
|
53
|
+
|
|
54
|
+
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
55
|
+
var siteId = window._govcySiteId || "";
|
|
56
|
+
var pageUrl = window._govcyPageUrl || "";
|
|
57
|
+
var lang = window._govcyLang || "el";
|
|
58
|
+
|
|
59
|
+
// 📦 Grab the selected file
|
|
60
|
+
var file = event.target.files[0];
|
|
61
|
+
var elementName = input.name; // Form field's `name` attribute
|
|
62
|
+
var elementId = input.id; // Form field's `id` attribute
|
|
63
|
+
|
|
64
|
+
if (!file) return; // Exit if no file was selected
|
|
65
|
+
|
|
66
|
+
// 🧵 Prepare form-data payload for the API
|
|
67
|
+
var formData = new FormData();
|
|
68
|
+
formData.append('file', file); // Attach the actual file
|
|
69
|
+
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
70
|
+
|
|
71
|
+
// 🚀 CHANGED: using fetch instead of axios.post
|
|
72
|
+
fetch(`/apis/${siteId}/${pageUrl}/upload`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
|
|
76
|
+
},
|
|
77
|
+
body: formData
|
|
78
|
+
})
|
|
79
|
+
.then(function(response) {
|
|
80
|
+
// 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return response.json().then(function(errData) {
|
|
83
|
+
throw { response: { data: errData } };
|
|
84
|
+
});
|
|
139
85
|
}
|
|
140
|
-
|
|
141
|
-
|
|
86
|
+
return response.json();
|
|
87
|
+
})
|
|
88
|
+
.then(function(data) {
|
|
89
|
+
// ✅ Success response
|
|
90
|
+
var sha256 = data.Data.sha256;
|
|
91
|
+
var fileId = data.Data.fileId;
|
|
92
|
+
|
|
93
|
+
// 📝 Store returned metadata in hidden fields if needed
|
|
94
|
+
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
|
|
95
|
+
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
|
|
96
|
+
|
|
97
|
+
// Render the file view
|
|
98
|
+
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
|
|
99
|
+
|
|
100
|
+
// Accessibility: Update ARIA live region with success message
|
|
101
|
+
var statusRegion = document.getElementById('_govcy-upload-status');
|
|
142
102
|
if (statusRegion) {
|
|
143
|
-
|
|
144
|
-
|
|
103
|
+
setTimeout(function() {
|
|
104
|
+
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
105
|
+
}, 200);
|
|
106
|
+
setTimeout(function() {
|
|
145
107
|
statusRegion.textContent = '';
|
|
146
|
-
},
|
|
108
|
+
}, 5000);
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.catch(function(err) {
|
|
112
|
+
// ⚠️ Show an error message if upload fails
|
|
113
|
+
var errorMessage = messages.uploadFailed;
|
|
114
|
+
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
|
|
115
|
+
|
|
116
|
+
if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
|
|
117
|
+
errorMessage = messages["uploadFailed" + errorCode];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Render the file input with error
|
|
121
|
+
_renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
|
|
122
|
+
|
|
123
|
+
// Re-bind the file input's change handler
|
|
124
|
+
var newInput = document.getElementById(elementId);
|
|
125
|
+
if (newInput) {
|
|
126
|
+
newInput.addEventListener('change', _uploadFileEventHandler);
|
|
147
127
|
}
|
|
148
|
-
|
|
149
|
-
//
|
|
128
|
+
|
|
129
|
+
// Accessibility: Focus on the form field
|
|
130
|
+
document.getElementById(elementId)?.focus();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Renders a file element in the DOM
|
|
137
|
+
*
|
|
138
|
+
* @param {string} elementState The element state. Can be "fileInput" or "fileView"
|
|
139
|
+
* @param {string} elementId The element id
|
|
140
|
+
* @param {string} elementName The element name
|
|
141
|
+
* @param {string} fileId The file id
|
|
142
|
+
* @param {string} sha256 The sha256
|
|
143
|
+
* @param {object} errorMessage The error message in all supported languages
|
|
144
|
+
*/
|
|
145
|
+
function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
|
|
146
|
+
|
|
147
|
+
// Grab the query string part (?foo=bar&route=something)
|
|
148
|
+
var queryString = window.location.search;
|
|
149
|
+
// Parse it
|
|
150
|
+
var params = new URLSearchParams(queryString);
|
|
151
|
+
// Get the "route" value (null if not present)
|
|
152
|
+
var route = params.get("route");
|
|
153
|
+
|
|
154
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
155
|
+
var renderer = new GovcyFrontendRendererBrowser();
|
|
156
|
+
var lang = window._govcyLang || "el";
|
|
157
|
+
// Define the input data
|
|
158
|
+
var inputData =
|
|
159
|
+
{
|
|
160
|
+
"site": {
|
|
161
|
+
"lang": lang
|
|
150
162
|
}
|
|
151
|
-
}
|
|
152
|
-
|
|
163
|
+
};
|
|
164
|
+
var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
|
|
165
|
+
var fileElement = fileInputMap[elementName];
|
|
166
|
+
fileElement.element = elementState;
|
|
167
|
+
if (errorMessage != null) fileElement.params.error = errorMessage;
|
|
168
|
+
if (fileId != null) fileElement.params.fileId = fileId;
|
|
169
|
+
if (sha256 != null) fileElement.params.sha256 = sha256;
|
|
170
|
+
if (elementState == "fileView") {
|
|
171
|
+
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
172
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
173
|
+
fileElement.params.viewHref = "#viewHref";
|
|
174
|
+
fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/" + elementName + "/delete-file"
|
|
175
|
+
+ (route !== null ? "?route=" + encodeURIComponent(route) : "");
|
|
176
|
+
}
|
|
177
|
+
// Construct the JSONTemplate
|
|
178
|
+
var JSONTemplate = {
|
|
179
|
+
"elements": [fileElement]
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
//render HTML into string
|
|
183
|
+
var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
184
|
+
var outerElement = document.getElementById(`${elementId}-outer-control`)
|
|
185
|
+
|| document.getElementById(`${elementId}-input-control`)
|
|
186
|
+
|| document.getElementById(`${elementId}-view-control`);
|
|
187
|
+
|
|
188
|
+
if (outerElement) {
|
|
189
|
+
//remove all classes from outerElement
|
|
190
|
+
outerElement.className = "";
|
|
191
|
+
//set the id of the outerElement to `${elementId}-outer-control`
|
|
192
|
+
outerElement.id = `${elementId}-outer-control`;
|
|
193
|
+
//update DOM and initialize the JS components
|
|
194
|
+
renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", function () {
|
|
2
2
|
// --- Show conditionals for checked radios ---
|
|
3
|
-
|
|
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: "Ακύρωση",
|
|
@@ -100,6 +105,41 @@ export const staticResources = {
|
|
|
100
105
|
en: "We have received your request. ",
|
|
101
106
|
el: "Έχουμε λάβει την αίτησή σας. ",
|
|
102
107
|
tr: "We have received your request. "
|
|
108
|
+
},
|
|
109
|
+
fileUploaded : {
|
|
110
|
+
en: "File uploaded",
|
|
111
|
+
el: "Το αρχείο ανεβάστηκε",
|
|
112
|
+
tr: "File uploaded"
|
|
113
|
+
},
|
|
114
|
+
fileNotUploaded : {
|
|
115
|
+
en: "File has not been uploaded. ",
|
|
116
|
+
el: "Το αρχείο δεν ανεβάστηκε. ",
|
|
117
|
+
tr: "File has not been uploaded. "
|
|
118
|
+
},
|
|
119
|
+
fileYouHaveUploaded : {
|
|
120
|
+
en: "You have uploaded the file for \"{{file}}\"",
|
|
121
|
+
el: "Έχετε ανεβάσει το αρχείο \"{{file}}\"",
|
|
122
|
+
tr: "You have uploaded the file for \"{{file}}\""
|
|
123
|
+
},
|
|
124
|
+
deleteFileTitle : {
|
|
125
|
+
en: "Are you sure you want to delete the file \"{{file}}\"? ",
|
|
126
|
+
el: "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
|
|
127
|
+
tr: "Are you sure you want to delete the file \"{{file}}\"? "
|
|
128
|
+
},
|
|
129
|
+
deleteYesOption: {
|
|
130
|
+
el:"Ναι, θέλω να διαγράψω το αρχείο",
|
|
131
|
+
en:"Yes, I want to delete this file",
|
|
132
|
+
tr:"Yes, I want to delete this file"
|
|
133
|
+
},
|
|
134
|
+
deleteNoOption: {
|
|
135
|
+
el:"Όχι, δεν θέλω να διαγράψω το αρχείο",
|
|
136
|
+
en:"No, I don't want to delete this file",
|
|
137
|
+
tr:"No, I don't want to delete this file"
|
|
138
|
+
},
|
|
139
|
+
deleteFileValidationError: {
|
|
140
|
+
en: "Select if you want to delete the file",
|
|
141
|
+
el: "Επιλέξτε αν θέλετε να διαγράψετε το αρχείο",
|
|
142
|
+
tr: "Select if you want to delete the filez"
|
|
103
143
|
}
|
|
104
144
|
},
|
|
105
145
|
//remderer sections
|
|
@@ -113,9 +153,9 @@ export const staticResources = {
|
|
|
113
153
|
element: "htmlElement",
|
|
114
154
|
params: {
|
|
115
155
|
text: {
|
|
116
|
-
en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
117
|
-
el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
118
|
-
tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`
|
|
156
|
+
en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
157
|
+
el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
158
|
+
tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
|
|
119
159
|
}
|
|
120
160
|
}
|
|
121
161
|
},
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
8
8
|
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
9
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -17,10 +18,15 @@ import * as dataLayer from "./govcyDataLayer.mjs";
|
|
|
17
18
|
* @param {string} siteId The site ID
|
|
18
19
|
* @param {string} pageUrl The page URL
|
|
19
20
|
* @param {string} lang The language
|
|
21
|
+
* @param {Object} fileInputElements The file input elements
|
|
22
|
+
* @param {string} routeParam The route parameter
|
|
20
23
|
*/
|
|
21
|
-
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el") {
|
|
24
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
|
|
22
25
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
23
|
-
|
|
26
|
+
const isRootCall = !fileInputElements;
|
|
27
|
+
if (isRootCall) {
|
|
28
|
+
fileInputElements = {};
|
|
29
|
+
}
|
|
24
30
|
// Recursively populate form data with session data
|
|
25
31
|
formElements.forEach(element => {
|
|
26
32
|
if (inputElements.includes(element.element)) {
|
|
@@ -62,7 +68,9 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
62
68
|
} else if (element.element === "fileInput") {
|
|
63
69
|
// For fileInput, we change the element.element to "fileView" and set the
|
|
64
70
|
// fileId and sha256 from the session store
|
|
65
|
-
|
|
71
|
+
// unneeded handle of `Attachment` at the end
|
|
72
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
|
|
73
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
|
|
66
74
|
// TODO: Ask Andreas how to handle empty file inputs
|
|
67
75
|
if (fileData) {
|
|
68
76
|
element.element = "fileView";
|
|
@@ -71,7 +79,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
71
79
|
element.params.visuallyHiddenText = element.params.label;
|
|
72
80
|
// TODO: Also need to set the `view` and `download` URLs
|
|
73
81
|
element.params.viewHref = "#viewHref";
|
|
74
|
-
element.params.deleteHref =
|
|
82
|
+
element.params.deleteHref = `/${siteId}/${pageUrl}/${fieldName}/delete-file${(routeParam) ? `?route=${routeParam}` : ''}`;
|
|
75
83
|
} else {
|
|
76
84
|
// TODO: Ask Andreas how to handle empty file inputs
|
|
77
85
|
element.params.value = "";
|
|
@@ -102,7 +110,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
102
110
|
if (element.element === "radios" && element.params.items) {
|
|
103
111
|
element.params.items.forEach(item => {
|
|
104
112
|
if (item.conditionalElements) {
|
|
105
|
-
populateFormData(item.conditionalElements, theData, validationErrors);
|
|
113
|
+
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
|
|
106
114
|
|
|
107
115
|
// Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
|
|
108
116
|
if (item.conditionalElements.some(condEl => condEl.params?.error)) {
|
|
@@ -113,7 +121,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
113
121
|
}
|
|
114
122
|
});
|
|
115
123
|
// add file input elements's definition in js object
|
|
116
|
-
if (fileInputElements
|
|
124
|
+
if (isRootCall && Object.keys(fileInputElements).length > 0) {
|
|
117
125
|
const scriptTag = `
|
|
118
126
|
<script type="text/javascript">
|
|
119
127
|
window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
|
|
@@ -121,7 +129,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
121
129
|
window._govcyPageUrl = "${pageUrl}";
|
|
122
130
|
window._govcyLang = "${lang}";
|
|
123
131
|
</script>
|
|
124
|
-
<div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="
|
|
132
|
+
<div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="assertive"></div>
|
|
125
133
|
<div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
|
|
126
134
|
`.trim();
|
|
127
135
|
formElements.push({
|
|
@@ -165,7 +173,7 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
|
|
|
165
173
|
if (item.conditionalElements) {
|
|
166
174
|
Object.assign(
|
|
167
175
|
filteredData,
|
|
168
|
-
getFormData(item.conditionalElements, formData)
|
|
176
|
+
getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
|
|
169
177
|
);
|
|
170
178
|
}
|
|
171
179
|
});
|
|
@@ -184,12 +192,18 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
|
|
|
184
192
|
} else if (element.element === "fileInput") {
|
|
185
193
|
// fileInput elements are already stored in the store when it was uploaded
|
|
186
194
|
// so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
|
|
187
|
-
|
|
195
|
+
// unneeded handle of `Attachment` at the end
|
|
196
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
|
|
197
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
|
|
188
198
|
if (fileData) {
|
|
189
|
-
|
|
199
|
+
// unneeded handle of `Attachment` at the end
|
|
200
|
+
// filteredData[name + "Attachment"] = fileData;
|
|
201
|
+
filteredData[name] = fileData;
|
|
190
202
|
} else {
|
|
191
203
|
//TODO: Ask Andreas how to handle empty file inputs
|
|
192
|
-
|
|
204
|
+
// unneeded handle of `Attachment` at the end
|
|
205
|
+
// filteredData[name + "Attachment"] = ""; // or handle as needed
|
|
206
|
+
filteredData[name] = ""; // or handle as needed
|
|
193
207
|
}
|
|
194
208
|
// Handle other elements (e.g., textInput, textArea, datePicker)
|
|
195
209
|
} else {
|
|
@@ -27,6 +27,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
27
27
|
if (!file || !elementName) {
|
|
28
28
|
return {
|
|
29
29
|
status: 400,
|
|
30
|
+
dataStatus: 400,
|
|
30
31
|
errorMessage: 'Missing file or element name'
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -37,6 +38,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
37
38
|
if (!uploadCfg?.url || !uploadCfg?.clientKey || !uploadCfg?.serviceId) {
|
|
38
39
|
return {
|
|
39
40
|
status: 400,
|
|
41
|
+
dataStatus: 401,
|
|
40
42
|
errorMessage: 'Missing upload configuration'
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -53,6 +55,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
53
55
|
if (!url || !clientKey) {
|
|
54
56
|
return {
|
|
55
57
|
status: 400,
|
|
58
|
+
dataStatus: 402,
|
|
56
59
|
errorMessage: 'Missing environment variables for upload'
|
|
57
60
|
};
|
|
58
61
|
}
|
|
@@ -67,6 +70,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
67
70
|
if (!page?.pageTemplate) {
|
|
68
71
|
return {
|
|
69
72
|
status: 400,
|
|
73
|
+
dataStatus: 403,
|
|
70
74
|
errorMessage: 'Invalid page configuration'
|
|
71
75
|
};
|
|
72
76
|
}
|
|
@@ -77,6 +81,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
77
81
|
if (conditionResult.result === false) {
|
|
78
82
|
return {
|
|
79
83
|
status: 403,
|
|
84
|
+
dataStatus: 404,
|
|
80
85
|
errorMessage: 'This page is skipped by conditional logic'
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -88,6 +93,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
88
93
|
if (!isAllowed) {
|
|
89
94
|
return {
|
|
90
95
|
status: 403,
|
|
96
|
+
dataStatus: 405,
|
|
91
97
|
errorMessage: `File input [${elementName}] not allowed on this page`
|
|
92
98
|
};
|
|
93
99
|
}
|
|
@@ -96,6 +102,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
96
102
|
if (file.size === 0) {
|
|
97
103
|
return {
|
|
98
104
|
status: 400,
|
|
105
|
+
dataStatus: 406,
|
|
99
106
|
errorMessage: 'Uploaded file is empty'
|
|
100
107
|
};
|
|
101
108
|
}
|
|
@@ -105,6 +112,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
105
112
|
if (!ALLOWED_FILE_MIME_TYPES.includes(file.mimetype)) {
|
|
106
113
|
return {
|
|
107
114
|
status: 400,
|
|
115
|
+
dataStatus: 407,
|
|
108
116
|
errorMessage: 'Invalid file type (MIME not allowed)'
|
|
109
117
|
};
|
|
110
118
|
}
|
|
@@ -113,6 +121,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
113
121
|
if (!isMagicByteValid(file.buffer, file.mimetype)) {
|
|
114
122
|
return {
|
|
115
123
|
status: 400,
|
|
124
|
+
dataStatus: 408,
|
|
116
125
|
errorMessage: 'Invalid file type (magic byte mismatch)'
|
|
117
126
|
};
|
|
118
127
|
}
|
|
@@ -121,6 +130,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
121
130
|
if (file.size > ALLOWED_FILE_SIZE_MB * 1024 * 1024) {
|
|
122
131
|
return {
|
|
123
132
|
status: 400,
|
|
133
|
+
dataStatus: 409,
|
|
124
134
|
errorMessage: 'File exceeds allowed size'
|
|
125
135
|
};
|
|
126
136
|
}
|
|
@@ -161,6 +171,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
161
171
|
if (!response?.Succeeded) {
|
|
162
172
|
return {
|
|
163
173
|
status: 500,
|
|
174
|
+
dataStatus: 410,
|
|
164
175
|
errorMessage: `${response?.ErrorCode} - ${response?.ErrorMessage} - fileUploadAPIEndpoint returned succeeded false`
|
|
165
176
|
};
|
|
166
177
|
}
|
|
@@ -169,13 +180,16 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
169
180
|
if (!response?.Data?.fileId || !response?.Data?.sha256) {
|
|
170
181
|
return {
|
|
171
182
|
status: 500,
|
|
183
|
+
dataStatus: 411,
|
|
172
184
|
errorMessage: 'Missing fileId or sha256 in response'
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
// ✅ Success
|
|
177
189
|
// Store the file metadata in the session store
|
|
178
|
-
|
|
190
|
+
// unneeded handle of `Attachment` at the end
|
|
191
|
+
// dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
|
|
192
|
+
dataLayer.storePageDataElement(store, siteId, pageUrl, elementName, {
|
|
179
193
|
sha256: response.Data.sha256,
|
|
180
194
|
fileId: response.Data.fileId,
|
|
181
195
|
});
|
|
@@ -195,6 +209,7 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
195
209
|
} catch (err) {
|
|
196
210
|
return {
|
|
197
211
|
status: 500,
|
|
212
|
+
dataStatus: 500,
|
|
198
213
|
errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
|
|
199
214
|
};
|
|
200
215
|
}
|
|
@@ -219,11 +234,12 @@ function containsFileInput(elements = [], targetName) {
|
|
|
219
234
|
for (const el of elements) {
|
|
220
235
|
// ✅ Direct file input match
|
|
221
236
|
if (el.element === 'fileInput' && el.params?.name === targetName) {
|
|
222
|
-
return
|
|
237
|
+
return el;
|
|
223
238
|
}
|
|
224
239
|
// 🔁 Recurse into nested elements (e.g. groups, conditionals)
|
|
225
240
|
if (Array.isArray(el?.params?.elements)) {
|
|
226
|
-
|
|
241
|
+
const nestedMatch = containsFileInput(el.params.elements, targetName);
|
|
242
|
+
if (nestedMatch) return nestedMatch; // ← propagate the found element
|
|
227
243
|
}
|
|
228
244
|
|
|
229
245
|
// 🎯 Special case: conditional radios/checkboxes
|
|
@@ -233,7 +249,8 @@ function containsFileInput(elements = [], targetName) {
|
|
|
233
249
|
) {
|
|
234
250
|
for (const item of el.params.items) {
|
|
235
251
|
if (Array.isArray(item?.conditionalElements)) {
|
|
236
|
-
|
|
252
|
+
const match = containsFileInput(item.conditionalElements, targetName);
|
|
253
|
+
if (match) return match; // ← propagate the found element
|
|
237
254
|
}
|
|
238
255
|
}
|
|
239
256
|
}
|
|
@@ -249,16 +266,24 @@ function containsFileInput(elements = [], targetName) {
|
|
|
249
266
|
* @param {string} elementName The name of the element to check
|
|
250
267
|
* @return {boolean} True if a fileInput exists, false otherwise
|
|
251
268
|
*/
|
|
252
|
-
function pageContainsFileInput(pageTemplate, elementName) {
|
|
269
|
+
export function pageContainsFileInput(pageTemplate, elementName) {
|
|
253
270
|
const sections = pageTemplate?.sections || [];
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
|
|
272
|
+
for (const section of sections) {
|
|
273
|
+
for (const el of section?.elements || []) {
|
|
274
|
+
if (el.element === 'form') {
|
|
275
|
+
const match = containsFileInput(el.params?.elements, elementName);
|
|
276
|
+
if (match) {
|
|
277
|
+
return match; // ← return the actual element
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null; // no match found
|
|
260
284
|
}
|
|
261
285
|
|
|
286
|
+
|
|
262
287
|
/**
|
|
263
288
|
* Validates magic bytes against expected mimetype
|
|
264
289
|
* @param {Buffer} buffer
|
|
@@ -66,6 +66,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
66
66
|
// Store in submissionData
|
|
67
67
|
submissionData[pageUrl].formData[elId] = value;
|
|
68
68
|
|
|
69
|
+
// handle fileInput
|
|
70
|
+
if (elType === "fileInput") {
|
|
71
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
72
|
+
submissionData[pageUrl].formData[elId + "Attachment"] = value;
|
|
73
|
+
delete submissionData[pageUrl].formData[elId];
|
|
74
|
+
}
|
|
69
75
|
|
|
70
76
|
// 🔄 If radios with conditionalElements, walk ALL options
|
|
71
77
|
if (elType === "radios" && Array.isArray(element.params?.items)) {
|
|
@@ -85,6 +91,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
85
91
|
|
|
86
92
|
// Store even if the field was not visible to user
|
|
87
93
|
submissionData[pageUrl].formData[condId] = condValue;
|
|
94
|
+
// handle fileInput
|
|
95
|
+
if (condType === "fileInput") {
|
|
96
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
97
|
+
submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
|
|
98
|
+
delete submissionData[pageUrl].formData[condId];
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
}
|
|
@@ -291,6 +303,10 @@ function getValue(formElement, pageUrl, req, siteId) {
|
|
|
291
303
|
let value = ""
|
|
292
304
|
if (formElement.element === "dateInput") {
|
|
293
305
|
value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
|
|
306
|
+
} else if (formElement.element === "fileInput") {
|
|
307
|
+
// unneeded handle of `Attachment` at the end
|
|
308
|
+
// value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
|
|
309
|
+
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
294
310
|
} else {
|
|
295
311
|
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
296
312
|
}
|
|
@@ -340,6 +356,17 @@ function getValueLabel(formElement, value, pageUrl, req, siteId, service) {
|
|
|
340
356
|
return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
|
|
341
357
|
}
|
|
342
358
|
|
|
359
|
+
// handle fileInput
|
|
360
|
+
if (formElement.element === "fileInput") {
|
|
361
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
362
|
+
if (value) {
|
|
363
|
+
return govcyResources.staticResources.text.fileUploaded;
|
|
364
|
+
} else {
|
|
365
|
+
return govcyResources.getSameMultilingualObject(service.site.languages, "");
|
|
366
|
+
// return govcyResources.staticResources.text.fileNotUploaded;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
// textInput, textArea, etc.
|
|
344
371
|
return govcyResources.getSameMultilingualObject(service.site.languages, value);
|
|
345
372
|
}
|
|
@@ -263,8 +263,9 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
263
263
|
formData[`${field.params.name}_day`]]
|
|
264
264
|
.filter(Boolean) // Remove empty values
|
|
265
265
|
.join("-") // Join remaining parts
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
// unneeded handle of `Attachment` at the end
|
|
267
|
+
// : (field.element === "fileInput") // Handle fileInput
|
|
268
|
+
// ? formData[`${field.params.name}Attachment`] || ""
|
|
268
269
|
: formData[field.params.name] || ""; // Get submitted value
|
|
269
270
|
|
|
270
271
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
|
|
@@ -313,7 +314,9 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
313
314
|
.filter(Boolean) // Remove empty values
|
|
314
315
|
.join("-") // Join remaining parts
|
|
315
316
|
: (conditionalElement.element === "fileInput") // Handle fileInput
|
|
316
|
-
|
|
317
|
+
// unneeded handle of `Attachment` at the end
|
|
318
|
+
// ? formData[`${conditionalElement.params.name}Attachment`] || ""
|
|
319
|
+
? formData[`${conditionalElement.params.name}`] || ""
|
|
317
320
|
: formData[conditionalElement.params.name] || ""; // Get submitted value
|
|
318
321
|
|
|
319
322
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
|