Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update form timer to handle groups #4437

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
114 changes: 114 additions & 0 deletions __fixtures__/formDelayElements.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"formElementsNonGroups": [
{
"id": 1,
"type": "textField",
"properties": {
"choices": [
{
"en": "",
"fr": ""
}
],
"titleEn": "Q1",
"titleFr": "1",
"validation": {
"required": true
},
"subElements": [],
"descriptionEn": "",
"descriptionFr": "",
"placeholderEn": "",
"placeholderFr": ""
}
},
{
"id": 2,
"type": "textField",
"properties": {
"choices": [
{
"en": "",
"fr": ""
}
],
"titleEn": "Q2",
"titleFr": "1",
"validation": {
"required": false
},
"subElements": [],
"descriptionEn": "",
"descriptionFr": "",
"placeholderEn": "",
"placeholderFr": ""
}
},
{
"id": 3,
"type": "textField",
"properties": {
"choices": [
{
"en": "",
"fr": ""
}
],
"titleEn": "Q3",
"titleFr": "1",
"validation": {
"required": true
},
"subElements": [],
"descriptionEn": "",
"descriptionFr": "",
"placeholderEn": "",
"placeholderFr": ""
}
},
{
"id": 4,
"type": "textField",
"properties": {
"choices": [
{
"en": "",
"fr": ""
}
],
"titleEn": "Q4",
"titleFr": "1",
"validation": {
"required": true
},
"subElements": [],
"descriptionEn": "",
"descriptionFr": "",
"placeholderEn": "",
"placeholderFr": ""
}
},
{
"id": 5,
"type": "textField",
"properties": {
"choices": [
{
"en": "",
"fr": ""
}
],
"titleEn": "Q5 (not required)",
"titleFr": "1",
"validation": {
"required": false
},
"subElements": [],
"descriptionEn": "",
"descriptionFr": "",
"placeholderEn": "",
"placeholderFr": ""
}
}
]
}
27 changes: 13 additions & 14 deletions components/clientComponents/forms/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ import {
import { filterShownElements, filterValuesByShownElements } from "@lib/formContext";
import { formHasGroups } from "@lib/utils/form-builder/formHasGroups";
import { showReviewPage } from "@lib/utils/form-builder/showReviewPage";
import { useFormDelay } from "@lib/hooks/useFormDelayContext";

interface SubmitButtonProps {
numberOfRequiredQuestions: number;
getNumberOfRequiredQuestions: () => number;
formID: string;
formTitle: string;
}
const SubmitButton: React.FC<SubmitButtonProps> = ({
numberOfRequiredQuestions,
getNumberOfRequiredQuestions,
formID,
formTitle,
}) => {
Expand All @@ -44,15 +45,13 @@ const SubmitButton: React.FC<SubmitButtonProps> = ({
const [submitTooEarly, setSubmitTooEarly] = useState(false);
const screenReaderRemainingTime = useRef(formTimerState.remainingTime);

// calculate initial delay for submit timer
const secondsBaseDelay = 2;
const secondsPerFormElement = 2;
const submitDelaySeconds = secondsBaseDelay + numberOfRequiredQuestions * secondsPerFormElement;

const formTimerEnabled = process.env.NEXT_PUBLIC_APP_ENV !== "test";

// If the timer hasn't started yet, start the timer
if (!formTimerState.timerDelay && formTimerEnabled) startTimer(submitDelaySeconds);
if (!formTimerState.timerDelay && formTimerEnabled) {
// calculate initial delay for submit timer
startTimer(getNumberOfRequiredQuestions());
}

useEffect(() => {
if (!formTimerEnabled && !formTimerState.canSubmit) {
Expand Down Expand Up @@ -166,6 +165,8 @@ const InnerForm: React.FC<InnerFormProps> = (props) => {
// Used to set any values we'd like added for use in the below withFormik handleSubmit().
useFormValuesChanged();

const { getFormDelay } = useFormDelay();

const errorList = props.errors ? getErrorList(props) : null;
const errorId = "gc-form-errors";
const serverErrorId = `${errorId}-server`;
Expand Down Expand Up @@ -194,10 +195,6 @@ const InnerForm: React.FC<InnerFormProps> = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formStatusError, errorList, lastSubmitCount, canFocusOnError]);

const numberOfRequiredQuestions = form.elements.filter(
(element) => element.properties.validation?.required === true
).length;

return status === "submitting" ? (
<>
<title>{t("loading")}</title>
Expand Down Expand Up @@ -304,7 +301,9 @@ const InnerForm: React.FC<InnerFormProps> = (props) => {
)}
<div className="inline-block">
<SubmitButton
numberOfRequiredQuestions={numberOfRequiredQuestions}
getNumberOfRequiredQuestions={() =>
getFormDelay(form.elements, isShowReviewPage)
}
formID={formID}
formTitle={form.titleEn}
/>
Expand All @@ -315,7 +314,7 @@ const InnerForm: React.FC<InnerFormProps> = (props) => {
})
) : (
<SubmitButton
numberOfRequiredQuestions={numberOfRequiredQuestions}
getNumberOfRequiredQuestions={() => getFormDelay(form.elements, isShowReviewPage)}
formID={formID}
formTitle={form.titleEn}
/>
Expand Down
3 changes: 3 additions & 0 deletions components/clientComponents/forms/NextButton/NextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Language } from "@lib/types/form-builder-types";
import { getLocalizedProperty } from "@lib/utils";
import { showReviewPage } from "@lib/utils/form-builder/showReviewPage";
import { focusElement } from "@lib/client/clientHelpers";
import { useFormDelay } from "@lib/hooks/useFormDelayContext";

export const NextButton = ({
validateForm,
Expand All @@ -26,6 +27,7 @@ export const NextButton = ({
formRecord: PublicFormRecord;
}) => {
const { currentGroup, hasNextAction, handleNextAction, isOffBoardSection } = useGCFormsContext();
const { addRequiredQuestions } = useFormDelay();
const { t } = useTranslation("form-builder");

const handleValidation = async () => {
Expand Down Expand Up @@ -89,6 +91,7 @@ export const NextButton = ({
onClick={async (e) => {
e.preventDefault();
if (await handleValidation()) {
addRequiredQuestions(formRecord.form, currentGroup);
handleNextAction();
focusElement("h2");
}
Expand Down
5 changes: 4 additions & 1 deletion components/clientComponents/globals/ClientContexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LiveMessagePovider } from "@lib/hooks/useLiveMessage";
import { RefsProvider } from "@formBuilder/[id]/edit/components/RefsContext";
import { FeatureFlagsProvider } from "@lib/hooks/useFeatureFlags";
import { Flags } from "@lib/cache/types";
import { FormDelayProvider } from "@lib/hooks/useFormDelayContext";

export const ClientContexts: React.FC<{
session: Session | null;
Expand All @@ -25,7 +26,9 @@ export const ClientContexts: React.FC<{
<AccessControlProvider>
<RefsProvider>
<FeatureFlagsProvider featureFlags={featureFlags}>
<LiveMessagePovider>{children}</LiveMessagePovider>
<LiveMessagePovider>
<FormDelayProvider>{children}</FormDelayProvider>
</LiveMessagePovider>
</FeatureFlagsProvider>
</RefsProvider>
</AccessControlProvider>
Expand Down
118 changes: 118 additions & 0 deletions lib/hooks/useFormDelayContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { logMessage } from "@lib/logger";
import { FormElement, FormProperties } from "@lib/types";
import { createContext, useContext, useState } from "react";

interface FormDelay {
startTime: number;
requiredQuestions: number;
}

const timerDefault: FormDelay = {
startTime: 0,
requiredQuestions: 0,
};

const FormDelayContext = createContext<{
formDelay: FormDelay;
setFormDelay: React.Dispatch<React.SetStateAction<FormDelay>>;
}>({
formDelay: timerDefault,
setFormDelay: () => {},
});

export const FormDelayProvider = ({ children }: { children: React.ReactNode }) => {
const [formDelay, setFormDelay] = useState(timerDefault);
return (
<FormDelayContext.Provider value={{ formDelay, setFormDelay }}>
{children}
</FormDelayContext.Provider>
);
};

// Adds in a little math to make it more unpredictable and calculates the final form delay
const calculateSubmitDelay = (delayFromFormData: number) => {
const secondsBaseDelay = 2; // To help test group forms, make this big e.g. 20000
const secondsPerFormElement = 2;
return secondsBaseDelay + delayFromFormData * secondsPerFormElement;
};

export const calculateDelayWithGroups = (
startTime: number,
endTime: number,
requiredQuestions: number
) => {
if (isNaN(startTime) || isNaN(endTime) || isNaN(requiredQuestions)) {
return -1;
}
const elapsedTime = Math.floor((endTime - startTime) / 1000);
const delayFromFormData = requiredQuestions - elapsedTime;
return calculateSubmitDelay(delayFromFormData);
};

export const calculateDelayWithoutGroups = (formElements: FormElement[]) => {
if (!Array.isArray(formElements)) {
return -1;
}
const delayFromFormData = formElements.filter(
(element) => element.properties.validation?.required === true
).length;
return calculateSubmitDelay(delayFromFormData);
};

// Turn on for local testing
const debug = false;

export const useFormDelay = () => {
const { formDelay, setFormDelay } = useContext(FormDelayContext);
return {
/**
* Adds the number of required questions in the current group to the form delay state.
* @param form the current form
* @param currentGroupId group Id of the current page
*/
addRequiredQuestions: (form: FormProperties, currentGroupId: string) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to "updateFormDelay" and mention setting the time in the comments

try {
const groupIds = form?.groups?.[currentGroupId].elements;
if (!groupIds) {
return;
}

const currentGroupRequiredQuestions = form.elements
.filter((element) => groupIds.find((id) => String(id) === String(element.id)))
.filter((element) => element.properties.validation?.required === true).length;

setFormDelay({
requiredQuestions: formDelay.requiredQuestions + currentGroupRequiredQuestions,
// Set start time timestamp on initial call
startTime: formDelay.startTime === 0 ? Date.now() : formDelay.startTime,
});
} catch (error) {
logMessage.info("Error adding required questions to form delay");
}
},
/**
* Gets the form delay based on the form type. For non-group forms, just send the required
* questions on a form. For group forms, subtract the time spent on the form from the tally of
* required questions from their group history (pages navigated).
* @param form current form
* @param hasGroups boolean to determine if the form has groups
* @returns delay in seconds or in the case of an error -1 is used to fallback to no delay
*/
getFormDelay: (formElements: FormElement[], hasGroups: boolean) => {
try {
const endTime = Date.now();
const delay = hasGroups
? calculateDelayWithGroups(formDelay.startTime, endTime, formDelay.requiredQuestions)
: calculateDelayWithoutGroups(formElements);

debug && logMessage.info(`Delay: ${delay}, formDelay: ${JSON.stringify(formDelay)}`);

// Avoid 0 because the SubmitButton relies on this to disable itself
return delay === 0 ? -1 : delay;
} catch (error) {
logMessage.info("Error calculating form delay.");
return -1;
}
},
};
};
Loading
Loading