diff --git a/compendium-frontend/src/survey/ProgressBar.tsx b/compendium-frontend/src/survey/ProgressBar.tsx index 0232a12c0da82bff755a15cfcbb0decae598c997..aa2d1cc843092f44beba88f3f453560fb7f79899 100644 --- a/compendium-frontend/src/survey/ProgressBar.tsx +++ b/compendium-frontend/src/survey/ProgressBar.tsx @@ -8,7 +8,7 @@ interface Progress { pageTitle: string; } -function ProgressBar({ surveyModel, pageNoSetter }) { +function ProgressBar({ surveyModel, pageNoSetter, pageNo}) { const [progress, setProgress] = useState<Progress[]>([]); const filterCallback = (question) => { @@ -78,7 +78,7 @@ function ProgressBar({ surveyModel, pageNoSetter }) { }}>{index + 1}</span> <span style={{ whiteSpace: "nowrap", - ...(surveyModel.currentPageNo == index) && { + ...(pageNo == index) && { fontWeight: "bold", }, }}>{sectionProgress.pageTitle}</span> diff --git a/compendium-frontend/src/survey/Prompt.tsx b/compendium-frontend/src/survey/Prompt.tsx index f2ce6b0d8cefef8d303fbdc1afb2dd414154d78d..713b2171429786401adf07812acd096c7bfa7ffe 100644 --- a/compendium-frontend/src/survey/Prompt.tsx +++ b/compendium-frontend/src/survey/Prompt.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useBlocker } from "react-router-dom"; // adapted from https://stackoverflow.com/a/75920683 diff --git a/compendium-frontend/src/survey/SurveyComponent.tsx b/compendium-frontend/src/survey/SurveyComponent.tsx index 051f45648accabd0056ef10242c894e4ed5e8bd3..6e48c3100c84dbd8b4e63ce33e1335f57b693e25 100644 --- a/compendium-frontend/src/survey/SurveyComponent.tsx +++ b/compendium-frontend/src/survey/SurveyComponent.tsx @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useEffect } from "react"; import { Question } from "survey-core"; import { Survey } from "survey-react-ui"; import { VerificationStatus } from './Schema'; @@ -144,50 +144,52 @@ function setVerifyButton(question: Question, state: VerificationStatus, surveyMo } function SurveyComponent({ surveyModel }) { - - const alwaysSetVerify = useCallback((_, options) => { - const status = surveyModel.verificationStatus.get(options.question.name); - const readonly = options.question?.readOnly; - - if (status && !readonly) { - setVerifyButton(options.question, status, surveyModel); + + useEffect(() => { + const updateFromUnverified = (_, options) => { + const currentStatus = surveyModel.verificationStatus.get(options.question.name); + if (currentStatus == VerificationStatus.Unverified) { + setVerifyButton(options.question, VerificationStatus.Edited, surveyModel); + } } - else if (readonly) { - strikeThroughOrHide(options.question); + + const alwaysSetVerify = (_, options) => { + const status = surveyModel.verificationStatus.get(options.question.name); + const readonly = options.question?.readOnly; + + if (status && !readonly) { + setVerifyButton(options.question, status, surveyModel); + } + else if (readonly) { + strikeThroughOrHide(options.question); + } } - }, [surveyModel]) - const updateFromUnverified = useCallback((_, options) => { - const currentStatus = surveyModel.verificationStatus.get(options.question.name); - if (currentStatus == VerificationStatus.Unverified) { - setVerifyButton(options.question, VerificationStatus.Edited, surveyModel); + if (!surveyModel.onAfterRenderQuestion.hasFunc(alwaysSetVerify)) { + surveyModel.onAfterRenderQuestion.add(alwaysSetVerify); + surveyModel.onAfterRenderQuestion.add(fixTitleCss); } - }, [surveyModel]) - - if (!surveyModel.onAfterRenderQuestion.hasFunc(alwaysSetVerify)) { - surveyModel.onAfterRenderQuestion.add(alwaysSetVerify); - surveyModel.onAfterRenderQuestion.add(fixTitleCss); - } - if (!surveyModel.onValueChanged.hasFunc(updateFromUnverified)) { - surveyModel.onValueChanged.add(updateFromUnverified); - } + if (!surveyModel.onValueChanged.hasFunc(updateFromUnverified)) { + surveyModel.onValueChanged.add(updateFromUnverified); + } - if (!surveyModel.onUpdateQuestionCssClasses.hasFunc(hideCheckboxLabels)) { - surveyModel.onUpdateQuestionCssClasses.add(hideCheckboxLabels); - } + 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 columns become visble or invisible) - surveyModel.onMatrixAfterCellRender.add(customDescriptionCallback); - } + 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 columns become visble or invisible) + surveyModel.onMatrixAfterCellRender.add(customDescriptionCallback); + } - if (!surveyModel.onTextMarkdown.hasFunc(setDescriptionHTML)) { - // WARNING: THIS ASSUMES THAT THE QUESTION DESCRIPTION IS NOT USER-PROVIDED. - // SETTING THE HTML HERE IS A SECURITY RISK IF NOT PROPERLY SANITISED - surveyModel.onTextMarkdown.add(setDescriptionHTML); - } + if (!surveyModel.onTextMarkdown.hasFunc(setDescriptionHTML)) { + // WARNING: THIS ASSUMES THAT THE QUESTION DESCRIPTION IS NOT USER-PROVIDED. + // SETTING THE HTML HERE IS A SECURITY RISK IF NOT PROPERLY SANITISED + surveyModel.onTextMarkdown.add(setDescriptionHTML); + } + }, [surveyModel]); return <Survey model={surveyModel} /> } diff --git a/compendium-frontend/src/survey/SurveyContainerComponent.tsx b/compendium-frontend/src/survey/SurveyContainerComponent.tsx index dbdf1c1dc83915aeb8520bb1bce66de987c62ee3..5661a7c00b17cd2fa76d243dc9c9c9b4f8132d08 100644 --- a/compendium-frontend/src/survey/SurveyContainerComponent.tsx +++ b/compendium-frontend/src/survey/SurveyContainerComponent.tsx @@ -11,76 +11,10 @@ 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 { validateQuestion, oldValidateWebsiteUrl } 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"); @@ -147,6 +81,7 @@ function SurveyContainerComponent({ loadFrom }) { survey.data = json['data']; survey.clearIncorrectValues(true); survey.currentPageNo = json['page']; + // setPageNo(json['page']); survey.mode = json['mode']; survey['lockedBy'] = json['locked_by']; @@ -308,7 +243,9 @@ function SurveyContainerComponent({ loadFrom }) { } const onPageChange = (page) => { + if (!surveyModel) return; surveyModel.currentPageNo = page; + setSurveyModel(Object.create(surveyModel)); } return ( @@ -316,8 +253,9 @@ function SurveyContainerComponent({ loadFrom }) { {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}> + <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} onPageChange={onPageChange}> <SurveyComponent surveyModel={surveyModel} /> </SurveyNavigationComponent> </Container> diff --git a/compendium-frontend/src/survey/SurveyNavigationComponent.tsx b/compendium-frontend/src/survey/SurveyNavigationComponent.tsx index b76dabcdf7aa49a20aa39c4c518d46c6f4afd837..10fd4991bea9ed77aca40107ed62ffb7523dc92b 100644 --- a/compendium-frontend/src/survey/SurveyNavigationComponent.tsx +++ b/compendium-frontend/src/survey/SurveyNavigationComponent.tsx @@ -1,46 +1,24 @@ -import { useContext, useEffect, useState, useCallback } from "react"; +import { useContext } from "react"; import ProgressBar from './ProgressBar'; import { Container, Row } from "react-bootstrap"; import { userContext } from "../providers/UserProvider"; -function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, children, onPageChange }) { - // We use some state variables that are directly derived from the surveyModel to - // ensure React rerenders properly. It would honestly be just as easy to remove the state - // and force the rerender ourselves, because we know exactly when the rerender is necessary. - const [pageNo, setPageNo] = useState(0); - const [editing, setEditing] = useState(false); - const [lockedBy, setLockedBy] = useState(""); - const [responseStatus, setResponseStatus] = useState(""); +function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, children, onPageChange + }) { const { user: loggedInUser } = useContext(userContext); + const pageNo = surveyModel?.currentPageNo ?? 0; + const editing = surveyModel?.mode === 'edit'; + const lockedBy = surveyModel?.["lockedBy"] ?? ""; + const responseStatus = surveyModel?.["status"] ?? ""; - // useCallback to keep the linter happy with the useEffect dependencies below - const copySurveyState = useCallback(() => { - setEditing(surveyModel.mode == 'edit'); - setLockedBy(surveyModel.lockedBy); - setPageNo(surveyModel.currentPageNo); - setResponseStatus(surveyModel.status); - }, [surveyModel]); - - useEffect(() => { - copySurveyState(); - }, [copySurveyState]); - - const pageNoSetter = (page) => { - setPageNo(page); - onPageChange(page); - } - // const decrementPageNo = () => { pageNoSetter(surveyModel.currentPageNo - 1); }; - const incrementPageNo = () => { pageNoSetter(surveyModel.currentPageNo + 1); }; + const incrementPageNo = () => { onPageChange(surveyModel.currentPageNo + 1); }; const doSurveyAction = async (action) => { await surveyActions[action](); - copySurveyState(); + // onPageChange triggers a re-render, just keep the same page number + onPageChange(surveyModel.currentPageNo); } - const renderActionButton = (text, action) => { - return renderButton(text, () => doSurveyAction(action)); - }; - const renderButton = (text, action) => { return ( <button className="sv-btn sv-btn--navigation" onClick={action}> @@ -49,6 +27,10 @@ function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, chi ); }; + const renderActionButton = (text, action) => { + return renderButton(text, () => doSurveyAction(action)); + }; + const saveAndStopEdit = 'Save and stop editing'; const save = 'Save progress'; const startEditing = 'Start editing'; @@ -63,8 +45,6 @@ function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, chi {editing && renderActionButton(saveAndStopEdit, 'saveAndStopEdit')} {editing && renderActionButton(completeSurvey, 'complete')} {(pageNo !== surveyModel.visiblePages.length - 1) && renderButton('Next Section', incrementPageNo)} - {/* {renderActionButton('Validate Page', 'validatePage')} */} - {/* {pageNo !== 0 && renderButton('Previous Page', decrementPageNo)} */} </div> ); }; @@ -103,7 +83,7 @@ function SurveyNavigationComponent({ surveyModel, surveyActions, year, nren, chi )} </Row> <Row> - <ProgressBar surveyModel={surveyModel} pageNoSetter={pageNoSetter} /> + <ProgressBar surveyModel={surveyModel} pageNoSetter={onPageChange} pageNo={pageNo} /> {children} </Row> <Row>