aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Hassan Abouelela <[email protected]>2021-02-20 03:50:58 +0300
committerGravatar Hassan Abouelela <[email protected]>2021-02-20 03:53:45 +0300
commit0278c8f567bfc50fcb65aaf6afe7cd82c5031023 (patch)
treea20d335623dc8b9e9038de1d5dc03e481039ecd8
parentRemoves Path From Auth (diff)
parentAdds Missing Fields To Test Models (diff)
Merge branch 'main' into discord-oauth
Signed-off-by: Hassan Abouelela <[email protected]> # Conflicts: # package.json # src/commonStyles.tsx # src/pages/FormPage.tsx
-rw-r--r--package.json2
-rw-r--r--src/api/forms.ts3
-rw-r--r--src/commonStyles.tsx12
-rw-r--r--src/components/ErrorMessage.tsx24
-rw-r--r--src/components/InputTypes/Radio.tsx5
-rw-r--r--src/components/InputTypes/Range.tsx6
-rw-r--r--src/components/InputTypes/Select.tsx18
-rw-r--r--src/components/InputTypes/ShortText.tsx14
-rw-r--r--src/components/InputTypes/TextArea.tsx14
-rw-r--r--src/components/InputTypes/index.tsx19
-rw-r--r--src/components/Question.tsx112
-rw-r--r--src/pages/FormPage.tsx127
-rw-r--r--src/tests/components/FormListing.test.tsx6
-rw-r--r--src/tests/pages/LandingPage.test.tsx3
-rw-r--r--yarn.lock8
15 files changed, 324 insertions, 49 deletions
diff --git a/package.json b/package.json
index 8c5b69d..165884c 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"react-transition-group": "4.4.1",
"smoothscroll-polyfill": "0.4.4",
"swc-loader": "0.1.12",
- "typescript": "4.1.4",
+ "typescript": "4.1.5",
"universal-cookie": "4.0.4",
"webpack": "5.21.2",
"webpack-cli": "4.5.0",
diff --git a/src/api/forms.ts b/src/api/forms.ts
index 12b9abf..77fbb8e 100644
--- a/src/api/forms.ts
+++ b/src/api/forms.ts
@@ -16,7 +16,8 @@ export interface Form {
webhook: WebHook | null,
questions: Array<Question>,
name: string,
- description: string
+ description: string,
+ submitted_text: string | null
}
export interface WebHook {
diff --git a/src/commonStyles.tsx b/src/commonStyles.tsx
index eb3e319..bfae17e 100644
--- a/src/commonStyles.tsx
+++ b/src/commonStyles.tsx
@@ -53,6 +53,7 @@ const textInputs = css`
const submitStyles = css`
text-align: right;
+ white-space: nowrap;
button:disabled {
background-color: ${colors.greyple};
@@ -81,6 +82,14 @@ const submitStyles = css`
}
`;
+const invalidStyles = css`
+ .invalid-box {
+ -webkit-appearance: none;
+ -webkit-box-shadow: 0 0 0.6rem ${colors.error};
+ box-shadow: 0 0 0.6rem ${colors.error};
+ border-color: transparent;
+ }
+`;
export {
selectable,
@@ -88,5 +97,6 @@ export {
hiddenInput,
multiSelectInput,
textInputs,
- submitStyles
+ submitStyles,
+ invalidStyles
};
diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx
new file mode 100644
index 0000000..650100d
--- /dev/null
+++ b/src/components/ErrorMessage.tsx
@@ -0,0 +1,24 @@
+/** @jsx jsx */
+import { jsx, css } from "@emotion/react";
+import colors from "../colors";
+
+interface ErrorMessageProps {
+ show: boolean,
+ message: string
+}
+
+export default function ErrorMessage(props: ErrorMessageProps): JSX.Element | null {
+ const styles = css`
+ color: ${colors.error};
+ font-size: 1.15rem;
+ line-height: 1.1rem;
+ margin: 1rem 0 0;
+ visibility: ${props.show ? "visible" : "hidden"};
+ position: absolute;
+ z-index: -1;
+ `;
+
+ return (
+ <p css={styles}>{props.message}</p>
+ );
+}
diff --git a/src/components/InputTypes/Radio.tsx b/src/components/InputTypes/Radio.tsx
index 3bf13ed..a857964 100644
--- a/src/components/InputTypes/Radio.tsx
+++ b/src/components/InputTypes/Radio.tsx
@@ -7,7 +7,8 @@ import { multiSelectInput, hiddenInput } from "../../commonStyles";
interface RadioProps {
option: string,
question_id: string,
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ onBlurHandler: () => void
}
const styles = css`
@@ -31,7 +32,7 @@ const styles = css`
export default function Radio(props: RadioProps): JSX.Element {
return (
<label css={styles}>
- <input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput}/>
+ <input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput} onBlur={props.onBlurHandler}/>
<div css={multiSelectInput}/>
{props.option}<br/>
</label>
diff --git a/src/components/InputTypes/Range.tsx b/src/components/InputTypes/Range.tsx
index e2f89f4..23cb3f6 100644
--- a/src/components/InputTypes/Range.tsx
+++ b/src/components/InputTypes/Range.tsx
@@ -7,7 +7,9 @@ import { hiddenInput, multiSelectInput } from "../../commonStyles";
interface RangeProps {
question_id: string,
options: Array<string>,
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ required: boolean,
+ onBlurHandler: () => void
}
const containerStyles = css`
@@ -99,7 +101,7 @@ export default function Range(props: RangeProps): JSX.Element {
return (
<label css={[selectorStyles, css`width: 1rem`]} key={index}>
<span css={optionStyles}>{option}</span>
- <input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler}/>
+ <input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler} onBlur={props.onBlurHandler}/>
<div css={multiSelectInput}/>
</label>
);
diff --git a/src/components/InputTypes/Select.tsx b/src/components/InputTypes/Select.tsx
index e753357..2d0187a 100644
--- a/src/components/InputTypes/Select.tsx
+++ b/src/components/InputTypes/Select.tsx
@@ -1,11 +1,13 @@
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import React from "react";
-import { hiddenInput } from "../../commonStyles";
+import { hiddenInput, invalidStyles } from "../../commonStyles";
interface SelectProps {
options: Array<string>,
- state_dict: Map<string, string | boolean | null>
+ state_dict: Map<string, string | boolean | null>,
+ valid: boolean,
+ onBlurHandler: () => void
}
const containerStyles = css`
@@ -175,6 +177,14 @@ class Select extends React.Component<SelectProps> {
}
}
+ focusOption(): void {
+ if (!this.props.state_dict.get("value")) {
+ this.props.state_dict.set("value", "temporary");
+ this.props.onBlurHandler();
+ this.props.state_dict.set("value", null);
+ }
+ }
+
render(): JSX.Element {
const container_ref: React.RefObject<HTMLDivElement> = React.createRef();
const selected_option_ref: React.RefObject<HTMLDivElement> = React.createRef();
@@ -182,8 +192,8 @@ class Select extends React.Component<SelectProps> {
const handle_click = (event: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => this.handle_click(container_ref, selected_option_ref, event);
return (
- <div css={[containerStyles, arrowStyles, optionContainerStyles]} ref={container_ref}>
- <div className="selected_container" css={mainWindowStyles}>
+ <div css={[containerStyles, arrowStyles, optionContainerStyles, invalidStyles]} onFocus={this.focusOption.bind(this)} ref={container_ref} onBlur={this.props.onBlurHandler}>
+ <div css={mainWindowStyles} className={!this.props.valid ? "invalid-box selected_container" : "selected_container"}>
<span className="arrow"/>
<div tabIndex={0} className="selected_option" ref={selected_option_ref} onMouseDown={handle_click} onKeyDown={handle_click}>...</div>
</div>
diff --git a/src/components/InputTypes/ShortText.tsx b/src/components/InputTypes/ShortText.tsx
index 1e38bcd..8d99dc6 100644
--- a/src/components/InputTypes/ShortText.tsx
+++ b/src/components/InputTypes/ShortText.tsx
@@ -1,12 +1,20 @@
/** @jsx jsx */
import { jsx } from "@emotion/react";
import React, { ChangeEvent } from "react";
-import { textInputs } from "../../commonStyles";
+import { textInputs, invalidStyles } from "../../commonStyles";
interface ShortTextProps {
- handler: (event: ChangeEvent<HTMLInputElement>) => void
+ handler: (event: ChangeEvent<HTMLInputElement>) => void,
+ onBlurHandler: () => void,
+ valid: boolean,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
export default function ShortText(props: ShortTextProps): JSX.Element {
- return <input type="text" css={textInputs} placeholder="Enter Text..." onChange={props.handler}/>;
+ return (
+ <div css={invalidStyles}>
+ <input type="text" css={textInputs} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
+ </div>
+ );
}
diff --git a/src/components/InputTypes/TextArea.tsx b/src/components/InputTypes/TextArea.tsx
index 6e46c27..08424fb 100644
--- a/src/components/InputTypes/TextArea.tsx
+++ b/src/components/InputTypes/TextArea.tsx
@@ -1,10 +1,14 @@
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
-import { textInputs } from "../../commonStyles";
+import { invalidStyles, textInputs } from "../../commonStyles";
interface TextAreaProps {
- handler: (event: ChangeEvent<HTMLTextAreaElement>) => void
+ handler: (event: ChangeEvent<HTMLTextAreaElement>) => void,
+ onBlurHandler: () => void,
+ valid: boolean,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
const styles = css`
@@ -17,5 +21,9 @@ const styles = css`
`;
export default function TextArea(props: TextAreaProps): JSX.Element {
- return <textarea css={[textInputs, styles]} placeholder="Enter Text..." onChange={props.handler}/>;
+ return (
+ <div css={invalidStyles}>
+ <textarea css={[textInputs, styles]} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
+ </div>
+ );
}
diff --git a/src/components/InputTypes/index.tsx b/src/components/InputTypes/index.tsx
index f1e0b30..bc65248 100644
--- a/src/components/InputTypes/index.tsx
+++ b/src/components/InputTypes/index.tsx
@@ -18,12 +18,17 @@ const require_options: Array<QuestionType> = [
QuestionType.Range
];
-export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void): JSX.Element | JSX.Element[] {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] {
let result: JSX.Element | JSX.Element[];
// eslint-disable-next-line
// @ts-ignore
let options: string[] = question.data["options"];
+ let valid = true;
+ if (!public_state.get("valid")) {
+ valid = false;
+ }
// Catch input types that require options but don't have any
if ((options === undefined || typeof options !== "object") && require_options.includes(question.type)) {
@@ -34,7 +39,7 @@ export default function create_input({ question, public_state }: QuestionProp, h
/* eslint-disable react/react-in-jsx-scope */
switch (question.type) {
case QuestionType.TextArea:
- result = <TextArea handler={handler}/>;
+ result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
break;
case QuestionType.Checkbox:
@@ -42,19 +47,19 @@ export default function create_input({ question, public_state }: QuestionProp, h
break;
case QuestionType.Radio:
- result = options.map((option, index) => <Radio option={option} question_id={question.id} handler={handler} key={index}/>);
+ result = options.map((option, index) => <Radio option={option} question_id={question.id} handler={handler} key={index} onBlurHandler={onBlurHandler}/>);
break;
case QuestionType.Select:
- result = <Select options={options} state_dict={public_state}/>;
+ result = <Select options={options} state_dict={public_state} valid={valid} onBlurHandler={onBlurHandler}/>;
break;
case QuestionType.ShortText:
- result = <ShortText handler={handler}/>;
+ result = <ShortText handler={handler} onBlurHandler={onBlurHandler} valid={valid} focus_ref={focus_ref}/>;
break;
case QuestionType.Range:
- result = <Range question_id={question.id} options={options} handler={handler}/>;
+ result = <Range question_id={question.id} options={options} handler={handler} required={question.required} onBlurHandler={onBlurHandler}/>;
break;
case QuestionType.Code:
@@ -63,7 +68,7 @@ export default function create_input({ question, public_state }: QuestionProp, h
break;
default:
- result = <TextArea handler={handler}/>;
+ result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
}
/* eslint-enable react/react-in-jsx-scope */
diff --git a/src/components/Question.tsx b/src/components/Question.tsx
index 735d69b..0af745e 100644
--- a/src/components/Question.tsx
+++ b/src/components/Question.tsx
@@ -5,6 +5,7 @@ import React, { ChangeEvent } from "react";
import { Question, QuestionType } from "../api/question";
import { selectable } from "../commonStyles";
import create_input from "./InputTypes";
+import ErrorMessage from "./ErrorMessage";
const skip_normal_state: Array<QuestionType> = [
QuestionType.Radio,
@@ -17,6 +18,9 @@ const skip_normal_state: Array<QuestionType> = [
export type QuestionProp = {
question: Question,
public_state: Map<string, string | boolean | null>,
+ scroll_ref: React.RefObject<HTMLDivElement>,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ focus_ref: React.RefObject<any>
}
class RenderedQuestion extends React.Component<QuestionProp> {
@@ -27,6 +31,10 @@ class RenderedQuestion extends React.Component<QuestionProp> {
} else {
this.handler = this.normal_handler.bind(this);
}
+ this.blurHandler = this.blurHandler.bind(this);
+
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
if (!skip_normal_state.includes(props.question.type)) {
this.setPublicState("value", "");
@@ -41,6 +49,18 @@ class RenderedQuestion extends React.Component<QuestionProp> {
// This is here to allow dynamic selection between the general handler, and the textarea handler.
handler(_: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {} // eslint-disable-line
+ blurHandler(): void {
+ if (this.props.question.required) {
+ if (!this.props.public_state.get("value")) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+ }
+
normal_handler(event: ChangeEvent<HTMLInputElement>): void {
let target: string;
let value: string | boolean;
@@ -73,12 +93,90 @@ class RenderedQuestion extends React.Component<QuestionProp> {
event.target.parentElement.classList.toggle("unselected");
event.target.parentElement.classList.toggle("selected");
}
+
+ const options: string | string[] = this.props.question.data["options"];
+ switch (event.target.type) {
+ case "text":
+ this.setPublicState("valid", true);
+ break;
+
+ case "checkbox":
+ // We need to check this here, because checkbox doesn't have onBlur
+ if (this.props.question.required && typeof options !== "string") {
+ const keys: string[] = [];
+ options.forEach((val, index) => {
+ keys.push(`${("000" + index).slice(-4)}. ${val}`);
+ });
+ if (keys.every(v => !this.props.public_state.get(v))) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+ break;
+
+ case "radio":
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
+ break;
+ }
}
text_area_handler(event: ChangeEvent<HTMLTextAreaElement>): void {
+ // We will validate again when focusing out.
+ this.setPublicState("valid", true);
+ this.setPublicState("error", "");
+
this.setPublicState("value", event.target.value);
}
+ validateField(): void {
+ if (!this.props.question.required) {
+ return;
+ }
+
+ let invalid = false;
+ const options: string | string[] = this.props.question.data["options"];
+ switch (this.props.question.type) {
+ case QuestionType.TextArea:
+ case QuestionType.ShortText:
+ if (this.props.public_state.get("value") === "") {
+ invalid = true;
+ }
+ break;
+
+ case QuestionType.Select:
+ case QuestionType.Range:
+ case QuestionType.Radio:
+ if (!this.props.public_state.get("value")) {
+ invalid = true;
+ }
+ break;
+
+ case QuestionType.Checkbox:
+ if (typeof options !== "string") {
+ const keys: string[] = [];
+ options.forEach((val, index) => {
+ keys.push(`${("000" + index).slice(-4)}. ${val}`);
+ });
+ if (keys.every(v => !this.props.public_state.get(v))) {
+ invalid = true;
+ }
+ }
+ break;
+ }
+
+ if (invalid) {
+ this.setPublicState("error", "Field must be filled.");
+ this.setPublicState("valid", false);
+ } else {
+ this.setPublicState("error", "");
+ this.setPublicState("valid", true);
+ }
+ }
+
componentDidMount(): void {
// Initialize defaults for complex and nested fields
const options: string | string[] = this.props.question.data["options"];
@@ -151,12 +249,22 @@ class RenderedQuestion extends React.Component<QuestionProp> {
margin-left: 0.2rem;
}
`;
+ let valid = true;
+ if (!this.props.public_state.get("valid")) {
+ valid = false;
+ }
+ const rawError = this.props.public_state.get("error");
+ let error = "";
+ if (typeof rawError === "string") {
+ error = rawError;
+ }
- return <div>
+ return <div ref={this.props.scroll_ref}>
<h2 css={[selectable, requiredStarStyles]}>
{question.name}<span className={question.required ? "required" : ""}>*</span>
</h2>
- { create_input(this.props, this.handler) }
+ { create_input(this.props, this.handler, this.blurHandler, this.props.focus_ref) }
+ <ErrorMessage show={!valid} message={error} />
<hr css={css`color: gray; margin: 3rem 0;`}/>
</div>;
}
diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx
index 4e13b8a..fa63282 100644
--- a/src/pages/FormPage.tsx
+++ b/src/pages/FormPage.tsx
@@ -2,8 +2,9 @@
import { jsx, css } from "@emotion/react";
import { Link } from "react-router-dom";
-import React, { SyntheticEvent, useEffect, useState } from "react";
+import React, { SyntheticEvent, useEffect, useState, createRef } from "react";
import { useParams } from "react-router";
+import { PropagateLoader } from "react-spinners";
import HeaderBar from "../components/HeaderBar";
import RenderedQuestion from "../components/Question";
@@ -14,8 +15,9 @@ 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 { submitStyles, unselectable } from "../commonStyles";
+import { Question, QuestionType } from "../api/question";
+import ApiClient from "../api/client";
interface PathParams {
id: string
@@ -27,13 +29,12 @@ interface NavigationProps {
}
class Navigation extends React.Component<NavigationProps> {
- containerStyles = css`
+ static containerStyles = css`
margin: auto;
width: 50%;
text-align: center;
font-size: 1.5rem;
- white-space: nowrap;
> div {
display: inline-block;
@@ -70,12 +71,13 @@ class Navigation extends React.Component<NavigationProps> {
}
`;
- returnStyles = css`
- padding: 0.55rem 2.2rem;
+ 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;
@@ -83,7 +85,6 @@ class Navigation extends React.Component<NavigationProps> {
:hover {
background-color: ${colors.darkerGreyple};
}
- }
`;
constructor(props: NavigationProps) {
@@ -104,9 +105,9 @@ class Navigation extends React.Component<NavigationProps> {
}
return (
- <div css={[unselectable, this.containerStyles]}>
+ <div css={[unselectable, Navigation.containerStyles]}>
<div className={ "return_button" + (this.props.form_state ? "" : " closed") }>
- <Link to="/" css={this.returnStyles}>Return Home</Link>
+ <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
</div>
<br css={this.separatorStyles}/>
<div css={submitStyles}>{ submit }</div>
@@ -144,6 +145,8 @@ function FormPage(): JSX.Element {
const { id } = useParams<PathParams>();
const [form, setForm] = useState<Form>();
+ const [sending, setSending] = useState<boolean>();
+ const [sent, setSent] = useState<boolean>();
useEffect(() => {
getForm(id).then(form => {
@@ -151,26 +154,118 @@ function FormPage(): JSX.Element {
});
}, []);
+ if (form && sent) {
+ const thanksStyle = css`font-family: "Uni Sans", "Hind", "Arial", sans-serif; margin-top: 15.5rem;`;
+ const divStyle = css`width: 80%;`;
+ return (
+ <div>
+ <HeaderBar title={form.name} description={form.description}/>
+ <div css={[unselectable, Navigation.containerStyles, divStyle]}>
+ <h3 css={thanksStyle}>{form.submitted_text ?? "Thanks for your response!"}</h3>
+ <div className={ "return_button closed" }>
+ <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ if (sending) {
+ return (
+ <div>
+ <HeaderBar title={"Submitting..."}/>
+ <div css={{display: "flex", justifyContent: "center", paddingTop: "40px"}}>
+ <PropagateLoader color="white"/>
+ </div>
+ </div>
+ );
+ }
+
if (!form) {
return <Loading/>;
}
+ const refMap: Map<string, React.RefObject<RenderedQuestion>> = new Map();
const questions = form.questions.map((question, index) => {
- return <RenderedQuestion question={question} public_state={new Map()} key={index + Date.now()}/>;
+ const questionRef = createRef<RenderedQuestion>();
+ refMap.set(question.id, questionRef);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return <RenderedQuestion ref={questionRef} focus_ref={createRef<any>()} scroll_ref={createRef<HTMLDivElement>()} question={question} public_state={new Map()} key={index + Date.now()}/>;
});
- function handleSubmit(event: SyntheticEvent) {
+ 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) {
+ 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;
+ }
+
+ setSending(true);
+
+ const answers: { [key: string]: unknown } = {};
questions.forEach(prop => {
- const question = prop.props.question;
+ const question: Question = prop.props.question;
+ const options: string | string[] = question.data["options"];
- // TODO: Parse input from each question, and submit
+ // 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<string, string> = 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:
- console.log(question.id, prop.props.public_state);
+ answers[question.id] = prop.props.public_state.get("value");
}
});
- event.preventDefault();
+ await ApiClient.post(`forms/submit/${id}`, {response: answers});
+ setSending(false);
+ setSent(true);
}
const open: boolean = form.features.includes(FormFeatures.Open);
diff --git a/src/tests/components/FormListing.test.tsx b/src/tests/components/FormListing.test.tsx
index f269dbf..2116e48 100644
--- a/src/tests/components/FormListing.test.tsx
+++ b/src/tests/components/FormListing.test.tsx
@@ -21,7 +21,8 @@ const openFormListing: Form = {
required: false
}
],
- webhook: null
+ webhook: null,
+ submitted_text: null
};
const closedFormListing: Form = {
@@ -38,7 +39,8 @@ const closedFormListing: Form = {
required: false
}
],
- webhook: null
+ webhook: null,
+ submitted_text: null
};
test("renders form listing with specified title", () => {
diff --git a/src/tests/pages/LandingPage.test.tsx b/src/tests/pages/LandingPage.test.tsx
index e461815..6f8a530 100644
--- a/src/tests/pages/LandingPage.test.tsx
+++ b/src/tests/pages/LandingPage.test.tsx
@@ -21,7 +21,8 @@ const testingForm: forms.Form = {
required: true
}
],
- "webhook": null
+ "webhook": null,
+ submitted_text: null
};
test("renders landing page", () => {
diff --git a/yarn.lock b/yarn.lock
index 131a86c..b0d3a24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8704,10 +8704,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
- version "4.1.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.4.tgz#f058636e2f4f83f94ddaae07b20fd5e14598432f"
- integrity sha512-+Uru0t8qIRgjuCpiSPpfGuhHecMllk5Zsazj5LZvVsEStEjmIRRBZe+jHjGQvsgS7M1wONy2PQXd67EMyV6acg==
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
+ integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"