diff --git a/public/css/fonts/Inter-Bold.woff b/public/css/fonts/Inter-Bold.woff deleted file mode 100644 index eaf3d4bf..00000000 Binary files a/public/css/fonts/Inter-Bold.woff and /dev/null differ diff --git a/public/css/fonts/Inter-Bold.woff2 b/public/css/fonts/Inter-Bold.woff2 deleted file mode 100644 index 2846f29c..00000000 Binary files a/public/css/fonts/Inter-Bold.woff2 and /dev/null differ diff --git a/public/css/fonts/Inter-ExtraBold.woff b/public/css/fonts/Inter-ExtraBold.woff deleted file mode 100644 index c2c17ede..00000000 Binary files a/public/css/fonts/Inter-ExtraBold.woff and /dev/null differ diff --git a/public/css/fonts/Inter-ExtraBold.woff2 b/public/css/fonts/Inter-ExtraBold.woff2 deleted file mode 100644 index c24c2bdc..00000000 Binary files a/public/css/fonts/Inter-ExtraBold.woff2 and /dev/null differ diff --git a/public/css/fonts/Inter-Light.woff b/public/css/fonts/Inter-Light.woff deleted file mode 100644 index c496464d..00000000 Binary files a/public/css/fonts/Inter-Light.woff and /dev/null differ diff --git a/public/css/fonts/Inter-Light.woff2 b/public/css/fonts/Inter-Light.woff2 deleted file mode 100644 index bc4be665..00000000 Binary files a/public/css/fonts/Inter-Light.woff2 and /dev/null differ diff --git a/public/css/fonts/Inter-Medium.woff b/public/css/fonts/Inter-Medium.woff deleted file mode 100644 index d546843f..00000000 Binary files a/public/css/fonts/Inter-Medium.woff and /dev/null differ diff --git a/public/css/fonts/Inter-Medium.woff2 b/public/css/fonts/Inter-Medium.woff2 deleted file mode 100644 index f92498a2..00000000 Binary files a/public/css/fonts/Inter-Medium.woff2 and /dev/null differ diff --git a/public/css/fonts/Inter-Regular.woff b/public/css/fonts/Inter-Regular.woff deleted file mode 100644 index 62d3a618..00000000 Binary files a/public/css/fonts/Inter-Regular.woff and /dev/null differ diff --git a/public/css/fonts/Inter-Regular.woff2 b/public/css/fonts/Inter-Regular.woff2 deleted file mode 100644 index 6c2b6893..00000000 Binary files a/public/css/fonts/Inter-Regular.woff2 and /dev/null differ diff --git a/public/css/fonts/Inter-SemiBold.woff b/public/css/fonts/Inter-SemiBold.woff deleted file mode 100644 index a815f43a..00000000 Binary files a/public/css/fonts/Inter-SemiBold.woff and /dev/null differ diff --git a/public/css/fonts/Inter-SemiBold.woff2 b/public/css/fonts/Inter-SemiBold.woff2 deleted file mode 100644 index 611e90c9..00000000 Binary files a/public/css/fonts/Inter-SemiBold.woff2 and /dev/null differ diff --git a/public/css/fonts/OpenSans-VariableFont.ttf b/public/css/fonts/OpenSans-VariableFont.ttf deleted file mode 100644 index 9cae0f79..00000000 Binary files a/public/css/fonts/OpenSans-VariableFont.ttf and /dev/null differ diff --git a/public/css/fonts/Poppins-Bold.ttf b/public/css/fonts/Poppins-Bold.ttf deleted file mode 100644 index 00559eeb..00000000 Binary files a/public/css/fonts/Poppins-Bold.ttf and /dev/null differ diff --git a/public/css/fonts/Poppins-Light.ttf b/public/css/fonts/Poppins-Light.ttf deleted file mode 100644 index bc36bcc2..00000000 Binary files a/public/css/fonts/Poppins-Light.ttf and /dev/null differ diff --git a/public/css/fonts/Poppins-Medium.ttf b/public/css/fonts/Poppins-Medium.ttf deleted file mode 100644 index 6bcdcc27..00000000 Binary files a/public/css/fonts/Poppins-Medium.ttf and /dev/null differ diff --git a/public/css/fonts/Poppins-Regular.ttf b/public/css/fonts/Poppins-Regular.ttf deleted file mode 100644 index 9f0c71b7..00000000 Binary files a/public/css/fonts/Poppins-Regular.ttf and /dev/null differ diff --git a/public/css/fonts/Poppins-SemiBold.ttf b/public/css/fonts/Poppins-SemiBold.ttf deleted file mode 100644 index 74c726e3..00000000 Binary files a/public/css/fonts/Poppins-SemiBold.ttf and /dev/null differ diff --git a/public/css/fonts/ReemKufi-VariableFont_wght.ttf b/public/css/fonts/ReemKufi-VariableFont_wght.ttf deleted file mode 100644 index af0a7389..00000000 Binary files a/public/css/fonts/ReemKufi-VariableFont_wght.ttf and /dev/null differ diff --git a/public/css/index.css b/public/css/index.css deleted file mode 100644 index edec1319..00000000 --- a/public/css/index.css +++ /dev/null @@ -1,81 +0,0 @@ -@font-face { - font-family:"poppins"; - src:url(./fonts/Poppins-Light.ttf); - font-weight:300; -} - -@font-face { - font-family:"poppins"; - src:url(./fonts/Poppins-Regular.ttf); - font-weight:400; -} - -@font-face { - font-family:"poppins"; - src:url(./fonts/Poppins-Medium.ttf); - font-weight:500; -} - -@font-face { - font-family:"poppins"; - src:url(./fonts/Poppins-SemiBold.ttf); - font-weight:600; -} - -@font-face { - font-family:"poppins"; - src:url(./fonts/Poppins-Bold.ttf); - font-weight:700; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-Light.woff2) format("woff2"), - url(./fonts/Inter-Light.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:300;font-stretch:normal; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-Regular.woff2) format("woff2"), - url(./fonts/Inter-Regular.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:400;font-stretch:normal; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-Medium.woff2) format("woff2"), - url(./fonts/Inter-Medium.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:500;font-stretch:normal; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-SemiBold.woff2) format("woff2"), - url(./fonts/Inter-SemiBold.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:600;font-stretch:normal; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-Bold.woff2) format("woff2"), - url(./fonts/Inter-Bold.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:700;font-stretch:normal; -} - -@font-face { - font-family:"Inter"; - src:url(./fonts/Inter-ExtraBold.woff2) format("woff2"), - url(./fonts/Inter-ExtraBold.woff) format("woff"); - font-display:auto;font-style:normal;font-weight:800;font-stretch:normal; -} - -@font-face { - font-family:"Open Sans"; - src:url(./fonts/OpenSans-VariableFont.ttf); -} - -@font-face { - font-family:"Reem Kufi"; - src:url(./fonts/ReemKufi-VariableFont_wght.ttf); -} \ No newline at end of file diff --git a/src/assets/dataSubmissions/summary_banner.png b/src/assets/banner/summary_banner.png similarity index 100% rename from src/assets/dataSubmissions/summary_banner.png rename to src/assets/banner/summary_banner.png diff --git a/src/assets/dataSubmissions/dashboard_banner.png b/src/assets/dataSubmissions/dashboard_banner.png deleted file mode 100644 index ecb72eab..00000000 Binary files a/src/assets/dataSubmissions/dashboard_banner.png and /dev/null differ diff --git a/src/assets/history/submissionRequest/index.tsx b/src/assets/history/submissionRequest/index.tsx deleted file mode 100644 index 9d3f0127..00000000 --- a/src/assets/history/submissionRequest/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import New from "./SubmissionRequestNew.svg"; -import Submitted from "./SubmissionRequestSubmitted.svg"; -import Rejected from "./Rejected.svg"; -import Approved from "./Approved.svg"; -import UnderReview from "./UnderReview.svg"; -import StatusApproved from "./StatusApproved.svg"; -import StatusRejected from "./StatusRejected.svg"; -import InProgress from "./InProgress.svg"; - -export type IconType = { - [key: string]: string; -}; - -/** - * Map of ApplicationStatus to Icon for History Modal - * - * @see ApplicationStatus - */ -export const HistoryIconMap: IconType = { - New, - Submitted, - Rejected, - Approved, - "In Review": UnderReview, - "In Progress": InProgress, -}; - -/** - * Map of ApplicationStatus to Icon for Status Bar - * - * @see ApplicationStatus - */ -export const StatusIconMap: IconType = { - Rejected: StatusRejected, - Approved: StatusApproved, -}; diff --git a/src/components/DataSubmissions/CopyAdornment.test.tsx b/src/components/DataSubmissions/CopyAdornment.test.tsx new file mode 100644 index 00000000..a6f3e2d8 --- /dev/null +++ b/src/components/DataSubmissions/CopyAdornment.test.tsx @@ -0,0 +1,65 @@ +import { render } from "@testing-library/react"; +import { axe } from "jest-axe"; +import userEvent from "@testing-library/user-event"; +import CopyAdornment from "./CopyAdornment"; + +const mockWriteText = jest.fn(); +Object.assign(navigator, { + clipboard: { + writeText: mockWriteText, + }, +}); + +describe("Accessibility", () => { + it("should not have any violations", async () => { + const { container } = render(); + + expect(await axe(container)).toHaveNoViolations(); + }); + + it("should not have any violations (no _ID)", async () => { + const { container } = render(); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); + +describe("Implementation Requirements", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should render the Submission ID and copy button", () => { + const { getByTestId } = render(); + + expect(getByTestId("data-submission-id-value")).toBeInTheDocument(); + expect(getByTestId("data-submission-id-value")).toHaveTextContent("abc-123-this-is-a-id"); + + expect(getByTestId("data-submission-copy-id-button")).toBeInTheDocument(); + expect(getByTestId("data-submission-copy-id-button")).toBeEnabled(); + }); + + it("should copy the ID to the clipboard when clicking the copy button", () => { + const { getByTestId } = render(); + + userEvent.click(getByTestId("data-submission-copy-id-button")); + + expect(mockWriteText).toHaveBeenCalledWith("abc-123-this-is-a-id"); + }); + + it("should not copy the ID to the clipboard when no _ID is provided", () => { + const { getByTestId } = render(); + + userEvent.click(getByTestId("data-submission-copy-id-button"), null, { + skipPointerEventsCheck: true, + }); + + expect(mockWriteText).not.toHaveBeenCalled(); + }); + + it("should disable the copy button when no _ID is provided", () => { + const { getByTestId } = render(); + + expect(getByTestId("data-submission-copy-id-button")).toBeDisabled(); + }); +}); diff --git a/src/components/DataSubmissions/CopyAdornment.tsx b/src/components/DataSubmissions/CopyAdornment.tsx new file mode 100644 index 00000000..6bc7e8b4 --- /dev/null +++ b/src/components/DataSubmissions/CopyAdornment.tsx @@ -0,0 +1,90 @@ +import { FC, memo } from "react"; +import { IconButton, Stack, styled, Typography } from "@mui/material"; +import { isEqual } from "lodash"; +import { ReactComponent as CopyIconSvg } from "../../assets/icons/copy_icon_2.svg"; + +const StyledCopyWrapper = styled(Stack)({ + height: "42px", + width: "fit-content", + minWidth: "508px", + padding: "11px 20px", + borderRadius: "8px 8px 0px 0px", + border: "1.25px solid #6DADDB", + borderBottom: 0, + background: "#fff", + color: "#125868", +}); + +const StyledCopyLabel = styled(Typography)({ + fontFamily: "'Nunito', 'Rubik', sans-serif", + fontSize: "12px", + fontStyle: "normal", + fontWeight: 800, + lineHeight: "19.6px", + letterSpacing: "0.24px", + textTransform: "uppercase", + userSelect: "none", +}); + +const StyledCopyValue = styled(Typography)({ + fontFamily: "'Nunito', 'Rubik', sans-serif", + fontSize: "16px", + fontStyle: "normal", + fontWeight: 400, + lineHeight: "19.6px", + letterSpacing: "0.32px", + userSelect: "all", +}); + +const StyledCopyIDButton = styled(IconButton)({ + padding: 0, + "&.MuiIconButton-root.Mui-disabled": { + color: "#B0B0B0", + }, + marginLeft: "auto !important", +}); + +type Props = { + /** + * The Data Submission ID + * + * @note This is passed as a prop instead of using the SubmissionContext to prevent a delay in displaying the ID + */ + _id: string; +}; + +/** + * Provides Data Submission ID display and copy functionality + * + * @returns {React.FC} + */ +const CopyAdornment: FC = ({ _id }) => { + const handleCopyID = () => { + if (!_id) { + return; + } + + navigator.clipboard.writeText(_id); + }; + + return ( + + + SUBMISSION ID: + + + {_id} + + + + + + ); +}; + +export default memo(CopyAdornment, isEqual); diff --git a/src/components/DataSubmissions/DataSubmissionIconMap.tsx b/src/components/DataSubmissions/DataSubmissionIconMap.tsx index 6b0bbdce..42add3f2 100644 --- a/src/components/DataSubmissions/DataSubmissionIconMap.tsx +++ b/src/components/DataSubmissions/DataSubmissionIconMap.tsx @@ -7,7 +7,7 @@ import Rejected from "../../assets/history/dataSubmission/rejected.svg"; import Completed from "../../assets/history/dataSubmission/completed.svg"; import Archived from "../../assets/history/dataSubmission/archived.svg"; import Canceled from "../../assets/history/dataSubmission/canceled.svg"; -import { IconType } from "../Shared/HistoryDialog"; +import { IconType } from "../HistoryDialog"; /** * Map of ApplicationStatus to Icon for History Modal diff --git a/src/components/DataSubmissions/DataSubmissionSummary.test.tsx b/src/components/DataSubmissions/DataSubmissionSummary.test.tsx index 9d27bbf8..7c281cc5 100644 --- a/src/components/DataSubmissions/DataSubmissionSummary.test.tsx +++ b/src/components/DataSubmissions/DataSubmissionSummary.test.tsx @@ -197,15 +197,16 @@ describe("DataSubmissionSummary History Dialog Tests", () => { fireEvent.click(getByText("Full History")); - const elements = getAllByTestId("history-item"); - expect(elements[0]).toHaveTextContent(/ARCHIVED/i); - expect(elements[0]).toHaveTextContent("1/9/2023"); - expect(elements[1]).toHaveTextContent(/COMPLETED/i); - expect(elements[1]).toHaveTextContent("1/8/2023"); - expect(elements[2]).toHaveTextContent(/RELEASED/i); - expect(elements[2]).toHaveTextContent("1/7/2023"); - expect(elements[8]).toHaveTextContent(/NEW/i); - expect(elements[8]).toHaveTextContent("1/1/2023"); + const dates = getAllByTestId(/history-item-\d-date/i); + const statuses = getAllByTestId(/history-item-\d-status/i); + expect(statuses[0]).toHaveTextContent(/ARCHIVED/i); + expect(dates[0]).toHaveTextContent("1/9/2023"); + expect(statuses[1]).toHaveTextContent(/COMPLETED/i); + expect(dates[1]).toHaveTextContent("1/8/2023"); + expect(statuses[2]).toHaveTextContent(/RELEASED/i); + expect(dates[2]).toHaveTextContent("1/7/2023"); + expect(statuses[8]).toHaveTextContent(/NEW/i); + expect(dates[8]).toHaveTextContent("1/1/2023"); }); it("closes the History dialog with the close button", async () => { @@ -238,7 +239,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { fireEvent.click(getByText("Full History")); await waitFor(() => { - const items = getAllByTestId("history-item-date"); + const items = getAllByTestId(/history-item-\d-date/); expect(new Date(items[0].textContent).getTime()).toBeGreaterThan( new Date(items[1].textContent).getTime() ); diff --git a/src/components/DataSubmissions/DataSubmissionSummary.tsx b/src/components/DataSubmissions/DataSubmissionSummary.tsx index b6808d6d..8cb416fb 100644 --- a/src/components/DataSubmissions/DataSubmissionSummary.tsx +++ b/src/components/DataSubmissions/DataSubmissionSummary.tsx @@ -4,7 +4,7 @@ import { isEqual } from "lodash"; import SubmissionHeaderProperty, { StyledValue } from "./SubmissionHeaderProperty"; import Tooltip from "./Tooltip"; import { ReactComponent as EmailIconSvg } from "../../assets/icons/email_icon.svg"; -import HistoryDialog from "../Shared/HistoryDialog"; +import HistoryDialog from "../HistoryDialog"; import DataSubmissionIconMap from "./DataSubmissionIconMap"; import ReviewCommentsDialog from "../Shared/ReviewCommentsDialog"; import { SortHistory } from "../../utils"; diff --git a/src/components/Header/HeaderDesktop.tsx b/src/components/Header/components/HeaderDesktop.tsx similarity index 75% rename from src/components/Header/HeaderDesktop.tsx rename to src/components/Header/components/HeaderDesktop.tsx index e45cfcd3..aa2b5bae 100644 --- a/src/components/Header/HeaderDesktop.tsx +++ b/src/components/Header/components/HeaderDesktop.tsx @@ -1,7 +1,6 @@ -import React from "react"; import { styled } from "@mui/material"; -import Logo from "./components/LogoDesktop"; -import NavBar from "./components/NavbarDesktop"; +import Logo from "./LogoDesktop"; +import NavBar from "./NavbarDesktop"; const HeaderBanner = styled("div")({ width: "100%", @@ -15,7 +14,7 @@ const HeaderContainer = styled("div")({ }); const Header = () => ( - + diff --git a/src/components/Header/HeaderTabletAndMobile.tsx b/src/components/Header/components/HeaderTabletAndMobile.tsx similarity index 95% rename from src/components/Header/HeaderTabletAndMobile.tsx rename to src/components/Header/components/HeaderTabletAndMobile.tsx index 6bf80130..a4023263 100644 --- a/src/components/Header/HeaderTabletAndMobile.tsx +++ b/src/components/Header/components/HeaderTabletAndMobile.tsx @@ -1,16 +1,16 @@ import React, { HTMLProps, useEffect, useState } from "react"; import { NavLink, Link, useNavigate, useLocation } from "react-router-dom"; import { styled } from "@mui/material"; -import Logo from "./components/LogoMobile"; -import menuClearIcon from "../../assets/header/Menu_Cancel_Icon.svg"; -import rightArrowIcon from "../../assets/header/Right_Arrow.svg"; -import leftArrowIcon from "../../assets/header/Left_Arrow.svg"; -import { navMobileList, navbarSublists } from "../../config/globalHeaderData"; -import { GenerateApiTokenRoles } from "../../config/AuthRoles"; -import { useAuthContext } from "../Contexts/AuthContext"; -import GenericAlert from "../GenericAlert"; -import APITokenDialog from "../../content/users/APITokenDialog"; -import UploaderToolDialog from "../UploaderToolDialog"; +import Logo from "./LogoMobile"; +import menuClearIcon from "../../../assets/header/Menu_Cancel_Icon.svg"; +import rightArrowIcon from "../../../assets/header/Right_Arrow.svg"; +import leftArrowIcon from "../../../assets/header/Left_Arrow.svg"; +import { navMobileList, navbarSublists } from "../../../config/globalHeaderData"; +import { GenerateApiTokenRoles } from "../../../config/AuthRoles"; +import { useAuthContext } from "../../Contexts/AuthContext"; +import GenericAlert from "../../GenericAlert"; +import APITokenDialog from "../../../content/users/APITokenDialog"; +import UploaderToolDialog from "../../UploaderToolDialog"; const HeaderBanner = styled("div")({ width: "100%", @@ -212,7 +212,7 @@ const Header = () => { You have been logged out. - +
diff --git a/src/components/Header/components/NavbarDesktop.tsx b/src/components/Header/components/NavbarDesktop.tsx index 41f236ed..d94eac78 100644 --- a/src/components/Header/components/NavbarDesktop.tsx +++ b/src/components/Header/components/NavbarDesktop.tsx @@ -93,7 +93,7 @@ const LiSection = styled("li")({ color: "#585C65", fontFamily: "poppins", fontSize: "17px", - fontWeight: 700, + fontWeight: 600, lineHeight: "40px", letterSpacing: "normal", textDecoration: "none", @@ -181,7 +181,7 @@ const LiSection = styled("li")({ color: "#FFFFFF", fontFamily: "poppins", fontSize: "17px", - fontWeight: 700, + fontWeight: 600, lineHeight: "40px", letterSpacing: "normal", textDecoration: "none", diff --git a/src/components/Header/USABanner.tsx b/src/components/Header/components/USABanner.tsx similarity index 89% rename from src/components/Header/USABanner.tsx rename to src/components/Header/components/USABanner.tsx index 1e0be921..f0a364b7 100644 --- a/src/components/Header/USABanner.tsx +++ b/src/components/Header/components/USABanner.tsx @@ -1,6 +1,6 @@ import React from "react"; import { styled } from "@mui/material"; -import { headerData } from "../../config/globalHeaderData"; +import { headerData } from "../../../config/globalHeaderData"; const BannerArea = styled("div")({ flexDirection: "row", @@ -35,7 +35,7 @@ const BannerContainer = styled("div")({ }); const USABanner = () => ( - + {headerData.usaFlagSmallAltText}
An official website of the United States government
diff --git a/src/components/Header/index.test.tsx b/src/components/Header/index.test.tsx index 9f70710b..f6c01303 100644 --- a/src/components/Header/index.test.tsx +++ b/src/components/Header/index.test.tsx @@ -1,22 +1,30 @@ import { FC, useMemo } from "react"; import { BrowserRouter } from "react-router-dom"; import { axe } from "jest-axe"; -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import { MockedProvider } from "@apollo/client/testing"; import Header from "./index"; import { ContextState, Context, Status } from "../Contexts/AuthContext"; -const Parent: FC<{ children: React.ReactElement; loggedIn: boolean }> = ({ +const mockUseMediaQuery = jest.fn(); +jest.mock("@mui/material", () => ({ + ...jest.requireActual("@mui/material"), + useMediaQuery: (query: string) => mockUseMediaQuery(query), +})); + +const Parent: FC<{ children: React.ReactElement; loggedIn?: boolean; error?: string }> = ({ children, - loggedIn, + loggedIn = false, + error = null, }) => { const value: ContextState = useMemo( () => ({ isLoggedIn: loggedIn, - status: Status.LOADED, + status: error ? Status.ERROR : Status.LOADED, user: null, + error, }), - [loggedIn] + [loggedIn, error] ); return ( @@ -28,26 +36,74 @@ const Parent: FC<{ children: React.ReactElement; loggedIn: boolean }> = ({ ); }; -describe("should not have any accessibility violations", () => { - it("when logged in", async () => { - const { container } = render( - -
- - ); - const results = await axe(container); +describe("Accessibility", () => { + it("should not have accessibility violations (Logged In)", async () => { + const { container } = render(
, { wrapper: (p) => }); + + expect(await axe(container)).toHaveNoViolations(); + }); + + it("should not have accessibility violations (Logged Out)", async () => { + const { container } = render(
, { + wrapper: (p) => , + }); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); + +describe("Implementation Requirements", () => { + it("should show the desktop header when the screen is larger than 1024px", () => { + mockUseMediaQuery.mockReturnValue(false); + + const { getByTestId } = render(
, { + wrapper: (p) => , + }); + + expect(getByTestId("navigation-header-desktop")).toBeInTheDocument(); + }); + + it("should show the tablet and mobile header when the screen is 1024px or smaller", () => { + mockUseMediaQuery.mockReturnValue(true); + + const { getByTestId } = render(
, { + wrapper: (p) => , + }); + + expect(getByTestId("navigation-header-mobile")).toBeInTheDocument(); + }); + + it("should always show the USA banner", () => { + const { getByTestId } = render(
, { + wrapper: (p) => , + }); + + expect(getByTestId("navigation-flag-banner")).toBeInTheDocument(); + }); + + it("should show an error snackbar when there is an authentication error", async () => { + render(
, { + wrapper: (p) => , + }); - expect(results).toHaveNoViolations(); + await waitFor(() => { + expect(global.mockEnqueue).toHaveBeenCalledWith("mock auth error", { + variant: "error", + }); + }); }); - it("when logged out", async () => { - const { container } = render( - -
- - ); - const results = await axe(container); + it("should show an error snackbar when there is an unknown error type", async () => { + render(
, { + wrapper: (p) => ( + + ), + }); - expect(results).toHaveNoViolations(); + await waitFor(() => { + expect(global.mockEnqueue).toHaveBeenCalledWith("An unknown error occurred during login", { + variant: "error", + }); + }); }); }); diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index efee2084..2cb51d6d 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,34 +1,34 @@ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { useMediaQuery } from "@mui/material"; -import HeaderDesktop from "./HeaderDesktop"; -import HeaderTabletAndMobile from "./HeaderTabletAndMobile"; -import USABanner from "./USABanner"; -import GenericAlert from "../GenericAlert"; +import { useSnackbar } from "notistack"; +import HeaderDesktop from "./components/HeaderDesktop"; +import HeaderTabletAndMobile from "./components/HeaderTabletAndMobile"; +import USABanner from "./components/USABanner"; import { useAuthContext } from "../Contexts/AuthContext"; const Header = () => { + const { enqueueSnackbar } = useSnackbar(); + const { error: authError } = useAuthContext(); const tabletAndMobile = useMediaQuery("(max-width: 1024px)"); - const [showLoginError, setShowLoginError] = useState(false); - const authContext = useAuthContext(); - useEffect(() => { - if (authContext.error !== undefined) { - setShowLoginError(true); - setTimeout(() => setShowLoginError(false), 10000); + if (!authError) { + return; } - }, [authContext]); + + enqueueSnackbar( + typeof authError === "string" && authError?.length > 0 + ? authError + : "An unknown error occurred during login", + { variant: "error" } + ); + }, [authError]); return ( - <> - - {authContext.error} - -
- - {tabletAndMobile ? : } -
- +
+ + {tabletAndMobile ? : } +
); }; diff --git a/src/components/HistoryDialog/index.test.tsx b/src/components/HistoryDialog/index.test.tsx new file mode 100644 index 00000000..3d9eb7bf --- /dev/null +++ b/src/components/HistoryDialog/index.test.tsx @@ -0,0 +1,169 @@ +import { axe } from "jest-axe"; +import { render, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import HistoryDialog, { IconType } from "./index"; + +type MockStatuses = "uploaded" | "downloaded" | "error"; + +const IconMap: IconType = { + uploaded: "upload", + downloaded: "download", + error: "error", +}; + +const BaseProps: React.ComponentProps = { + open: true, + preTitle: "", + title: "", + history: [], + iconMap: IconMap, + getTextColor: () => "red", + onClose: () => {}, +}; + +describe("Accessibility", () => { + it("should have no violations", async () => { + const { container } = render(); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); + +describe("Basic Functionality", () => { + it("should render without crashing", () => { + expect(() => render()).not.toThrow(); + }); + + it("should close the dialog when the 'Close' button is clicked", () => { + const onClose = jest.fn(); + const { getByTestId } = render(); + + userEvent.click(getByTestId("history-dialog-close")); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("should close the dialog when the backdrop is clicked", () => { + const onClose = jest.fn(); + const { getAllByRole } = render(); + + userEvent.click(getAllByRole("presentation")[1]); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("should render the correct title", () => { + const title = "This is a Test Title"; + const { getByTestId } = render(); + + expect(within(getByTestId("history-dialog")).getByText(title)).toBeInTheDocument(); + }); + + it("should render the correct pre-title", () => { + const preTitle = "This is a Test Pre-Title"; + const { getByTestId } = render(); + + expect(within(getByTestId("history-dialog")).getByText(preTitle)).toBeInTheDocument(); + }); + + it("should call getTextColor with the correct status", () => { + const getTextColor = jest.fn(); + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: new Date().toISOString(), userID: "test", reviewComment: "" }, + ]; + + render(); + + expect(getTextColor).toHaveBeenCalledWith("uploaded"); + }); +}); + +describe("Implementation Requirements", () => { + it("should sort the history by date in descending order", () => { + const history: HistoryBase[] = [ + { status: "error", dateTime: "2022-05-19T04:12:00Z", userID: "test", reviewComment: "" }, // 1 + { status: "uploaded", dateTime: "2022-03-20T14:45:00Z", userID: "test", reviewComment: "" }, // 3 + { status: "downloaded", dateTime: "2022-04-25T11:56:00Z", userID: "test", reviewComment: "" }, // 2 + ]; + + const { getByTestId } = render(); + + expect( + within(getByTestId("history-item-0")).getByTestId("history-item-0-status") + ).toHaveTextContent(/error/i); + + expect( + within(getByTestId("history-item-1")).getByTestId("history-item-1-status") + ).toHaveTextContent(/downloaded/i); + + expect( + within(getByTestId("history-item-2")).getByTestId("history-item-2-status") + ).toHaveTextContent(/uploaded/i); + }); + + it("should render the parsed date for each history item", () => { + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: "2022-03-20T14:45:00Z", userID: "test", reviewComment: "" }, + ]; + + const { getByTestId } = render(); + + expect( + within(getByTestId("history-item-0")).getByTestId("history-item-0-date") + ).toHaveTextContent("3/20/2022"); + }); + + it("should render corresponding icon for the first history item", () => { + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: "2022-03-20T14:45:00Z", userID: "test", reviewComment: "" }, + { status: "downloaded", dateTime: "2019-09-05T11:56:00Z", userID: "test", reviewComment: "" }, + ]; + + const { getByTestId, queryByTestId } = render( + + ); + + expect(getByTestId("history-item-0-icon")).toBeInTheDocument(); + expect(getByTestId("history-item-0-icon")).toHaveAttribute("src", IconMap.uploaded); + + expect(queryByTestId("history-item-1-icon")).not.toBeInTheDocument(); + }); + + it("should render the status for each history item in uppercase", () => { + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: "2024-09-05T14:45:00Z", userID: "test", reviewComment: "" }, + ]; + + const { getByTestId } = render(); + + expect( + within(getByTestId("history-item-0")).getByTestId("history-item-0-status") + ).toHaveTextContent(/UPLOADED/); + }); + + it("should have the unparsed date as a title attribute on the date element", () => { + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: "2024-09-05T14:45:00Z", userID: "test", reviewComment: "" }, + ]; + + const { getByTestId } = render(); + + expect( + within(getByTestId("history-item-0")).getByTestId("history-item-0-date") + ).toHaveAttribute("title", "2024-09-05T14:45:00Z"); + }); + + it("should fallback to #FFF if getTextColor is not a function", () => { + const history: HistoryBase[] = [ + { status: "uploaded", dateTime: "2024-09-05T14:45:00Z", userID: "test", reviewComment: "" }, + ]; + + const { getByTestId } = render( + + ); + + expect(within(getByTestId("history-item-0")).getByTestId("history-item-0-status")).toHaveStyle({ + color: "#FFF", + }); + }); +}); diff --git a/src/components/Shared/HistoryDialog.tsx b/src/components/HistoryDialog/index.tsx similarity index 96% rename from src/components/Shared/HistoryDialog.tsx rename to src/components/HistoryDialog/index.tsx index 4bbe5414..26162357 100644 --- a/src/components/Shared/HistoryDialog.tsx +++ b/src/components/HistoryDialog/index.tsx @@ -150,9 +150,6 @@ const StyledTypography = styled(Typography)<{ color: CSSProperties["color"] }>(( const StyledAvatar = styled(Avatar)({ background: "transparent", marginRight: "8px", - "&.MuiAvatar-root": { - marginLeft: "auto", - }, }); const StyledCloseButton = styled(Button)({ @@ -215,7 +212,7 @@ const HistoryDialog = ({ {sortedHistory?.map(({ status, dateTime }, index) => ( @@ -226,13 +223,13 @@ const HistoryDialog = ({ {FormatDate(dateTime, "M/D/YYYY", "N/A")} {status?.toString()?.toUpperCase()} @@ -254,7 +251,7 @@ const HistoryDialog = ({ onClose()} + onClick={onClose} variant="outlined" size="large" aria-label="Close dialog" diff --git a/src/components/StatusBar/StatusBar.test.tsx b/src/components/StatusBar/StatusBar.test.tsx index 0652b732..c503c72a 100644 --- a/src/components/StatusBar/StatusBar.test.tsx +++ b/src/components/StatusBar/StatusBar.test.tsx @@ -7,7 +7,7 @@ import StatusBar from "./StatusBar"; import StatusApproved from "../../assets/history/submissionRequest/StatusApproved.svg"; import StatusRejected from "../../assets/history/submissionRequest/StatusRejected.svg"; import { FormatDate } from "../../utils"; -import { HistoryIconMap } from "../../assets/history/submissionRequest"; +import { HistoryIconMap } from "./components/SubmissionRequestIconMap"; type Props = { data: object; @@ -372,8 +372,8 @@ describe("StatusBar > History Modal Tests", () => { fireEvent.click(getByText("Full History")); - expect(getByTestId("status-bar-history-item-0-icon")).toBeVisible(); - expect(() => getByTestId("status-bar-history-item-1-icon")).toThrow(); + expect(getByTestId("history-item-0-icon")).toBeVisible(); + expect(() => getByTestId("history-item-1-icon")).toThrow(); }); it.each(Object.entries(HistoryIconMap))( @@ -387,7 +387,7 @@ describe("StatusBar > History Modal Tests", () => { fireEvent.click(getByText("Full History")); - const icon = getByTestId("status-bar-history-item-0-icon"); + const icon = getByTestId("history-item-0-icon"); expect(icon).toBeVisible(); expect(icon).toHaveAttribute("alt", `${status} icon`); @@ -418,7 +418,7 @@ describe("StatusBar > History Modal Tests", () => { expect(queryByTestId("status-bar-history-dialog")).toBeVisible(); - fireEvent.click(queryByTestId("status-bar-dialog-close")); + fireEvent.click(queryByTestId("history-dialog-close")); await waitFor(() => expect(queryByTestId("status-bar-history-dialog")).toBeNull()); }); diff --git a/src/components/StatusBar/components/HistorySection.tsx b/src/components/StatusBar/components/HistorySection.tsx index 1374e69e..242af131 100644 --- a/src/components/StatusBar/components/HistorySection.tsx +++ b/src/components/StatusBar/components/HistorySection.tsx @@ -1,19 +1,10 @@ -import { CSSProperties, FC, useMemo, useState } from "react"; -import { - Avatar, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Stack, - Typography, -} from "@mui/material"; -import { Timeline, TimelineItem, TimelineSeparator, TimelineDot, TimelineContent } from "@mui/lab"; +import { CSSProperties, FC, useState } from "react"; +import { Button } from "@mui/material"; import { styled } from "@mui/material/styles"; import { useFormContext } from "../../Contexts/FormContext"; -import { HistoryIconMap } from "../../../assets/history/submissionRequest"; -import { FormatDate, SortHistory } from "../../../utils"; +import { HistoryIconMap } from "./SubmissionRequestIconMap"; +import { FormatDate } from "../../../utils"; +import HistoryDialog from "../../HistoryDialog"; /** * Determines the text color for a History event based @@ -64,158 +55,6 @@ const StyledButton = styled(Button)({ }, }); -const StyledDialog = styled(Dialog)({ - "& .MuiDialog-paper": { - borderRadius: "8px", - boxShadow: "0px 4px 45px 0px rgba(0, 0, 0, 0.40)", - padding: "28px 24px", - width: "567px !important", - border: "2px solid #388DEE", - background: "#2E4D7B", - }, -}); - -const StyledDialogTitle = styled(DialogTitle)({ - paddingBottom: "0", -}); - -const StyledPreTitle = styled("p")({ - color: "#D5DAE7", - fontSize: "13px", - fontFamily: "Nunito Sans", - lineHeight: "27px", - letterSpacing: "0.5px", - textTransform: "uppercase", - margin: "0", -}); - -const StyledTitle = styled("p")({ - color: "#FFF", - fontSize: "35px", - fontFamily: "Nunito Sans", - fontWeight: "900", - lineHeight: "30px", - margin: "0", -}); - -const StyledDialogContent = styled(DialogContent)({ - marginTop: "20px", - marginBottom: "22px", -}); - -const StyledTimelineItem = styled(TimelineItem)({ - alignItems: "center", - "&::before": { - flex: "0", - padding: "0", - paddingLeft: "55px", - }, - // Add vertical separator line between timeline items - "&:not(:last-of-type)::after": { - content: '" "', - height: "1px", - left: "0", - right: "0", - bottom: "0", - position: "absolute", - background: "#033277", - }, - // Add vertical lines between timeline item dots (top) - "&:not(:first-of-type) .MuiTimelineSeparator-root::before": { - content: '" "', - width: "6px", - background: "#fff", - top: "0", - bottom: "50%", - right: "50%", - position: "absolute", - transform: "translateX(50%)", - zIndex: "1", - }, - // Add vertical lines between timeline item dots (bottom) - "&:not(:last-of-type) .MuiTimelineSeparator-root::after": { - content: '" "', - width: "6px", - background: "#fff", - top: "50%", - bottom: "0", - right: "50%", - position: "absolute", - transform: "translateX(50%)", - zIndex: "1", - }, -}); - -const StyledTimelineSeparator = styled(TimelineSeparator)({ - position: "relative", - minHeight: "70px", -}); - -const StyledTimelineDot = styled(TimelineDot)({ - background: "#fff", - borderWidth: "4px", - margin: "0", - zIndex: "2", - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", -}); - -const StyledTimelineVerticalLine = styled("span")({ - width: "60px", - height: "2px", - background: "#fff", - position: "absolute", - top: "50%", - left: "50%", - "&::after": { - content: '" "', - width: "6px", - height: "6px", - borderRadius: "50%", - background: "#fff", - top: "50%", - bottom: "0", - right: "0", - position: "absolute", - transform: "translateY(-50%)", - zIndex: "1", - }, -}); - -const StyledTimelineContent = styled(TimelineContent)({ - marginLeft: "60px", - color: "#fff", -}); - -const StyledTypography = styled(Typography)<{ status?: ApplicationStatus }>(({ status }) => ({ - lineHeight: "2.5", - minWidth: "100px", - textAlign: "left", - color: getStatusColor(status), -})); - -const StyledAvatar = styled(Avatar)({ - background: "transparent", - marginRight: "8px", -}); - -const StyledCloseButton = styled(Button)({ - minWidth: "137px", - fontWeight: "700", - borderRadius: "8px", - textTransform: "none", - color: "#fff", - borderColor: "#fff", - margin: "0 auto", - padding: "10px", - lineHeight: "24px", - "&:hover": { - borderColor: "#fff", - }, -}); - /** * Status Bar History Section * @@ -226,7 +65,6 @@ const HistorySection: FC = () => { data: { updatedAt, history }, } = useFormContext(); const [open, setOpen] = useState(false); - const sortedHistory = useMemo(() => SortHistory(history), [history]); return ( <> @@ -246,61 +84,16 @@ const HistorySection: FC = () => { > Full History - setOpen(false)} - scroll="body" data-testid="status-bar-history-dialog" - > - - CRDC Submission Request - Submission History - - - - {sortedHistory.map(({ status, dateTime }, index) => ( - - - - - - - - - {FormatDate(dateTime, "M/D/YYYY", "N/A")} - - {status?.toUpperCase()} - {index === 0 && HistoryIconMap[status] && ( - - {`${status} - - )} - - - - ))} - - - - setOpen(false)} - variant="outlined" - size="large" - aria-label="Close dialog" - data-testid="status-bar-dialog-close" - > - Close - - - + /> )} diff --git a/src/components/StatusBar/components/StatusSection.tsx b/src/components/StatusBar/components/StatusSection.tsx index 75c9dc17..0f2f91d1 100644 --- a/src/components/StatusBar/components/StatusSection.tsx +++ b/src/components/StatusBar/components/StatusSection.tsx @@ -2,7 +2,7 @@ import { CSSProperties, FC, useMemo, useState } from "react"; import { Avatar, Button } from "@mui/material"; import { styled } from "@mui/material/styles"; import { useFormContext } from "../../Contexts/FormContext"; -import { StatusIconMap } from "../../../assets/history/submissionRequest"; +import { StatusIconMap } from "./SubmissionRequestIconMap"; import { SortHistory } from "../../../utils"; import ReviewCommentsDialog from "../../Shared/ReviewCommentsDialog"; diff --git a/src/components/StatusBar/components/SubmissionRequestIconMap.tsx b/src/components/StatusBar/components/SubmissionRequestIconMap.tsx new file mode 100644 index 00000000..705f8cff --- /dev/null +++ b/src/components/StatusBar/components/SubmissionRequestIconMap.tsx @@ -0,0 +1,33 @@ +import New from "../../../assets/history/submissionRequest/SubmissionRequestNew.svg"; +import Submitted from "../../../assets/history/submissionRequest/SubmissionRequestSubmitted.svg"; +import Rejected from "../../../assets/history/submissionRequest/Rejected.svg"; +import Approved from "../../../assets/history/submissionRequest/Approved.svg"; +import UnderReview from "../../../assets/history/submissionRequest/UnderReview.svg"; +import StatusApproved from "../../../assets/history/submissionRequest/StatusApproved.svg"; +import StatusRejected from "../../../assets/history/submissionRequest/StatusRejected.svg"; +import InProgress from "../../../assets/history/submissionRequest/InProgress.svg"; +import { IconType } from "../../HistoryDialog"; + +/** + * Map of ApplicationStatus to Icon for History Modal + * + * @see ApplicationStatus + */ +export const HistoryIconMap: IconType = { + New, + Submitted, + Rejected, + Approved, + "In Review": UnderReview, + "In Progress": InProgress, +} as IconType; + +/** + * Map of ApplicationStatus to Icon for Status Bar + * + * @see ApplicationStatus + */ +export const StatusIconMap: IconType = { + Rejected: StatusRejected, + Approved: StatusApproved, +} as IconType; diff --git a/src/content/dataSubmissions/DataSubmission.tsx b/src/content/dataSubmissions/DataSubmission.tsx index 818c7a7c..b4c54652 100644 --- a/src/content/dataSubmissions/DataSubmission.tsx +++ b/src/content/dataSubmissions/DataSubmission.tsx @@ -1,28 +1,15 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useMutation } from "@apollo/client"; -import { - Alert, - Box, - Card, - CardActions, - CardContent, - Container, - IconButton, - Stack, - Tabs, - Typography, - styled, -} from "@mui/material"; +import { Alert, Box, Card, CardActions, CardContent, Container, Tabs, styled } from "@mui/material"; import { useSnackbar, VariantType } from "notistack"; -import bannerPng from "../../assets/dataSubmissions/dashboard_banner.png"; -import summaryBannerSvg from "../../assets/dataSubmissions/summary_banner.png"; +import bannerPng from "../../assets/banner/submission_banner.png"; +import summaryBannerPng from "../../assets/banner/summary_banner.png"; import LinkTab from "../../components/DataSubmissions/LinkTab"; import MetadataUpload from "../../components/DataSubmissions/MetadataUpload"; import { SUBMISSION_ACTION, SubmissionActionResp } from "../../graphql"; import DataSubmissionSummary from "../../components/DataSubmissions/DataSubmissionSummary"; import DataSubmissionActions from "./DataSubmissionActions"; import QualityControl from "./QualityControl"; -import { ReactComponent as CopyIconSvg } from "../../assets/icons/copy_icon_2.svg"; import ValidationStatistics from "../../components/DataSubmissions/ValidationStatistics"; import ValidationControls from "../../components/DataSubmissions/ValidationControls"; import { useAuthContext } from "../../components/Contexts/AuthContext"; @@ -41,19 +28,15 @@ import { useSubmissionContext } from "../../components/Contexts/SubmissionContex import DataActivity, { DataActivityRef } from "./DataActivity"; import CrossValidation from "./CrossValidation"; import { CrossValidateRoles } from "../../config/AuthRoles"; +import CopyAdornment from "../../components/DataSubmissions/CopyAdornment"; const StyledBanner = styled("div")(({ bannerSrc }: { bannerSrc: string }) => ({ background: `url(${bannerSrc})`, backgroundBlendMode: "luminosity, normal", backgroundSize: "cover", - backgroundRepeat: "no-repeat", - backgroundPosition: "top", + backgroundPosition: "center", width: "100%", - height: "295px", - display: "flex", - justifyContent: "center", - alignItems: "center", - position: "relative", + height: "296px", zIndex: 0, })); @@ -150,53 +133,12 @@ const StyledWrapper = styled("div")({ }); const StyledCardContent = styled(CardContent)({ - background: `url(${summaryBannerSvg})`, + background: `url(${summaryBannerPng})`, backgroundSize: "auto", backgroundRepeat: "no-repeat", backgroundPosition: "top", }); -const StyledCopyWrapper = styled(Stack)(() => ({ - height: "42px", - width: "fit-content", - minWidth: "342px", - padding: "11px 20px", - borderRadius: "8px 8px 0px 0px", - borderTop: "1.25px solid #6DADDB", - borderRight: "1.25px solid #6DADDB", - borderLeft: "1.25px solid #6DADDB", - background: "#EAF5F8", -})); - -const StyledCopyLabel = styled(Typography)(() => ({ - color: "#125868", - fontFamily: "'Nunito', 'Rubik', sans-serif", - fontSize: "12px", - fontStyle: "normal", - fontWeight: 800, - lineHeight: "19.6px", - letterSpacing: "0.24px", - textTransform: "uppercase", -})); - -const StyledCopyValue = styled(Typography)(() => ({ - color: "#125868", - fontFamily: "'Nunito', 'Rubik', sans-serif", - fontSize: "16px", - fontStyle: "normal", - fontWeight: 400, - lineHeight: "19.6px", - letterSpacing: "0.32px", -})); - -const StyledCopyIDButton = styled(IconButton)(() => ({ - color: "#000000", - padding: 0, - "&.MuiIconButton-root.Mui-disabled": { - color: "#B0B0B0", - }, -})); - const StyledFlowContainer = styled(Box)({ padding: "27px 59px 59px 60px", }); @@ -309,13 +251,6 @@ const DataSubmission: FC = ({ submissionId, tab = URLTabs.UPLOAD_ACTIVITY [enqueueSnackbar, handleBatchRefresh, getSubmission] ); - const handleCopyID = () => { - if (!submissionId) { - return; - } - navigator.clipboard.writeText(submissionId); - }; - useEffect(() => { if (!submissionId) { setError("Invalid submission ID provided."); @@ -328,23 +263,7 @@ const DataSubmission: FC = ({ submissionId, tab = URLTabs.UPLOAD_ACTIVITY - - - SUBMISSION ID: - - - {submissionId} - - {submissionId && ( - - - - )} - + {error && Oops! An error occurred. {error}} diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index 1c127e73..d94fa4ba 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -26,7 +26,7 @@ const Layout: FC = ({ children }) => ( href={ "https://fonts.googleapis.com/css2?" + "family=Open+Sans&" + - "family=Poppins:wght@400;700&" + + "family=Poppins:wght@400;600;700&" + "family=Lato:wght@300;400;500;600;700&" + "family=Inter:wght@300;400;500;600;700&" + "family=Nunito+Sans:wght@400;500;600;700;800;900&" +