Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(add-breadcrumbs): Add Bread Crumbs to the Mako Web Application #162

Merged
merged 17 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/services/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@
"aws-amplify": "^5.2.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"shared-types": "*",
"shared-utils": "*",
"date-fns": "^2.30.0",
"export-to-csv": "^0.2.1",
"file-saver": "^2.0.5",
Expand All @@ -61,6 +59,8 @@
"react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.10.0",
"react-select": "^5.7.4",
"shared-types": "*",
"shared-utils": "*",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.0",
Expand All @@ -71,6 +71,7 @@
"@tailwindcss/typography": "^0.5.10",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^20.4.2",
"@types/react": "^18.0.28",
Expand All @@ -88,7 +89,7 @@
"postcss": "^8.4.31",
"serverless-s3-sync": "^3.1.0",
"tailwindcss": "^3.3.1",
"typescript": "^4.9.3",
"typescript": "^5.2.0",
"vite": "^4.2.0",
"vitest": "^0.30.1"
}
Expand Down
70 changes: 70 additions & 0 deletions src/services/ui/src/components/BreadCrumb/BreadCrumb.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, test, expect, beforeAll, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BreadCrumb, BreadCrumbBar } from "./BreadCrumb";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";

export const LocationDisplay = () => {
const location = useLocation();

return <div data-testid="location-display">{location.pathname}</div>;
};

describe("Bread Crumb Tests", () => {
describe("Bread Crumb Routing", () => {
test("Sucessfully navigate using breadcrumbs", async () => {
render(
<>
<Routes>
<Route path="/" />
<Route path="/test" />
<Route path="/test/:id" />
</Routes>
<BreadCrumbBar>
<BreadCrumb to="/test">Click Me</BreadCrumb>
</BreadCrumbBar>
<LocationDisplay />
</>,
{ wrapper: BrowserRouter }
);

const user = userEvent.setup();

await user.click(screen.getByText(/click me/i));
expect(screen.getByText("/test")).toBeInTheDocument();
});
});

describe("Bread Crumb Interations", async () => {
beforeEach(() => {
render(
<BreadCrumbBar>
<BreadCrumb data-testid="home" to="/">
Home
</BreadCrumb>
<BreadCrumb data-testid="dashboard" to="/test">
Test Dashboard
</BreadCrumb>
<BreadCrumb data-testid="item" to="/test/:id" active={false}>
Test Item
</BreadCrumb>
</BreadCrumbBar>,
{
wrapper: BrowserRouter,
}
);
});

test("active element is styled different", async () => {
const homeBreadCrumb = screen.getByText("Home");
const dashboardBreadCrumb = screen.getByText("Test Dashboard");
const itemBreadCrumb = screen.getByText("Test Item");

expect(homeBreadCrumb.classList.contains("underline")).toBeTruthy();
expect(dashboardBreadCrumb.classList.contains("underline")).toBeTruthy();
expect(itemBreadCrumb.classList.contains("underline")).toBeFalsy();
});
});

// TODO: Write a test to test the functionality of the BreadCrumbs component with a test config passed in
});
80 changes: 80 additions & 0 deletions src/services/ui/src/components/BreadCrumb/BreadCrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Link } from "react-router-dom";
import { type ReactNode } from "react";
import { ChevronRight } from "lucide-react";
import { BreadCrumbConfig } from "./bread-crumb-config";

type BreadCrumbsProps = {
options: BreadCrumbConfig[];
};

export const BreadCrumbs = ({ options }: BreadCrumbsProps) => {
const defaultBreadCrumb = options.find((option) => option.default);

return (
<BreadCrumbBar>
{defaultBreadCrumb && (
<BreadCrumb to={defaultBreadCrumb.to} showSeperator={false}>
{defaultBreadCrumb.displayText}
</BreadCrumb>
)}
{/* After this we map over the config and check to see if the breadcrumb needs to be displayed. Proper route paths are important here. It should be hierarchical */}
{options
.filter((option) => !option.default)
.filter((option) => window.location.href.includes(option.to))
.toSorted((option, prevOption) => option.order - prevOption.order)
.map(({ displayText, to }, index, optionsArray) => {
return (
<BreadCrumb
key={displayText}
to={to}
active={index !== optionsArray.length - 1}
>
{displayText}
</BreadCrumb>
);
})}
</BreadCrumbBar>
);
};

type BreadCrumbProps = {
to: string;
active?: boolean;
showSeperator?: boolean;
seperator?: ReactNode;
};

export const BreadCrumb = ({
to,
seperator = <BreadCrumbSeperator />,
showSeperator = true,
active = true,
children,
}: React.PropsWithChildren<BreadCrumbProps>) => {
return (
<li className="flex items-center">
{showSeperator && <span>{seperator}</span>}

{active && (
<Link to={to} className="underline text-sky-600 hover:text-sky-800">
{children}
</Link>
)}
{!active && <span aria-disabled>{children}</span>}
</li>
);
};

export const BreadCrumbSeperator = () => <ChevronRight className="w-5 h-5" />;

export const BreadCrumbBar = ({ children }: React.PropsWithChildren) => {
return (
<nav
role="navigation"
aria-label="breadcrumbs for spa or waiver choices"
className="mt-4"
>
<ul className="flex gap-1">{children}</ul>
</nav>
);
};
90 changes: 90 additions & 0 deletions src/services/ui/src/components/BreadCrumb/bread-crumb-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ROUTES } from "@/routes";

export type BreadCrumbConfig = {
default?: boolean;
order: number;
to: string;
displayText: string;
};

export const BREAD_CRUMB_CONFIG_NEW_SUBMISSION: BreadCrumbConfig[] = [
{
default: true,
displayText: "Dashboard",
to: ROUTES.DASHBOARD,
order: 1,
},
{
displayText: "Submission Type",
to: ROUTES.NEW_SUBMISSION_OPTIONS,
order: 2,
},
{
displayText: "SPA Type",
to: ROUTES.SPA_SUBMISSION_OPTIONS,
order: 3,
},
{
displayText: "Waiver Type",
to: ROUTES.WAIVER_SUBMISSION_OPTIONS,
order: 3,
},
{
displayText: "1915(b) Waiver Type",
to: ROUTES.B_WAIVER_SUBMISSION_OPTIONS,
order: 4,
},
{
displayText: "Medicaid SPA Type",
to: ROUTES.MEDICAID_SPA_SUB_OPTIONS,
order: 4,
},
{
displayText: "CHIP SPA Type",
to: ROUTES.CHIP_SPA_SUB_OPTIONS,
order: 4,
},
{
displayText: "CHIP Eligibility SPAs",
to: ROUTES.CHIP_ELIGIBILITY_LANDING,
order: 5,
},
{
displayText:
"Medicaid Alternative Benefits Plans (ABP), and Medicaid Premiums and Cost Sharing",
to: ROUTES.MEDICAID_ABP_LANDING,
order: 5,
},
{
displayText:
"Medicaid Eligibility, Enrollment, Administration, and Health Homes",
to: ROUTES.MEDICAID_ELIGIBILITY_LANDING,
order: 5,
},
{
displayText: "1915(b)(4) FFS Selective Contracting Waiver Types",
to: ROUTES.B4_WAIVER_OPTIONS,
order: 5,
},
{
displayText: "1915(b) Comprehensive (Capitated) Waiver Authority Types",
to: ROUTES.BCAP_WAIVER_OPTIONS,
order: 5,
},
];

export const BREAD_CRUMB_CONFIG_PACKAGE_DETAILS = (data: {
id: string;
}): BreadCrumbConfig[] => [
{
displayText: "Dashboard",
order: 1,
default: true,
to: ROUTES.DASHBOARD,
},
{
displayText: `${data.id}`,
order: 2,
to: ROUTES.DETAILS,
},
];
1 change: 1 addition & 0 deletions src/services/ui/src/components/BreadCrumb/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./BreadCrumb";
3 changes: 3 additions & 0 deletions src/services/ui/src/pages/create/create-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
WAIVER_OPTIONS,
} from "@/pages/create/options";
import { SimplePageContainer } from "@/components";
import { BreadCrumbs } from "@/components/BreadCrumb";
import { BREAD_CRUMB_CONFIG_NEW_SUBMISSION } from "@/components/BreadCrumb/bread-crumb-config";

/** Can be removed once page title bar with back nav is integrated */
export const SimplePageTitle = ({ title }: { title: string }) => (
Expand All @@ -32,6 +34,7 @@ type OptionsPageProps = {
const OptionsPage = ({ options, title, fieldsetLegend }: OptionsPageProps) => {
return (
<SimplePageContainer>
<BreadCrumbs options={BREAD_CRUMB_CONFIG_NEW_SUBMISSION} />
<SimplePageTitle title={title} />
<OptionFieldset legend={fieldsetLegend}>
{options.map((opt, idx) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { PropsWithChildren, ReactElement } from "react";
import { SimplePageTitle } from "@/pages/create/create-options";
import { SimplePageContainer } from "@/components";
import { FAQ_SECTION, ROUTES } from "@/routes";
import { BreadCrumbs } from "@/components/BreadCrumb";
import { BREAD_CRUMB_CONFIG_NEW_SUBMISSION } from "@/components/BreadCrumb/bread-crumb-config";
export enum EXTERNAL_APP {
MAC_PRO = "https://www.medicaid.gov/resources-for-states/medicaid-and-chip-program-macpro-portal/index.html#MACPro",
MMDL = "https://wms-mmdl.cms.gov/MMDL/faces/portal.jsp",
Expand Down Expand Up @@ -53,6 +55,7 @@ const ExternalAppLandingPage = ({
}: ExternalAppLandingPageConfig) => {
return (
<SimplePageContainer>
<BreadCrumbs options={BREAD_CRUMB_CONFIG_NEW_SUBMISSION} />
{/* TODO: Replace simple page title bar with breadcrumbs */}
<SimplePageTitle title={pageTitle} />
<div className="flex flex-col items-center justify-center m-4 pt-4 pb-12">
Expand Down
8 changes: 6 additions & 2 deletions src/services/ui/src/pages/detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { OsHit, OsMainSourceItem } from "shared-types";
import { useQuery } from "@/hooks";
import { useGetItem } from "@/api";
import { DetailNav } from "./detailNav";
import { BreadCrumbs } from "@/components/BreadCrumb";
import { BREAD_CRUMB_CONFIG_PACKAGE_DETAILS } from "@/components/BreadCrumb/bread-crumb-config";

export const DetailsContent = ({
data,
Expand Down Expand Up @@ -82,6 +84,7 @@ export const Details = () => {
const query = useQuery();
const id = query.get("id") as string;
const { data, isLoading, error } = useGetItem(id);

if (isLoading) {
return <LoadingSpinner />;
}
Expand All @@ -91,8 +94,9 @@ export const Details = () => {

return (
<>
<DetailNav id={id} type={data?._source.planType} />
<div className="max-w-screen-xl mx-auto py-8 px-4 lg:px-8">
{/* <DetailNav id={id} type={data?._source.planType} /> */}
<div className="max-w-screen-xl mx-auto py-1 px-4 lg:px-8 flex flex-col gap-4">
<BreadCrumbs options={BREAD_CRUMB_CONFIG_PACKAGE_DETAILS({ id })} />
<DetailsContent data={data} />
</div>
</>
Expand Down
6 changes: 3 additions & 3 deletions src/services/ui/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export enum ROUTES {
B_WAIVER_SUBMISSION_OPTIONS = "/new-submission/waiver/b",
B4_WAIVER_OPTIONS = "/new-submission/waiver/b/b4",
BCAP_WAIVER_OPTIONS = "/new-submission/waiver/b/capitated",
MEDICAID_ABP_LANDING = "/landing/medicaid-abp",
MEDICAID_ELIGIBILITY_LANDING = "/landing/medicaid-eligibility",
CHIP_ELIGIBILITY_LANDING = "/landing/chip-eligibility",
MEDICAID_ABP_LANDING = "/new-submission/spa/medicaid/landing/medicaid-abp",
MEDICAID_ELIGIBILITY_LANDING = "/new-submission/spa/medicaid/landing/medicaid-eligibility",
CHIP_ELIGIBILITY_LANDING = "/new-submission/spa/chip/landing/chip-eligibility",
CREATE = "/create",
}

Expand Down
2 changes: 1 addition & 1 deletion src/services/ui/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"lib": ["DOM", "DOM.Iterable", "ESNext", "ES2023"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
Expand Down
Loading
Loading