-
Bjarke Madsen authoredBjarke Madsen authored
SurveyContainerComponent.tsx 12.90 KiB
import { useEffect, useState, useCallback, useContext } from "react";
import { Container } from "react-bootstrap";
import toast, { Toaster } from "react-hot-toast";
import { Model, Serializer } from "survey-core";
import { useParams } from "react-router-dom";
import SurveyComponent from "./SurveyComponent";
import SurveyNavigationComponent from "./SurveyNavigationComponent";
import { VerificationStatus } from './Schema';
import Prompt from "./Prompt";
import "survey-core/modern.min.css";
import './survey.scss';
import useMatomo from "../matomo/UseMatomo";
import { FunctionFactory } from "survey-core";
import { validationFunctions } from "./validation/validation";
import SurveySidebar from "./management/SurveySidebar";
import { userContext } from "../providers/UserProvider";
interface ValidationQuestion {
name?: string;
value?: string | number | null;
data?: ValidationQuestion;
}
// Overrides for questions that need to be validated differently from the default expression in their group
const questionOverrides = {
data_protection_contact: (..._args) => true, // don't validate the contact field, anything goes..
}
function oldValidateWebsiteUrl(params) {
let value = params[0];
if ((value == undefined || value == null || value == '')) {
return true;
}
try {
value = value.trim();
if (value.includes(" ")) {
return false;
}
// if there's not a protocol, add one for the test
if (!value.includes(":/")) {
value = "https://" + value;
}
const url = new URL(value);
return !!url
} catch {
return false;
}
}
function validateQuestion(this: { question: ValidationQuestion, row?}, params) {
try {
const question = this.question;
const validator = params[0] || undefined;
const matrix = question.data && 'name' in question.data;
let questionName;
if (matrix) {
questionName = question.data!.name;
} else {
questionName = question.name;
}
const value = question.value
const hasOverride = questionOverrides[questionName];
if (hasOverride) {
return hasOverride(value, ...params.slice(1));
}
const validationFunction = validationFunctions[validator];
if (!validationFunction) {
throw new Error(`Validation function ${validator} not found for question ${questionName}`);
}
return validationFunction(value, ...params.slice(1));
} catch (e) {
console.error(e);
return false;
}
}
Serializer.addProperty("itemvalue", "customDescription:text");
Serializer.addProperty("question", "hideCheckboxLabels:boolean");
function SurveyContainerComponent({ loadFrom }) {
const [surveyModel, setSurveyModel] = useState<Model>(); // note that this is never updated and we abuse that fact by adding extra state to the surveyModel
const { year, nren } = useParams(); // nren stays empty for inspect and try
const [error, setError] = useState<string>('loading survey...');
const { user } = useContext(userContext);
const loggedIn = !!user.id;
const isAdmin = loggedIn ? user.permissions.admin : false;
if (!FunctionFactory.Instance.hasFunction("validateQuestion")) {
FunctionFactory.Instance.register("validateQuestion", validateQuestion);
}
if (!FunctionFactory.Instance.hasFunction("validateWebsiteUrl")) {
FunctionFactory.Instance.register("validateWebsiteUrl", oldValidateWebsiteUrl);
}
const { trackPageView } = useMatomo();
const beforeUnloadListener = useCallback((event) => {
event.preventDefault();
return (event.returnValue = "");
}, []);
const pageHideListener = useCallback(() => {
window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const onPageExitThroughRouter = useCallback(() => {
window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren);
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
async function getModel() {
const response = await fetch(loadFrom + year + (nren ? '/' + nren : ''))
const json = await response.json();
if (!response.ok) {
if ('message' in json) {
throw new Error(json.message);
} else {
throw new Error(`Request failed with status ${response.status}`);
}
}
const survey = new Model(json['model']);
survey.setVariable('surveyyear', year);
survey.setVariable('previousyear', parseInt(year!) - 1);
survey.showNavigationButtons = false;
survey.requiredText = '';
survey['verificationStatus'] = new Map<string, VerificationStatus>();
for (const questionName in json["verification_status"]) {
survey['verificationStatus'].set(questionName, json["verification_status"][questionName]);
}
survey.data = json['data'];
survey.clearIncorrectValues(true);
survey.currentPageNo = json['page'];
survey.mode = json['mode'];
survey['lockedBy'] = json['locked_by'];
survey['status'] = json['status'];
survey['editAllowed'] = json['edit_allowed'];
setSurveyModel(survey);
}
getModel().catch(error => setError('Error when loading survey: ' + error.message)).then(() => {
trackPageView({ documentTitle: `Survey for ${nren} (${year})` });
})
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (!surveyModel) {
return error;
}
const saveSurveyData = async (survey, newState) => {
if (!nren) {
return "Saving not available in inpect/try mode";
}
const saveData = {
lock_uuid: survey.lockUUID,
new_state: newState,
data: survey.data,
page: survey.currentPageNo,
verification_status: Object.fromEntries(survey.verificationStatus)
};
try {
const response = await fetch(
'/api/response/save/' + year + '/' + nren,
{ method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify(saveData) }
);
const json = await response.json();
if (!response.ok) {
return json['message'];
}
surveyModel.mode = json['mode'];
surveyModel['lockedBy'] = json['locked_by'];
surveyModel['status'] = json['status'];
} catch (e) {
return "Unknown Error: " + (e as Error).message;
}
}
const validateWithAnswerVerification = (validatorFunction, validateStatus = true) => {
let firstValidationError = '';
const verificationValidator = (survey, options) => {
const status = survey.verificationStatus.get(options.name);
if (status == VerificationStatus.Unverified) {
if (firstValidationError == '') {
firstValidationError = options.name;
}
options.error = 'Please verify that last years data is correct by editing the answer or pressing the "No change from previous year" button!';
}
};
if (validateStatus) surveyModel.onValidateQuestion.add(verificationValidator);
const validSurvey = validatorFunction();
if (validateStatus) surveyModel.onValidateQuestion.remove(verificationValidator);
if (!validSurvey) {
toast("Validation failed!");
}
return validSurvey;
}
const surveyActions = {
'save': async () => {
const allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
if (!allFieldsValid) {
toast("Please correct the invalid fields before saving!");
return;
}
const errorMessage = await saveSurveyData(surveyModel, "editing");
if (errorMessage) {
toast("Failed saving survey: " + errorMessage);
} else {
toast("Survey saved!");
}
},
'complete': async () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true));
if (validSurvey) {
const errorMessage = await saveSurveyData(surveyModel, "completed");
if (errorMessage) {
toast("Failed completing survey: " + errorMessage);
} else {
toast("Survey completed!");
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}
}
},
'saveAndStopEdit': async () => {
const allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
if (!allFieldsValid) {
toast("Please correct the invalid fields before saving.");
return;
}
const errorMessage = await saveSurveyData(surveyModel, "readonly");
if (errorMessage) {
toast("Failed saving survey: " + errorMessage);
} else {
toast("Survey saved!");
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
}
},
'startEdit': async () => {
const response = await fetch('/api/response/lock/' + year + '/' + nren, { method: "POST" });
const json = await response.json();
if (!response.ok) {
toast("Failed starting edit: " + json['message']);
return;
}
addEventListener("pagehide", pageHideListener);
addEventListener("beforeunload", beforeUnloadListener, { capture: true });
for (const questionName in json["verification_status"]) {
surveyModel['verificationStatus'].set(questionName, json["verification_status"][questionName]);
}
surveyModel.data = json['data'];
surveyModel.clearIncorrectValues(true);
surveyModel.mode = json['mode'];
surveyModel['lockedBy'] = json['locked_by']
surveyModel['lockUUID'] = json['lock_uuid'];
surveyModel['status'] = json['status'];
// Validate when we start editing to ensure invalid fields are corrected by the user
const allFieldsValid = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true), false);
if (!allFieldsValid) {
toast("Some fields are invalid, please correct them.");
return;
}
},
'releaseLock': async () => {
const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' });
const json = await response.json();
if (!response.ok) {
toast("Failed releasing lock: " + json['message']);
return;
}
surveyModel.mode = json['mode'];
surveyModel['lockedBy'] = json['locked_by'];
surveyModel['status'] = json['status'];
},
'validatePage': () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validatePage.bind(surveyModel));
if (validSurvey) {
toast("Page validation successful!");
}
}
}
if (!surveyModel.css.question.title.includes("sv-header-flex")) {
surveyModel.css.question.title = "sv-title sv-question__title sv-header-flex";
surveyModel.css.question.titleOnError = "sv-question__title--error sv-error-color-fix";
}
const onPageChange = (page) => {
surveyModel.currentPageNo = page;
}
return (
<>
{isAdmin ? <SurveySidebar /> : null}
<Container className="survey-container">
<Toaster />
<Prompt message="Are you sure you want to leave this page? Information you've entered may not be saved." when={() => { return surveyModel.mode == 'edit' && !!nren; }} onPageExit={onPageExitThroughRouter} />
<SurveyNavigationComponent onPageChange={onPageChange} surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}>
<SurveyComponent surveyModel={surveyModel} />
</SurveyNavigationComponent>
</Container>
</>
);
}
export default SurveyContainerComponent;