From a378b1c9ee096388002f3b0fdc26636e9c1cd57b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 1 Jul 2022 00:19:13 +0400 Subject: Restructure FormPage The main FormPage component had gotten very out of hand, with many moving parts that were hard to parse, understand, or modify. This refactors breaks things up into separate files with better defined goals. Most changes are just straight copies without much change, however some structural changes have been introduced as a foundation for improving the app. Signed-off-by: Hassan Abouelela --- src/pages/FormPage.tsx | 347 ------------------------------------------------- 1 file changed, 347 deletions(-) delete mode 100644 src/pages/FormPage.tsx (limited to 'src/pages/FormPage.tsx') diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx deleted file mode 100644 index f21e4f0..0000000 --- a/src/pages/FormPage.tsx +++ /dev/null @@ -1,347 +0,0 @@ -/** @jsx jsx */ -import { jsx, css } from "@emotion/react"; -import { Link } from "react-router-dom"; - -import React, { SyntheticEvent, useEffect, useState, createRef } from "react"; -import { useParams } from "react-router"; -import { PropagateLoader } from "react-spinners"; -import { AxiosError } from "axios"; - -import HeaderBar from "../components/HeaderBar"; -import RenderedQuestion from "../components/Question"; -import Loading from "../components/Loading"; -import ScrollToTop from "../components/ScrollToTop"; -import OAuth2Button from "../components/OAuth2Button"; - -import { Form, FormFeatures, getForm } from "../api/forms"; -import { OAuthScopes, checkScopes } from "../api/auth"; -import colors from "../colors"; -import { submitStyles, unselectable } from "../commonStyles"; -import { Question, QuestionType } from "../api/question"; -import ApiClient from "../api/client"; - -interface PathParams { - id: string -} - -interface NavigationProps { - form_state: boolean, // Whether the form is open or not - scopes: OAuthScopes[] -} - -class Navigation extends React.Component { - static containerStyles = css` - margin: auto; - width: 50%; - - text-align: center; - font-size: 1.5rem; - - > div { - display: inline-block; - margin: 2rem auto; - width: 50%; - } - - @media (max-width: 870px) { - width: 100%; - - > div { - display: flex; - justify-content: center; - - margin: 0 auto; - } - } - - .return_button { - text-align: left; - } - - .return_button.closed { - text-align: center; - } - `; - - separatorStyles = css` - height: 0; - display: none; - - @media (max-width: 870px) { - display: block; - } - `; - - static returnStyles = css` - padding: 0.5rem 2.2rem; - border-radius: 8px; - - color: white; - text-decoration: none; - white-space: nowrap; - - background-color: ${colors.greyple}; - transition: background-color 300ms; - - :hover { - background-color: ${colors.darkerGreyple}; - } - `; - - constructor(props: NavigationProps) { - super(props); - this.state = {"logged_in": false}; - } - - render(): JSX.Element { - let submit = null; - - if (this.props.form_state) { - let inner_submit; - if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes)) { - // Render OAuth button if login is required, and the scopes needed are not available - inner_submit = this.setState({"logged_in": true})}/>; - } else { - inner_submit = ; - } - submit =
{ inner_submit }
; - } - - return ( -
-
- Return Home -
-
- { submit } -
- ); - } -} - -const formStyles = css` - margin: auto; - width: 50%; - - @media (max-width: 800px) { - /* Make form larger on mobile and tablet screens */ - width: 80%; - } -`; - -const closedHeaderStyles = css` - margin-bottom: 2rem; - padding: 1rem 4rem; - border-radius: 8px; - - text-align: center; - font-size: 1.5rem; - - background-color: ${colors.error}; - - @media (max-width: 500px) { - padding: 1rem 1.5rem; - } -`; - -function FormPage(): JSX.Element { - const { id } = useParams(); - - const [form, setForm] = useState
(); - const [sending, setSending] = useState(); - const [sent, setSent] = useState(); - - const bottomDivRef = createRef(); - - useEffect(() => { - getForm(id).then(form => { - setForm(form); - }); - }, []); - - if (form && sent) { - const thanksStyle = css`font-family: "Uni Sans", "Hind", "Arial", sans-serif; margin-top: 15.5rem;`; - const divStyle = css`width: 80%;`; - - let submitted_text; - if (form.submitted_text) { - submitted_text = form.submitted_text.split("\n").map((line, index) => {line}
); - submitted_text.push({submitted_text.pop()?.props.children[0]}); - } else { - submitted_text = "Thanks for your response!"; - } - - return ( -
- -
-

{submitted_text}

-
- Return Home -
-
-
- ); - } - - if (sending) { - return ( -
- -
- -
-
- ); - } - - if (!form) { - return ; - } - - const refMap: Map> = new Map(); - const questions = form.questions.map((question, index) => { - const questionRef = createRef(); - refMap.set(question.id, questionRef); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ()} scroll_ref={createRef()} question={question} public_state={new Map()} key={index + Date.now()}/>; - }); - - const open: boolean = form.features.includes(FormFeatures.Open); - const require_auth: boolean = form.features.includes(FormFeatures.RequiresLogin); - - const scopes: OAuthScopes[] = []; - if (require_auth) { - scopes.push(OAuthScopes.Identify); - if (form.features.includes(FormFeatures.CollectEmail)) { scopes.push(OAuthScopes.Email); } - } - - let closed_header = null; - if (!open) { - closed_header =
This form is now closed. You will not be able to submit your response.
; - } - - async function handleSubmit(event: SyntheticEvent) { - event.preventDefault(); - // Client-side required validation - const invalidFieldIDs: number[] = []; - questions.forEach((prop, i) => { - const question: Question = prop.props.question; - if (!question.required) { - return; - } - - const questionRef = refMap.get(question.id); - if (questionRef && questionRef.current) { - if(questionRef.current.props.question.type == QuestionType.Code){ - questionRef.current.props.public_state.set("unittestsFailed", false); - } - questionRef.current.validateField(); - } - // In case when field is invalid, add this to invalid fields list. - if (prop.props.public_state.get("valid") === false) { - invalidFieldIDs.push(i); - } - }); - - if (invalidFieldIDs.length) { - const firstErrored = questions[invalidFieldIDs[0]]; - if (firstErrored && firstErrored.props.scroll_ref) { - // If any element is already focused, unfocus it to avoid not scrolling behavior. - if (document.activeElement && document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - - firstErrored.props.scroll_ref.current.scrollIntoView({ behavior: "smooth", block: "center" }); - if (firstErrored.props.focus_ref && firstErrored.props.focus_ref.current) { - firstErrored.props.focus_ref.current.focus({ preventScroll: true }); - } - } - return; - } - - // Scroll to bottom when OAuth2 required - if (scopes.length && !checkScopes(scopes)) { - bottomDivRef.current.scrollIntoView({ behavior: "smooth", block: "end"}); - - return; - } - - - const answers: { [key: string]: unknown } = {}; - questions.forEach(prop => { - const question: Question = prop.props.question; - const options: string | string[] = question.data["options"]; - - // Parse input from each question - switch (question.type) { - case QuestionType.Section: - answers[question.id] = false; - break; - - case QuestionType.Checkbox: { - if (typeof options !== "string") { - const keys: Map = new Map(); - options.forEach((val: string, index) => { - keys.set(val, `${("000" + index).slice(-4)}. ${val}`); - }); - const pairs: { [key: string]: boolean } = { }; - keys.forEach((val, key) => { - pairs[key] = !!prop.props.public_state.get(val); - }); - answers[question.id] = pairs; - } - break; - } - - case QuestionType.Code: - default: - answers[question.id] = prop.props.public_state.get("value"); - } - }); - await ApiClient.post(`forms/submit/${id}`, { response: answers }) - .then(() => { - setSending(true); - setSending(false); - setSent(true); - }) - .catch((err: AxiosError) => { - switch (err.response.status) { - case 422: { - // Validation on a submitted code questions - const questionId = - err.response.data.test_results[0].question_id; - questions.forEach((prop) => { - const question: Question = prop.props.question; - const questionRef = refMap.get(question.id); - if (question.id === questionId) { - prop.props.public_state.set("unittestsFailed", true); - questionRef.current.validateField(); - } - }); - break; - } - default: - throw err; - } - }); - } - return ( -
- - -
- - { closed_header } - { questions } - - -
- -
- -
- ); -} - -export default FormPage; -- cgit v1.2.3