Skip to content

Commit

Permalink
feat(alerts): Modal and Alert banner upgrades (#366)
Browse files Browse the repository at this point in the history
* Undo template: IssueRai

* Undo template: RespondToRai

* Undo template: WithdrawRai

* Undo template: WithdrawPackage

* Initialize alert content

* Initialize alert content

* Remove random modal usage from ActionCard

* Remove unused alert banner from Details

* Remove unused alert banner from Details

* Initialize new modal context

* Plug in context provider

* Implement CHIP SPA cancel modal; refine interface

* Implement Medicaid SPA cancel modal

* Undo handleSubmit abstraction: IssueRai

* Undo handleSubmit abstraction: RespondToRai

* Undo handleSubmit abstraction: WithdrawRai

* Undo handleSubmit abstraction: WithdrawPackage

* Fix Medicaid SPA routing

* Implement IssueRai cancel modal

* Implement RespondToRai cancel modal

* Implement WithdrawRai cancel modal

* Implement WithdrawPackage cancel modal

* Implement ToggleRaiWithdraw cancel modal

* Remove old modal context

* Modify modalContext for custom onAccept

* Modify Medicaid SPA cancel modal onAccept

* Modify CHIP SPA cancel modal onAccept

* Modify IssueRai cancel modal onAccept

* Modify RespondToRai cancel modal onAccept

* Modify ToggleRaiWithdraw cancel modal onAccept

* Modify WithdrawPackage cancel modal onAccept

* Modify WithdrawRai cancel modal onAccept

* Fix setOnSubmit calls

* Implement confirmation modal for WithdrawPackage

* Implement confirmation modal for WithdrawRai

* Initialize AlertContext

* Style success Alert variant

* Implement alert context for CHIP SPA

* Implement alert context for Medicaid SPA

* Add close button to Alert ui

* Implement alert context for IssueRai

* Implement alert context for RespondToRai

* Implement alert context for ToggleRaiWithdraw

* Implement alert context for WithdrawRai

* Implement alert context for WithdrawPackage

* Update alert with route based hiding

* Update alert with route based hiding; remaining forms

* Update Withdraw confirmation pattern

* Undo destructure for contexts: Medicaid SPA

* Undo destructure for contexts: CHIP SPA

* Undo destructure for contexts: IssueRai

* Undo destructure for contexts: RespondToRai

* Undo destructure for contexts: ToggleRaiWithdraw

* Undo destructure for contexts: WithdrawRai

* Undo destructure for contexts: WithdrawPackage

* Fix isLoading var source

* Update docs with code style

* Add alertContext tests

* Fix alertContext test

* Add modalContext tests

* Implement banners and modals on capitated b waivers

* Implement banners and modals on contracting b waivers

* Fix routing

* Add origin query

* Origin navigation: CHIP SPA

* Origin navigation: Medicaid SPA

* Origin navigation: Capitated B waivers

* Origin navigation: Contracting B waivers

* Origin navigation: Tooling update

* Origin navigation: Actions

* Docs: formOrigin util

* Cleanup: formOrigin
  • Loading branch information
Kevin Haube authored Feb 13, 2024
1 parent 67f5afb commit 91ec0bb
Show file tree
Hide file tree
Showing 28 changed files with 1,478 additions and 629 deletions.
86 changes: 86 additions & 0 deletions docs/docs/team-norms/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
layout: default
title: Code Style Cheatsheet
parent: Team Norms
nav_order: 2
---

# Code Style

### What is it?

A set of rules or guidelines used when writing the source code for a computer program.

### Why is it?

It keeps the code looking neat and tidy, so anyone on the team can jump in and not
get lost in a jungle of curly braces and indentation levels. It's like everyone speaking
the same slang in a super cool secret club. Plus, it saves time from arguing over trivial
stuff, like whether tabs are better than spaces, so there's more time for the fun
stuff—coding and creating!

# OneMAC Style Norms

## Cheatsheet

TL;DR? No worries, here's a cheatsheet of the concepts outlined below:

- [DO NOT destructure](#object-access) so we maintain object context for methods and properties used in code

### Object Access

When integrating with complex objects, consider maintaining the object's integrity rather
than opting for destructuring. This approach ensures that the object's context is
preserved, enhancing readability and maintainability.

#### Nomenclature simplification

For instance, rather than breaking
down the object into individual variables, which can lead to verbose and confusing naming
conventions, maintain the object as a whole.

```typescript jsx
const {
setModalOpen,
setContent: setModalContent,
setOnAccept: setModalOnAccept,
} = useModalContext();
const {
setContent: setBannerContent,
setBannerShow,
setBannerDisplayOn,
} = useAlertContext();

// vs

const modal = useModalContext();
const alert = useAlertContext();
```

#### Usage implication

This method simplifies reference to its properties and methods, providing a clearer and
more direct understanding of its usage within the code. This strategy is particularly
beneficial in scenarios where the object's structure and context significantly contribute
to its functionality and meaning in the application.

```typescript jsx
<form
onSubmit={form.handleSubmit(async (data) => {
try {
await submit({
//...
});
alert.setContent({
header: "RAI response submitted",
body: `The RAI response for ${item._source.id} has been submitted.`,
});
alert.setBannerShow(true);
alert.setBannerDisplayOn("/dashboard");
navigate({ path: "/dashboard" });
} catch (e) {
//...
}
})}
>
```
2 changes: 2 additions & 0 deletions src/services/ui/src/components/Alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const alertVariants = cva(
"border-destructive/50 text-destructive [&>svg]:text-destructive",
infoBlock:
"border-l-[6px] border-y-0 border-r-0 border-cyan-500 bg-cyan-300/10 rounded-none",
success:
"border-l-[6px] border-[#2E8540] border-y-0 border-r-0 bg-[#E7F4E4]",
},
},
defaultVariants: {
Expand Down
2 changes: 1 addition & 1 deletion src/services/ui/src/components/Cards/OptionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const OptionCard = ({
}: MACFieldsetOption) => {
return (
<label>
<Link to={linkTo} relative={"path"}>
<Link to={`${linkTo}?origin=options`} relative={"path"}>
<div
data-testid={"card-inner-wrapper"}
className={`flex items-center justify-between gap-6 px-6 py-4 ${
Expand Down
64 changes: 64 additions & 0 deletions src/services/ui/src/components/Context/alertContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { act, render, renderHook, screen } from "@testing-library/react";
import {
AlertProvider,
useAlertContext,
} from "@/components/Context/alertContext";
import { describe, test, expect } from "vitest";
import { PropsWithChildren, useEffect } from "react";
import { MemoryRouter } from "react-router-dom";

const wrapper = ({ children }: PropsWithChildren) => (
<MemoryRouter>
<AlertProvider>{children}</AlertProvider>
</MemoryRouter>
);

describe("useAlertContext", () => {
test("hook provides content values and setter", () => {
const ctx = renderHook(useAlertContext, { wrapper });
expect(ctx.result.current.content).toStrictEqual({
header: "No header given",
body: "No body given",
});
act(() =>
ctx.result.current.setContent({
header: "New header",
body: "New body",
})
);
expect(ctx.result.current.content).toStrictEqual({
header: "New header",
body: "New body",
});
});
test("hook provides switch for showing the banner, defaults to off", () => {
const ctx = renderHook(useAlertContext, { wrapper });
expect(ctx.result.current.bannerShow).toBe(false);
act(() => ctx.result.current.setBannerShow(true));
expect(ctx.result.current.bannerShow).toBe(true);
});
test("hook provides route-specific display criteria and setter", () => {
const ctx = renderHook(useAlertContext, { wrapper });
expect(ctx.result.current.bannerDisplayOn).toBe("/");
act(() => ctx.result.current.setBannerDisplayOn("/dashboard"));
expect(ctx.result.current.bannerDisplayOn).toBe("/dashboard");
});
});

const TestChild = () => {
const alert = useAlertContext();
useEffect(() => {
alert.setBannerShow(true);
}, []);
return <h1>test</h1>;
};
describe("AlertProvider", () => {
test("renders alert component above children", () => {
render(<TestChild />, { wrapper });
const child = screen.getByText("test");
const alert = screen.getByText("No header given");
expect(child.compareDocumentPosition(alert)).toBe(
Node.DOCUMENT_POSITION_PRECEDING
);
});
});
74 changes: 74 additions & 0 deletions src/services/ui/src/components/Context/alertContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { PropsWithChildren, useEffect, useState } from "react";
import { Check, X } from "lucide-react";
import { createContextProvider } from "@/utils";
import { Alert, SimplePageContainer } from "@/components";
import { useLocation } from "react-router-dom";
import { Route } from "@/components/Routing/types";

type BannerContent = {
header: string;
body: string;
};

const useAlertController = () => {
const [bannerShow, setBannerShow] = useState<boolean>(false);
const [bannerDisplayOn, setBannerDisplayOn] = useState<Route>("/");
const [content, setContent] = useState<BannerContent>({
header: "No header given",
body: "No body given",
});
return {
content,
setContent,
bannerDisplayOn,
setBannerDisplayOn,
bannerShow,
setBannerShow,
};
};

export const [AlertContextProvider, useAlertContext] = createContextProvider<
ReturnType<typeof useAlertController>
>({
name: "Banner Context",
errorMessage:
"This component requires the `AlertProvider` wrapper to make use of banner UIs.",
});

export const AlertProvider = ({ children }: PropsWithChildren) => {
const context = useAlertController();
const location = useLocation();
/* When a form redirects on success, these two values will match.
* Once a user navigates away from that path, we set the show boolean
* to false, so we ensure it won't show again if they navigate back to
* the Route defined in context.bannerDisplayOn */
useEffect(() => {
if (context.bannerDisplayOn !== location.pathname)
context.setBannerShow(false);
}, [location.pathname]);
return (
<AlertContextProvider value={context}>
{/* Relies on the effect above to swap context.bannerShow boolean on
* Route change*/}
{context.bannerDisplayOn === location.pathname && context.bannerShow && (
<SimplePageContainer>
<Alert variant={"success"} className="mt-4 mb-8 flex-row text-sm">
<div className={"flex items-start justify-between"}>
<Check />
<div className={"ml-2 w-full"}>
<h3 className={"text-lg font-bold"}>
{context.content.header}
</h3>
<p>{context.content.body}</p>
</div>
<button onClick={() => context.setBannerShow(false)}>
<X size={20} />
</button>
</div>
</Alert>
</SimplePageContainer>
)}
{children}
</AlertContextProvider>
);
};
72 changes: 72 additions & 0 deletions src/services/ui/src/components/Context/modalContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { act, render, renderHook, screen } from "@testing-library/react";
import { describe, test, expect, vi } from "vitest";
import { PropsWithChildren, useEffect } from "react";
import {
useModalContext,
ModalProvider,
} from "@/components/Context/modalContext";
import { MemoryRouter } from "react-router-dom";

const wrapper = ({ children }: PropsWithChildren) => (
<MemoryRouter>
<ModalProvider>{children}</ModalProvider>
</MemoryRouter>
);

describe("useAlertContext", () => {
test("hook provides content values and setter", () => {
const ctx = renderHook(useModalContext, { wrapper });
expect(ctx.result.current.content).toStrictEqual({
header: "No header given",
body: "No body given",
});
act(() =>
ctx.result.current.setContent({
header: "New header",
body: "New body",
})
);
expect(ctx.result.current.content).toStrictEqual({
header: "New header",
body: "New body",
});
});
test("hook provides switch for showing the modal, defaults to off", () => {
const ctx = renderHook(useModalContext, { wrapper });
expect(ctx.result.current.modalOpen).toBe(false);
act(() => ctx.result.current.setModalOpen(true));
expect(ctx.result.current.modalOpen).toBe(true);
});
test("hook provides onAccept action property and setter", () => {
const ctx = renderHook(useModalContext, { wrapper });
const action = vi.fn(() => console.log("test"));
expect(ctx.result.current.onAccept).toBe(void {});
act(() => ctx.result.current.setOnAccept(() => action));
act(() => ctx.result.current.onAccept());
expect(action).toHaveBeenCalledOnce();
});
});

describe("ModalProvider", () => {
test("accept button is operational", () => {
const action = vi.fn(() => console.log("test"));
const TestChild = () => {
const modal = useModalContext();
useEffect(() => {
modal.setContent({
header: "Test header",
body: "Test body",
acceptButtonText: "Accept",
cancelButtonText: "Cancel",
});
modal.setOnAccept(() => action);
modal.setModalOpen(true);
}, []);
return <h1>test</h1>;
};
render(<TestChild />, { wrapper });
const accept = screen.getByText("Accept");
accept.click();
expect(action).toHaveBeenCalledOnce();
});
});
54 changes: 54 additions & 0 deletions src/services/ui/src/components/Context/modalContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PropsWithChildren, useState } from "react";
import { createContextProvider } from "@/utils";
import { ConfirmationModal } from "@/components";

type SubmissionAlert = {
header: string;
body: string;
cancelButtonText?: string;
acceptButtonText?: string;
};
const useModalController = () => {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [onAccept, setOnAccept] = useState<VoidFunction>(() => void {});
const [content, setContent] = useState<SubmissionAlert>({
header: "No header given",
body: "No body given",
});
return {
content,
setContent,
onAccept,
setOnAccept,
modalOpen,
setModalOpen,
};
};

export const [ModalContextProvider, useModalContext] = createContextProvider<
ReturnType<typeof useModalController>
>({
name: "Modal Context",
errorMessage:
"This component requires the `ModalProvider` wrapper to make use of modal UIs.",
});

export const ModalProvider = ({ children }: PropsWithChildren) => {
const context = useModalController();
return (
<ModalContextProvider value={context}>
{children}
<ConfirmationModal
open={context.modalOpen}
onAccept={context.onAccept}
onCancel={() => context.setModalOpen(false)}
cancelButtonVisible={context.content.cancelButtonText !== undefined}
acceptButtonVisible={context.content.acceptButtonText !== undefined}
cancelButtonText={context.content.cancelButtonText}
acceptButtonText={context.content.acceptButtonText}
title={context.content.header}
body={context.content.body}
/>
</ModalContextProvider>
);
};
Loading

0 comments on commit 91ec0bb

Please sign in to comment.