Skip to content
Snippets Groups Projects
Commit 2a4df433 authored by Remco Tukker's avatar Remco Tukker
Browse files

clean up the surveycomponent and fix warning about editing state during render

parent bd5aa6a7
No related branches found
No related tags found
1 merge request!64useBlock in the survey frontend to ask users for confirmation
import React from "react"; import React, { useCallback } from "react";
import { Question, FunctionFactory } from "survey-core"; import { Question, FunctionFactory } from "survey-core";
import { Survey } from "survey-react-ui"; import { Survey } from "survey-react-ui";
import { VerificationStatus } from './Schema'; import { VerificationStatus } from './Schema';
function SurveyComponent({ surveyModel, verificationStatus }) {
function customDescriptionCallback(_, options) {
// get the customDescription for matrix rows and set it in the title
// attribute so that it shows up as a hover popup
if (options.column['indexValue'] == 0 && 'item' in options.row) {
const item = options.row['item'] as object;
if (item['customDescription'] !== undefined) {
options.htmlElement.parentElement?.children[0].setAttribute("title", item['customDescription']);
}
}
}
function validateWebsiteUrl(params) { function validateWebsiteUrl(params) {
const value = params[0]; const value = params[0];
...@@ -18,16 +28,16 @@ function SurveyComponent({ surveyModel, verificationStatus }) { ...@@ -18,16 +28,16 @@ function SurveyComponent({ surveyModel, verificationStatus }) {
} }
} }
surveyModel.css = { function hideCheckboxLabels(_, options) {
question: { if (options.question.hideCheckboxLabels) {
title: "sv-title sv-question__title sv-header-flex", const classes = options.cssClasses;
titleOnError: "sv-question__title--error sv-error-color-fix" classes.root += " hidden-checkbox-labels";
}
} }
};
function setVerifyButton(question: Question, state: VerificationStatus) { function setVerifyButton(question: Question, state: VerificationStatus, surveyModel) {
verificationStatus.current.set(question.name, state); surveyModel.verificationStatus.set(question.name, state);
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.type = "button"; btn.type = "button";
...@@ -42,7 +52,7 @@ function SurveyComponent({ surveyModel, verificationStatus }) { ...@@ -42,7 +52,7 @@ function SurveyComponent({ surveyModel, verificationStatus }) {
return; return;
} }
question.validate(); question.validate();
setVerifyButton(question, VerificationStatus.Verified); setVerifyButton(question, VerificationStatus.Verified, surveyModel);
} }
} else { } else {
btn.innerHTML = "Answer updated" btn.innerHTML = "Answer updated"
...@@ -60,42 +70,49 @@ function SurveyComponent({ surveyModel, verificationStatus }) { ...@@ -60,42 +70,49 @@ function SurveyComponent({ surveyModel, verificationStatus }) {
} }
} }
FunctionFactory.Instance.register("validateWebsiteUrl", validateWebsiteUrl);
surveyModel.onAfterRenderQuestion.add(function (survey, options) { function SurveyComponent({ surveyModel }) {
const status = verificationStatus.current.get(options.question.name);
const alwaysSetVerify = useCallback((_, options) => {
const status = surveyModel.verificationStatus.get(options.question.name);
if (status) { if (status) {
setVerifyButton(options.question, status); setVerifyButton(options.question, status, surveyModel);
} }
}); }, [surveyModel])
surveyModel.onValueChanged.add(function (survey, options) { const updateFromUnverified = useCallback((_, options) => {
const currentStatus = verificationStatus.current.get(options.question.name); const currentStatus = surveyModel.verificationStatus.get(options.question.name);
if (currentStatus == VerificationStatus.Unverified) { if (currentStatus == VerificationStatus.Unverified) {
setVerifyButton(options.question, VerificationStatus.Edited); setVerifyButton(options.question, VerificationStatus.Edited, surveyModel);
} }
}); }, [surveyModel])
surveyModel.onUpdateQuestionCssClasses.add(function (_, options) { if (!FunctionFactory.Instance.hasFunction("validateWebsiteUrl")) {
if (options.question.hideCheckboxLabels) { FunctionFactory.Instance.register("validateWebsiteUrl", validateWebsiteUrl);
const classes = options.cssClasses;
classes.root += " hidden-checkbox-labels";
} }
});
surveyModel.onMatrixAfterCellRender.add((survey, options) => { if (!surveyModel.css.question.title.includes("sv-header-flex")) {
// get the customDescription for matrix rows and set it in the title surveyModel.css.question.title = "sv-title sv-question__title sv-header-flex";
// attribute so that it shows up as a hover popup surveyModel.css.question.titleOnError = "sv-question__title--error sv-error-color-fix";
// NB I would have preferred using onAfterRenderQuestion, but unfortunately that is }
// not always triggered on re-renders (specifically when extra column become visble or invisible)
if (options.column['indexValue'] == 0 && 'item' in options.row) { if (!surveyModel.onAfterRenderQuestion.hasFunc(alwaysSetVerify)) {
const item = options.row['item'] as object; surveyModel.onAfterRenderQuestion.add(alwaysSetVerify);
if (item['customDescription'] !== undefined) {
options.htmlElement.parentElement?.children[0].setAttribute("title", item['customDescription']);
} }
if (!surveyModel.onValueChanged.hasFunc(updateFromUnverified)) {
surveyModel.onValueChanged.add(updateFromUnverified);
}
if (!surveyModel.onUpdateQuestionCssClasses.hasFunc(hideCheckboxLabels)) {
surveyModel.onUpdateQuestionCssClasses.add(hideCheckboxLabels);
}
if (!surveyModel.onMatrixAfterCellRender.hasFunc(customDescriptionCallback)) {
// NB I would have preferred using onAfterRenderQuestion, but unfortunately that is
// not always triggered on re-renders (specifically when extra column become visble or invisible)
surveyModel.onMatrixAfterCellRender.add(customDescriptionCallback);
} }
});
return <Survey model={surveyModel} /> return <Survey model={surveyModel} />
} }
... ...
......
import React, { useEffect, useRef, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { Container } from "react-bootstrap"; import { Container } from "react-bootstrap";
import toast, { Toaster } from "react-hot-toast"; import toast, { Toaster } from "react-hot-toast";
import { Model, Serializer } from "survey-core"; import { Model, Serializer } from "survey-core";
...@@ -15,14 +15,9 @@ Serializer.addProperty("question", "hideCheckboxLabels:boolean"); ...@@ -15,14 +15,9 @@ Serializer.addProperty("question", "hideCheckboxLabels:boolean");
function SurveyContainerComponent({ loadFrom }) { function SurveyContainerComponent({ loadFrom }) {
// NB This component should only rerender after the model is retrieved. const [surveyModel, setSurveyModel] = useState<Model>(); // note that this is never updated and we abuse that fact by adding extra state to the surveyModel
// If you change this to rerender more often, stuff will probably break. const { year, nren } = useParams(); // nren stays empty for inspect and try
const [surveyModel, setSurveyModel] = useState<Model>();
const verificationStatus = useRef<Map<string, VerificationStatus>>(new Map());
const { year, nren } = useParams(); // year is always set, nren stays empty for inspect and try
const [error, setError] = useState<string>('loading survey...'); const [error, setError] = useState<string>('loading survey...');
const lockUUID = useRef<string>();
const beforeUnloadListener = useCallback((event) => { const beforeUnloadListener = useCallback((event) => {
event.preventDefault(); event.preventDefault();
...@@ -53,17 +48,17 @@ function SurveyContainerComponent({ loadFrom }) { ...@@ -53,17 +48,17 @@ function SurveyContainerComponent({ loadFrom }) {
} }
} }
for (const questionName in json["verification_status"]) {
verificationStatus.current.set(questionName, json["verification_status"][questionName]);
}
const survey = new Model(json['model']); const survey = new Model(json['model']);
survey.setVariable('surveyyear', year); survey.setVariable('surveyyear', year);
survey.setVariable('previousyear', parseInt(year!) - 1); survey.setVariable('previousyear', parseInt(year!) - 1);
survey.showNavigationButtons = false; survey.showNavigationButtons = false;
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.data = json['data'];
survey.clearIncorrectValues(true); // TODO test if this really removes all old values and such survey.clearIncorrectValues(true); // TODO test if this really removes all old values and such
survey.currentPageNo = json['page']; survey.currentPageNo = json['page'];
...@@ -89,11 +84,11 @@ function SurveyContainerComponent({ loadFrom }) { ...@@ -89,11 +84,11 @@ function SurveyContainerComponent({ loadFrom }) {
} }
const saveData = { const saveData = {
lock_uuid: lockUUID.current, lock_uuid: survey.lockUUID,
new_state: newState, new_state: newState,
data: survey.data, data: survey.data,
page: survey.currentPageNo, page: survey.currentPageNo,
verification_status: Object.fromEntries(verificationStatus.current) verification_status: Object.fromEntries(survey.verificationStatus)
}; };
try { try {
...@@ -116,7 +111,7 @@ function SurveyContainerComponent({ loadFrom }) { ...@@ -116,7 +111,7 @@ function SurveyContainerComponent({ loadFrom }) {
const validateWithAnswerVerification = (validatorFunction) => { const validateWithAnswerVerification = (validatorFunction) => {
let firstValidationError = ''; let firstValidationError = '';
const verificationValidator = (survey, options) => { const verificationValidator = (survey, options) => {
const status = verificationStatus.current.get(options.name); const status = survey.verificationStatus.get(options.name);
if (status == VerificationStatus.Unverified) { if (status == VerificationStatus.Unverified) {
if (firstValidationError == '') { if (firstValidationError == '') {
firstValidationError = options.name; firstValidationError = options.name;
...@@ -180,8 +175,8 @@ function SurveyContainerComponent({ loadFrom }) { ...@@ -180,8 +175,8 @@ function SurveyContainerComponent({ loadFrom }) {
surveyModel.clearIncorrectValues(true); surveyModel.clearIncorrectValues(true);
surveyModel.mode = json['mode']; surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by'] surveyModel.lockedBy = json['locked_by']
surveyModel.lockUUID = json['lock_uuid'];
surveyModel.status = json['status']; surveyModel.status = json['status'];
lockUUID.current = json['lock_uuid'];
}, },
'releaseLock': async () => { 'releaseLock': async () => {
const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' }); const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' });
...@@ -207,7 +202,7 @@ function SurveyContainerComponent({ loadFrom }) { ...@@ -207,7 +202,7 @@ function SurveyContainerComponent({ loadFrom }) {
<Toaster /> <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} /> <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 surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}> <SurveyNavigationComponent surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}>
<SurveyComponent surveyModel={surveyModel} verificationStatus={verificationStatus} /> <SurveyComponent surveyModel={surveyModel} />
</SurveyNavigationComponent> </SurveyNavigationComponent>
</Container> </Container>
); );
... ...
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment