-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(alerts): Modal and Alert banner upgrades (#366)
* 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
Showing
28 changed files
with
1,478 additions
and
629 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
//... | ||
} | ||
})} | ||
> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
src/services/ui/src/components/Context/alertContext.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
72
src/services/ui/src/components/Context/modalContext.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.