diff --git a/survey-frontend/src/SurveyComponent.tsx b/survey-frontend/src/SurveyComponent.tsx index cd4ae0a8d3d5e1df8be3f8e9604d1906eeb72ea0..35e4deab7f6d89c214527f5dc656b56b8637533c 100644 --- a/survey-frontend/src/SurveyComponent.tsx +++ b/survey-frontend/src/SurveyComponent.tsx @@ -1,101 +1,118 @@ -import React from "react"; +import React, { useCallback } from "react"; import { Question, FunctionFactory } from "survey-core"; import { Survey } from "survey-react-ui"; import { VerificationStatus } from './Schema'; -function SurveyComponent({ surveyModel, verificationStatus }) { - function validateWebsiteUrl(params) { - const value = params[0]; - if (value === undefined || value == null || value == '') { - return true; - } - try { - const url = new URL(value); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch (err) { - return false; +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']); } } +} - surveyModel.css = { - question: { - title: "sv-title sv-question__title sv-header-flex", - titleOnError: "sv-question__title--error sv-error-color-fix" - } - }; - - function setVerifyButton(question: Question, state: VerificationStatus) { - - verificationStatus.current.set(question.name, state); - - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "sv-action-bar-item verification"; - btn.innerHTML = state; - - if (state == VerificationStatus.Unverified) { - btn.innerHTML = "No change from previous year"; - btn.className += " verification-required"; - btn.onclick = function () { - if (surveyModel.mode == "display") { - return; - } - question.validate(); - setVerifyButton(question, VerificationStatus.Verified); +function validateWebsiteUrl(params) { + const value = params[0]; + if (value === undefined || value == null || value == '') { + return true; + } + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch (err) { + return false; + } +} + +function hideCheckboxLabels(_, options) { + if (options.question.hideCheckboxLabels) { + const classes = options.cssClasses; + classes.root += " hidden-checkbox-labels"; + } +} + +function setVerifyButton(question: Question, state: VerificationStatus, surveyModel) { + + surveyModel.verificationStatus.set(question.name, state); + + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "sv-action-bar-item verification"; + btn.innerHTML = state; + + if (state == VerificationStatus.Unverified) { + btn.innerHTML = "No change from previous year"; + btn.className += " verification-required"; + btn.onclick = function () { + if (surveyModel.mode == "display") { + return; } - } else { - btn.innerHTML = "Answer updated" - btn.className += " verification-ok"; + question.validate(); + setVerifyButton(question, VerificationStatus.Verified, surveyModel); } + } else { + btn.innerHTML = "Answer updated" + btn.className += " verification-ok"; + } - const selector = '[data-name="' + question.name + '"]'; - const header = document.querySelector(selector)?.querySelector('h5'); + const selector = '[data-name="' + question.name + '"]'; + const header = document.querySelector(selector)?.querySelector('h5'); - const old = header?.querySelector(".verification"); - if (old) { - old.replaceWith(btn); - } else { - header?.appendChild(btn); - } + const old = header?.querySelector(".verification"); + if (old) { + old.replaceWith(btn); + } else { + header?.appendChild(btn); } +} - FunctionFactory.Instance.register("validateWebsiteUrl", validateWebsiteUrl); - surveyModel.onAfterRenderQuestion.add(function (survey, options) { - const status = verificationStatus.current.get(options.question.name); +function SurveyComponent({ surveyModel }) { + + const alwaysSetVerify = useCallback((_, options) => { + const status = surveyModel.verificationStatus.get(options.question.name); if (status) { - setVerifyButton(options.question, status); + setVerifyButton(options.question, status, surveyModel); } - }); + }, [surveyModel]) - surveyModel.onValueChanged.add(function (survey, options) { - const currentStatus = verificationStatus.current.get(options.question.name); + const updateFromUnverified = useCallback((_, options) => { + const currentStatus = surveyModel.verificationStatus.get(options.question.name); if (currentStatus == VerificationStatus.Unverified) { - setVerifyButton(options.question, VerificationStatus.Edited); + setVerifyButton(options.question, VerificationStatus.Edited, surveyModel); } - }); + }, [surveyModel]) - surveyModel.onUpdateQuestionCssClasses.add(function (_, options) { - if (options.question.hideCheckboxLabels) { - const classes = options.cssClasses; - classes.root += " hidden-checkbox-labels"; - } - }); + if (!FunctionFactory.Instance.hasFunction("validateWebsiteUrl")) { + FunctionFactory.Instance.register("validateWebsiteUrl", validateWebsiteUrl); + } + + 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"; + } + + if (!surveyModel.onAfterRenderQuestion.hasFunc(alwaysSetVerify)) { + surveyModel.onAfterRenderQuestion.add(alwaysSetVerify); + } - surveyModel.onMatrixAfterCellRender.add((survey, options) => { - // get the customDescription for matrix rows and set it in the title - // attribute so that it shows up as a hover popup + 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) - - 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']); - } - } - }); + surveyModel.onMatrixAfterCellRender.add(customDescriptionCallback); + } return <Survey model={surveyModel} /> } diff --git a/survey-frontend/src/SurveyContainerComponent.tsx b/survey-frontend/src/SurveyContainerComponent.tsx index 1322ddc4f69ef3984132f086767a65d9f0ea4d20..fdf88423874aecbf723cce3356ad1192fc168e23 100644 --- a/survey-frontend/src/SurveyContainerComponent.tsx +++ b/survey-frontend/src/SurveyContainerComponent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { Container } from "react-bootstrap"; import toast, { Toaster } from "react-hot-toast"; import { Model, Serializer } from "survey-core"; @@ -15,14 +15,9 @@ Serializer.addProperty("question", "hideCheckboxLabels:boolean"); function SurveyContainerComponent({ loadFrom }) { - // NB This component should only rerender after the model is retrieved. - // If you change this to rerender more often, stuff will probably break. - - 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 [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 lockUUID = useRef<string>(); const beforeUnloadListener = useCallback((event) => { event.preventDefault(); @@ -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']); survey.setVariable('surveyyear', year); survey.setVariable('previousyear', parseInt(year!) - 1); 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.clearIncorrectValues(true); // TODO test if this really removes all old values and such survey.currentPageNo = json['page']; @@ -89,11 +84,11 @@ function SurveyContainerComponent({ loadFrom }) { } const saveData = { - lock_uuid: lockUUID.current, + lock_uuid: survey.lockUUID, new_state: newState, data: survey.data, page: survey.currentPageNo, - verification_status: Object.fromEntries(verificationStatus.current) + verification_status: Object.fromEntries(survey.verificationStatus) }; try { @@ -116,7 +111,7 @@ function SurveyContainerComponent({ loadFrom }) { const validateWithAnswerVerification = (validatorFunction) => { let firstValidationError = ''; const verificationValidator = (survey, options) => { - const status = verificationStatus.current.get(options.name); + const status = survey.verificationStatus.get(options.name); if (status == VerificationStatus.Unverified) { if (firstValidationError == '') { firstValidationError = options.name; @@ -180,8 +175,8 @@ function SurveyContainerComponent({ loadFrom }) { surveyModel.clearIncorrectValues(true); surveyModel.mode = json['mode']; surveyModel.lockedBy = json['locked_by'] + surveyModel.lockUUID = json['lock_uuid']; surveyModel.status = json['status']; - lockUUID.current = json['lock_uuid']; }, 'releaseLock': async () => { const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' }); @@ -207,7 +202,7 @@ function SurveyContainerComponent({ loadFrom }) { <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 surveyModel={surveyModel} surveyActions={surveyActions} year={year} nren={nren}> - <SurveyComponent surveyModel={surveyModel} verificationStatus={verificationStatus} /> + <SurveyComponent surveyModel={surveyModel} /> </SurveyNavigationComponent> </Container> );