From a7222e1ccc3bf8bfc2b3e1f3b5f1806ac4ec7be0 Mon Sep 17 00:00:00 2001 From: Natalie Chan <86009011+natapokie@users.noreply.github.com> Date: Sun, 27 Aug 2023 18:21:21 -0400 Subject: [PATCH] Ieee 272 implement item incident form (#503) * incident form init * implementing form components * fixing form style * fixing position of menu items * implement back button * adding helper text for radio * fixing button * cleaning up unused vars * adding report broken lost button * init test * fixing link * reset form on successful submission * setting up testing * fix test * fixing tests * removing unused code * adding name to readme * fix type error * padding qty to new page & adding snackbar * rewriting formik with map * fixing responsive issues on form * fixing checkedout tables style * adding map to radio options * using map in form * fixing textfield width * fixing validation onchange * adding more unit tests to check for rendering of form components and submit button * adding more padding between form elements and labels * removing inline styles (sorry bad habitsgit status) * removing unused code and console.log & changing url format a little bit to pass more info about hardware for incident form * added yup validation to check if the input string is 0 for qty, is so don't submit the form * adding conditional redirects * cleaning up code * fixing tests for incident form to account for conditional rendering of the component when there are search params * fixing type, had to use any bc not sure about how the URLSearchParams typing works * fixing dependencies & adding const * used to bypass react-hooks/exhaustive-deps so that dependencies don't need to be in useEffect array, so that the useEffect will fire on first render * removing unused code --------- Co-authored-by: Mustafa --- README.md | 1 + .../assets/images/icons/arrow-left-solid.svg | 1 + .../dashboard/ItemTable/ItemTable.tsx | 30 +- .../OrderTables/OrderTables.module.scss | 23 +- .../general/OrderTables/OrderTables.tsx | 27 +- .../src/pages/IncidentForm/IncidentForm.js | 16 - .../IncidentForm/IncidentForm.module.scss | 74 +++ .../pages/IncidentForm/IncidentForm.test.js | 10 - .../pages/IncidentForm/IncidentForm.test.tsx | 60 +++ .../src/pages/IncidentForm/IncidentForm.tsx | 457 ++++++++++++++++++ 10 files changed, 637 insertions(+), 62 deletions(-) create mode 100644 hackathon_site/dashboard/frontend/src/assets/images/icons/arrow-left-solid.svg delete mode 100644 hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js delete mode 100644 hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js create mode 100644 hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx create mode 100644 hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.tsx diff --git a/README.md b/README.md index 1514984b3..2d5112029 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A website template for hackathons run by [IEEE University of Toronto Student Bra - Kenny Cui - Himanish Jindal - Abubukker Chaudhary +- Natalie Chan ## Contents - [Requirements](#requirements) diff --git a/hackathon_site/dashboard/frontend/src/assets/images/icons/arrow-left-solid.svg b/hackathon_site/dashboard/frontend/src/assets/images/icons/arrow-left-solid.svg new file mode 100644 index 000000000..0f1f2cbed --- /dev/null +++ b/hackathon_site/dashboard/frontend/src/assets/images/icons/arrow-left-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx index b27ceae3b..c8a5ad6ff 100644 --- a/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx +++ b/hackathon_site/dashboard/frontend/src/components/dashboard/ItemTable/ItemTable.tsx @@ -43,6 +43,7 @@ import { GeneralReturnTable, } from "components/general/OrderTables/OrderTables"; import PopupModal from "components/general/PopupModal/PopupModal"; +import { Link } from "react-router-dom"; import { sortPendingOrders, sortReturnedOrders } from "api/helpers"; import { sortCheckedOutOrders } from "api/helpers"; @@ -183,17 +184,24 @@ export const CheckedOutTables = () => {row.quantityGranted} - {/* TODO: Add back in when incident reports are being used*/} - {/* {*/} - {/* reportIncident(row.id);*/} - {/* push("/incident-form");*/} - {/* }}*/} - {/*>*/} - {/* Report broken/lost*/} - {/**/} + + + ) diff --git a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.module.scss b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.module.scss index b55e9b7a0..b54b3bc68 100644 --- a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.module.scss +++ b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.module.scss @@ -29,6 +29,24 @@ } } +.titleChip { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 20px 0 5px 0; + + @include responsive(sm-down) { + width: 100%; + flex-direction: column; + justify-content: flex-start; + } +} + +.titleChipText { + margin-bottom: 4px; +} + .itemImg { height: 80px; width: 80px; @@ -70,11 +88,6 @@ } } -.chipPadding { - position: relative; - left: 630px; -} - .noWrap { white-space: nowrap; } diff --git a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.tsx b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.tsx index 6dc79eb04..a19795da3 100644 --- a/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.tsx +++ b/hackathon_site/dashboard/frontend/src/components/general/OrderTables/OrderTables.tsx @@ -96,41 +96,28 @@ export const GeneralOrderTableTitle = ({ updatedTime, additionalChipFormatting, }: GeneralOrderTableTitleProps) => ( - +
Order #{orderId} - {orderStatus && ( - - - - )} + {orderStatus && } {createdTime && updatedTime ? ( - +
Created at: , formatDateTime(createdTime)]} icon={} - className={`${styles.chipPurple} ${styles.chip} ${ - additionalChipFormatting ? styles.chipPadding : "" - }`} + className={`${styles.chipPurple} ${styles.chip}`} /> - {" "} Updated at: , formatDateTime(updatedTime)]} icon={} - className={`${styles.chipBlue} ${styles.chip} ${ - additionalChipFormatting ? styles.chipPadding : "" - }`} + className={`${styles.chipBlue} ${styles.chip}`} /> - +
) : null} -
+
); export const GeneralPendingTable = ({ diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js deleted file mode 100644 index 040ba35ca..000000000 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -// import styles from "./IncidentForm.module.scss"; -import Header from "components/general/Header/Header"; -import Typography from "@material-ui/core/Typography"; - -const IncidentForm = () => { - return ( - <> -
- Broken/Lost Item Incident -

IEEEEEE

- - ); -}; - -export default IncidentForm; diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.module.scss b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.module.scss index e69de29bb..5e80429d4 100644 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.module.scss +++ b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.module.scss @@ -0,0 +1,74 @@ +@import "assets/abstracts/mixins.scss"; +@import "assets/abstracts/variables.scss"; + +.arrowIcon { + width: 50px; + height: 50px; + padding: 10px; + align-self: flex-start; + + transform: translateX(0px), scale(1); + transition: transform ease-in-out 300ms; + + &:hover { + transform: translateX(-10px); + transition: transform ease-in-out 300ms; + } + + &:active { + transform: scale(0.8); + transition: transform 300ms; + } +} + +.formContainer { + display: flex; + flex-direction: column; + width: max(40vw, 640px); + + @include responsive(md-down) { + width: 100%; + } +} + +.formComponentContainer { + all: unset; + padding-bottom: 20px; +} + +.card { + align-self: center; +} + +.cardContent { + padding: 20px; +} + +.pageContent { + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.dropdown { + width: 200px; + + @include responsive(md-down) { + width: 30%; + } +} + +.textboxFill { + width: 100%; +} + +.submitButton { + align-self: center; + width: fit-content; + margin-top: 10px; +} + +.titleMargin { + margin-bottom: 20px; +} diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js deleted file mode 100644 index f35a29c3c..000000000 --- a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -import { render } from "testing/utils"; - -import IncidentForm from "./IncidentForm"; - -test("renders without crashing", () => { - const { getByText } = render(); - expect(getByText("IEEEEEE")).toBeInTheDocument(); -}); diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx new file mode 100644 index 000000000..bf5763181 --- /dev/null +++ b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from "testing/utils"; // Import your testing utilities here +import IncidentForm from "./IncidentForm"; +import configureStore, { MockStore } from "redux-mock-store"; + +describe("IncidentForm", () => { + const mockStore = configureStore([]); + let store: MockStore; + + beforeEach(() => { + store = mockStore({ + yourReducerKey: { + id: 10, + quantityRequested: 3, + quantityGranted: 2, + }, + }); + }); + + it("renders correctly when searchParams is empty", () => { + render(); + + // Assert that the component renders nothing when searchParams is empty + expect(screen.queryByText("Item Incident Form")).toBeNull(); // Update the text according to your component's content + }); + + it("renders IncidentFormRender when searchParams is not empty", () => { + const mockSearchParams: any = { + get: jest.fn(() => + JSON.stringify({ id: 10, quantityRequested: 3, quantityGranted: 2 }) + ), + toString: jest.fn(() => "mockQueryString"), + }; + + jest.spyOn(global, "URLSearchParams").mockImplementation( + () => mockSearchParams + ); + + render(); + + // Assert that mockSearchParams methods are called + expect(mockSearchParams.toString).toHaveBeenCalled(); + expect(mockSearchParams.get).toHaveBeenCalledWith("data"); + + // Assert that the component renders IncidentFormRender + expect(screen.queryByText("Item Incident Form")).toBeInTheDocument(); // Update the text according to your component's content + + // Renders all form components + const radios = screen.getAllByRole("radio"); + const dropdown = screen.getByTestId("qty-dropdown"); + const textareas = screen.getAllByRole("textbox"); + + expect(radios).toHaveLength(3); // three radio buttons + expect(dropdown).toBeInTheDocument(); // one dropdown + expect(textareas).toHaveLength(3); // three text inputs + + // Renders submit button + const submitButton = screen.getByRole("button", { name: "Submit" }); + expect(submitButton).toBeInTheDocument(); + }); +}); diff --git a/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.tsx b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.tsx new file mode 100644 index 000000000..4b05bde8c --- /dev/null +++ b/hackathon_site/dashboard/frontend/src/pages/IncidentForm/IncidentForm.tsx @@ -0,0 +1,457 @@ +import React, { useEffect, useRef } from "react"; +import styles from "./IncidentForm.module.scss"; +import { + Typography, + Card, + CardContent, + RadioGroup, + FormControlLabel, + Radio, + Select, + MenuItem, + Button, + TextField, + FormControl, + InputLabel, + FormHelperText, + makeStyles, +} from "@material-ui/core"; +import { useHistory, useLocation } from "react-router-dom"; +import { Formik, Field, FormikValues } from "formik"; +import * as Yup from "yup"; +import Header from "components/general/Header/Header"; +import ArrowLeft from "../../assets/images/icons/arrow-left-solid.svg"; +import { displaySnackbar } from "slices/ui/uiSlice"; +import { useDispatch } from "react-redux"; + +export const INCIDENT_ERROR_MSG = { + state: "Please select an option", + qtyEmpty: "Please indicate the quantity", + qtyZero: "Cannot submit incident form for 0 items", + what: "Please indicate what happened to the hardware", + when: "Please indicate when this occurred", + where: "Please indicate where this occurred", +}; + +const useStyles = makeStyles({ + typographyLabel: { + paddingBottom: "10px", // Adjust the value as per your requirement + }, +}); + +const capitalizeFirstLetter = (str: string) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +const validationSchema = Yup.object({ + state: Yup.string().required(INCIDENT_ERROR_MSG.state), + qty: Yup.string() + .test("not-zero", INCIDENT_ERROR_MSG.qtyZero, (value) => value !== "0") + .required(INCIDENT_ERROR_MSG.qtyEmpty), + what: Yup.string().required(INCIDENT_ERROR_MSG.what), + when: Yup.string().required(INCIDENT_ERROR_MSG.when), + where: Yup.string().required(INCIDENT_ERROR_MSG.where), +}); + +const initialValues = { + state: "", + qty: "", + what: "", + when: "", + where: "", +}; + +const createQuantityList = (number: number) => { + let entry = []; + + for (let i = 1; i <= number; i++) { + entry.push( + + {i} + + ); + } + + return entry; +}; + +// used to bypass eslint react-hooks/exhaustive-deps +const useFirstRenderEffect = (callback: () => void) => { + const isFirstRender = useRef(true); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + callback(); + } + }, [callback]); +}; + +const IncidentForm = () => { + const dispatch = useDispatch(); + const history = useHistory(); + const location = useLocation(); + + // get info from url + const searchParams = new URLSearchParams(location.search); + + useFirstRenderEffect(() => { + if (searchParams.toString() === "") { + // check if there are empty query params + dispatch( + displaySnackbar({ + message: `You are not authorized to access this page.`, + options: { + variant: "error", + }, + }) + ); + history.push("/404"); // redirect to 404 page + } + }); + + let hardwareQuantity: number; // quantity used for dropdown + try { + const data = searchParams.get("data") ?? ""; + const checkedOutOrder = JSON.parse(data); // parse string from url into an object + hardwareQuantity = checkedOutOrder?.quantityGranted; // set the hardware qty for dropdown + } catch { + hardwareQuantity = 0; // set the qty to 0 if there is an error parsing + } + + return ( + <> + {searchParams.toString() === "" ? ( + <> + ) : ( + + )} + + ); +}; + +const IncidentFormRender = ({ hardwareQuantity }: { hardwareQuantity: number }) => { + const muiClasses = useStyles(); + + const dispatch = useDispatch(); + const history = useHistory(); + + const handleSubmit = async (values: FormikValues, { resetForm }: any) => { + // TODO: submit the form + + resetForm(); + // display success snackbar + dispatch( + displaySnackbar({ + message: `Successfully submitted incident form!`, + options: { + variant: "success", + }, + }) + ); + // navigate back to previous page + history.goBack(); + }; + + return ( + <> +
+
+ history.goBack()} + alt="back arrow" + className={styles.arrowIcon} + /> + + +
+
+ Item Incident Form +
+ + + {({ errors, handleSubmit, handleChange, values }) => { + const incidentFormComponents = [ + { + type: "radio", + id: "state", + name: "state", + options: ["broken", "lost", "other"], + helperText: errors?.state, + testId: "radio-state", + }, + { + type: "select", + id: "qty", + name: "qty", + label: "Qty", + description: + "Number of Grove temperature and humidity sensor pro affected.", + value: values.qty, + error: !!errors?.qty, + helperText: errors?.qty, + testId: "qty-dropdown", + }, + { + type: "text", + id: "what", + name: "what", + label: "", + description: "", + placeholder: + "What happened to the hardware?", + value: values.what, + error: !!errors?.what, + helperText: errors?.what, + }, + { + type: "text", + id: "when", + name: "when", + label: "", + description: "", + placeholder: "When did this occur?", + value: values.when, + error: !!errors?.when, + helperText: errors?.when, + }, + { + type: "text", + id: "where", + name: "where", + label: "", + description: "", + placeholder: "Where did this occur?", + value: values.where, + error: !!errors?.where, + helperText: errors?.where, + }, + ]; + + return ( +
+
+ {incidentFormComponents.map( + (item, index) => { + return ( +
+ {/* Used if the item has a description before the input form */} + {item?.description ? ( + + { + item.description + } + + ) : ( + <> + )} + + {item.type === + "radio" ? ( + <> + + + {({ + field, + }: { + field: any; + }) => ( + <> + + {item.options?.map( + ( + option: string, + index + ) => { + return ( + + + } + label={capitalizeFirstLetter( + option + )} + /> + + ); + } + )} + + + )} + + + { + item.helperText + } + + + + ) : item.type === + "select" ? ( + <> + + + { + item.label + } + + + + { + item.helperText + } + + + + ) : item.type === + "text" ? ( +
+ +
+ ) : ( + <> + )} +
+ ); + } + )} +
+ +
+
+
+ ); + }} +
+
+
+
+
+ + ); +}; + +export default IncidentForm;