From 481f66db44884cc1286bf71c7e9514e91cfef597 Mon Sep 17 00:00:00 2001 From: ailZhou Date: Tue, 12 Nov 2024 16:50:16 -0500 Subject: [PATCH] pulling admin banner from other apps --- .../banners/AdminBannerProvider.tsx | 129 ++++++++++++++++++ .../ui-src/src/components/banners/Banner.tsx | 20 +++ .../src/components/banners/PreviewBanner.tsx | 22 +++ services/ui-src/src/components/index.ts | 7 + services/ui-src/src/constants.ts | 3 + services/ui-src/src/types/banners.ts | 23 ++++ services/ui-src/src/types/index.ts | 1 + services/ui-src/src/types/states.ts | 21 ++- .../utils/api/requestMethods/banner.test.ts | 31 +++++ .../src/utils/api/requestMethods/banner.ts | 40 ++++++ services/ui-src/src/utils/index.ts | 1 + services/ui-src/src/utils/state/useStore.ts | 34 ++++- .../ui-src/src/utils/testing/mockBanner.tsx | 9 ++ .../ui-src/src/utils/testing/setupJest.tsx | 21 ++- services/ui-src/src/verbiage/errors.ts | 19 +++ 15 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 services/ui-src/src/components/banners/AdminBannerProvider.tsx create mode 100644 services/ui-src/src/components/banners/Banner.tsx create mode 100644 services/ui-src/src/components/banners/PreviewBanner.tsx create mode 100644 services/ui-src/src/types/banners.ts create mode 100644 services/ui-src/src/utils/api/requestMethods/banner.test.ts create mode 100644 services/ui-src/src/utils/api/requestMethods/banner.ts create mode 100644 services/ui-src/src/utils/testing/mockBanner.tsx diff --git a/services/ui-src/src/components/banners/AdminBannerProvider.tsx b/services/ui-src/src/components/banners/AdminBannerProvider.tsx new file mode 100644 index 0000000..1dd18a5 --- /dev/null +++ b/services/ui-src/src/components/banners/AdminBannerProvider.tsx @@ -0,0 +1,129 @@ +import { createContext, ReactNode, useMemo, useEffect } from "react"; +// utils +import { AdminBannerData, AdminBannerShape } from "types/banners"; +import { bannerId } from "../../constants"; +import { bannerErrors } from "verbiage/errors"; +// api +import { + checkDateRangeStatus, + deleteBanner, + getBanner, + useStore, + writeBanner, +} from "utils"; + +const ADMIN_BANNER_ID = bannerId; + +export const AdminBannerContext = createContext({ + fetchAdminBanner: Function, + writeAdminBanner: Function, + deleteAdminBanner: Function, +}); + +export const AdminBannerProvider = ({ children }: Props) => { + // state management + const { + bannerData, + setBannerData, + bannerActive, + setBannerActive, + bannerLoading, + setBannerLoading, + bannerErrorMessage, + setBannerErrorMessage, + bannerDeleting, + setBannerDeleting, + } = useStore(); + + const fetchAdminBanner = async () => { + setBannerLoading(true); + try { + const currentBanner = await getBanner(ADMIN_BANNER_ID); + const newBannerData = currentBanner as AdminBannerData | undefined; + setBannerData(newBannerData); + setBannerErrorMessage(undefined); + } catch (e: any) { + // 404 expected when no current banner exists + if (!e.toString().includes("404")) { + setBannerErrorMessage(bannerErrors.GET_BANNER_FAILED); + } + } + setBannerLoading(false); + }; + + const deleteAdminBanner = async () => { + setBannerDeleting(true); + try { + await deleteBanner(ADMIN_BANNER_ID); + await fetchAdminBanner(); + } catch { + setBannerErrorMessage(bannerErrors.DELETE_BANNER_FAILED); + } + setBannerDeleting(false); + }; + + const writeAdminBanner = async (newBannerData: AdminBannerData) => { + try { + await writeBanner(newBannerData); + } catch { + setBannerErrorMessage(bannerErrors.CREATE_BANNER_FAILED); + } + await fetchAdminBanner(); + }; + + useEffect(() => { + let bannerActivity = false; + if (bannerData) { + bannerActivity = checkDateRangeStatus( + bannerData.startDate, + bannerData.endDate + ); + } + setBannerActive(bannerActivity); + }, [bannerData]); + + useEffect(() => { + fetchAdminBanner(); + }, []); + + const providerValue = useMemo( + () => ({ + // Banner Data + bannerData, + setBannerData, + // Banner showing + bannerActive, + setBannerActive, + // Banner Loading + bannerLoading, + setBannerLoading, + // Banner Error State + bannerErrorMessage, + setBannerErrorMessage, + // Banner Deleting State + bannerDeleting, + setBannerDeleting, + // Banner API calls + fetchAdminBanner, + writeAdminBanner, + deleteAdminBanner, + }), + [ + bannerData, + bannerActive, + bannerLoading, + bannerErrorMessage, + bannerDeleting, + ] + ); + + return ( + + {children} + + ); +}; + +interface Props { + children?: ReactNode; +} diff --git a/services/ui-src/src/components/banners/Banner.tsx b/services/ui-src/src/components/banners/Banner.tsx new file mode 100644 index 0000000..e4bba4f --- /dev/null +++ b/services/ui-src/src/components/banners/Banner.tsx @@ -0,0 +1,20 @@ +// components +import { Alert } from "components"; +// types +import { BannerData } from "types"; + +export const Banner = ({ bannerData, ...props }: Props) => { + if (bannerData) { + const { title, description, link } = bannerData; + return ( + bannerData && ( + + ) + ); + } else return <>; +}; + +interface Props { + bannerData: BannerData | undefined; + [key: string]: any; +} diff --git a/services/ui-src/src/components/banners/PreviewBanner.tsx b/services/ui-src/src/components/banners/PreviewBanner.tsx new file mode 100644 index 0000000..70c25e3 --- /dev/null +++ b/services/ui-src/src/components/banners/PreviewBanner.tsx @@ -0,0 +1,22 @@ +import { useFormContext } from "react-hook-form"; +// components +import { Banner } from "components"; + +export const PreviewBanner = ({ ...props }: Props) => { + // get the form context + const form = useFormContext(); + + // set banner preview data + const formData = form.getValues(); + const bannerData = { + title: formData["bannerTitle"] || "New banner title", + description: formData["bannerDescription"] || "New banner description", + link: formData["bannerLink"] || "", + }; + + return ; +}; + +interface Props { + [key: string]: any; +} diff --git a/services/ui-src/src/components/index.ts b/services/ui-src/src/components/index.ts index 24687d3..1cb0ca3 100644 --- a/services/ui-src/src/components/index.ts +++ b/services/ui-src/src/components/index.ts @@ -9,6 +9,13 @@ export { ErrorAlert } from "./alerts/ErrorAlert"; // app export { App } from "./app/App"; export { Error } from "./app/Error"; +// banners +export { + AdminBannerContext, + AdminBannerProvider, +} from "./banners/AdminBannerProvider"; +export { Banner } from "./banners/Banner"; +export { PreviewBanner } from "./banners/PreviewBanner"; // layout export { HomePage } from "./layout/HomePage"; export { Header } from "./layout/Header"; diff --git a/services/ui-src/src/constants.ts b/services/ui-src/src/constants.ts index f7af349..8aba192 100644 --- a/services/ui-src/src/constants.ts +++ b/services/ui-src/src/constants.ts @@ -1,3 +1,6 @@ +// BANNERS +export const bannerId = "admin-banner-id"; + // HOST DOMAIN export const PRODUCTION_HOST_DOMAIN = "mdcthcbs.cms.gov"; diff --git a/services/ui-src/src/types/banners.ts b/services/ui-src/src/types/banners.ts new file mode 100644 index 0000000..be5b1fe --- /dev/null +++ b/services/ui-src/src/types/banners.ts @@ -0,0 +1,23 @@ +// BANNER + +export interface BannerData { + title: string; + description: string; + link?: string; + [key: string]: any; +} + +export interface AdminBannerData extends BannerData { + key: string; + startDate: number; + endDate: number; + isActive?: boolean; +} + +export interface AdminBannerMethods { + fetchAdminBanner: Function; + writeAdminBanner: Function; + deleteAdminBanner: Function; +} + +export interface AdminBannerShape extends AdminBannerMethods {} diff --git a/services/ui-src/src/types/index.ts b/services/ui-src/src/types/index.ts index 4bf35d9..1bcc9b0 100644 --- a/services/ui-src/src/types/index.ts +++ b/services/ui-src/src/types/index.ts @@ -1,3 +1,4 @@ +export * from "./banners"; export * from "./users"; export * from "./states"; export * from "./other"; diff --git a/services/ui-src/src/types/states.ts b/services/ui-src/src/types/states.ts index 47b3d82..36a9c5c 100644 --- a/services/ui-src/src/types/states.ts +++ b/services/ui-src/src/types/states.ts @@ -1,6 +1,25 @@ import { ParentPageTemplate, PageData, Report } from "types/report"; import React from "react"; -import { HcbsUser } from "types"; +import { AdminBannerData, ErrorVerbiage, HcbsUser } from "types"; + +// initial admin banner state +export interface AdminBannerState { + // INITIAL STATE + bannerData: AdminBannerData | undefined; + bannerActive: boolean; + bannerLoading: boolean; + bannerErrorMessage: ErrorVerbiage | undefined; + bannerDeleting: boolean; + // ACTIONS + setBannerData: (newBannerData: AdminBannerData | undefined) => void; + clearAdminBanner: () => void; + setBannerActive: (bannerStatus: boolean) => void; + setBannerLoading: (bannerLoading: boolean) => void; + setBannerErrorMessage: ( + bannerErrorMessage: ErrorVerbiage | undefined + ) => void; + setBannerDeleting: (bannerDeleting: boolean) => void; +} // initial user state export interface HcbsUserState { diff --git a/services/ui-src/src/utils/api/requestMethods/banner.test.ts b/services/ui-src/src/utils/api/requestMethods/banner.test.ts new file mode 100644 index 0000000..fe44521 --- /dev/null +++ b/services/ui-src/src/utils/api/requestMethods/banner.test.ts @@ -0,0 +1,31 @@ +import { getBanner, writeBanner, deleteBanner } from "./banner"; +// utils +import { bannerId } from "../../../constants"; +import { mockBannerData } from "utils/testing/setupJest"; +import { initAuthManager } from "utils/auth/authLifecycle"; + +describe("utils/banner", () => { + beforeEach(async () => { + jest.useFakeTimers(); + initAuthManager(); + jest.runAllTimers(); + }); + + describe("getBanner()", () => { + test("executes", () => { + expect(getBanner(bannerId)).toBeTruthy(); + }); + }); + + describe("writeBanner()", () => { + test("executes", () => { + expect(writeBanner(mockBannerData)).toBeTruthy(); + }); + }); + + describe("deleteBanner()", () => { + test("executes", () => { + expect(deleteBanner(bannerId)).toBeTruthy(); + }); + }); +}); diff --git a/services/ui-src/src/utils/api/requestMethods/banner.ts b/services/ui-src/src/utils/api/requestMethods/banner.ts new file mode 100644 index 0000000..12dfbc6 --- /dev/null +++ b/services/ui-src/src/utils/api/requestMethods/banner.ts @@ -0,0 +1,40 @@ +import { API } from "aws-amplify"; +import { getRequestHeaders } from "./getRequestHeaders"; +import { AdminBannerData } from "types/banners"; +import { updateTimeout } from "utils"; + +async function getBanner(bannerKey: string) { + const requestHeaders = await getRequestHeaders(); + const request = { + headers: { ...requestHeaders }, + }; + + updateTimeout(); + const response = await API.get("mfp", `/banners/${bannerKey}`, request); + return response; +} + +async function writeBanner(bannerData: AdminBannerData) { + const requestHeaders = await getRequestHeaders(); + const request = { + headers: { ...requestHeaders }, + body: bannerData, + }; + + updateTimeout(); + const response = await API.post("mfp", `/banners/${bannerData.key}`, request); + return response; +} + +async function deleteBanner(bannerKey: string) { + const requestHeaders = await getRequestHeaders(); + const request = { + headers: { ...requestHeaders }, + }; + + updateTimeout(); + const response = await API.del("mfp", `/banners/${bannerKey}`, request); + return response; +} + +export { getBanner, writeBanner, deleteBanner }; diff --git a/services/ui-src/src/utils/index.ts b/services/ui-src/src/utils/index.ts index dc25fcf..916f1cd 100644 --- a/services/ui-src/src/utils/index.ts +++ b/services/ui-src/src/utils/index.ts @@ -2,6 +2,7 @@ export * from "./api/apiLib"; export * from "./api/providers/ApiProvider"; export * from "./api/requestMethods/getTemplateUrl"; +export * from "./api/requestMethods/banner"; // auth export * from "./auth/UserProvider"; export * from "./auth/authLifecycle"; diff --git a/services/ui-src/src/utils/state/useStore.ts b/services/ui-src/src/utils/state/useStore.ts index 26af9c6..be7124e 100644 --- a/services/ui-src/src/utils/state/useStore.ts +++ b/services/ui-src/src/utils/state/useStore.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; -import { HcbsUserState, HcbsUser, HcbsReportState } from "types"; +import { HcbsUserState, HcbsUser, HcbsReportState, AdminBannerData, ErrorVerbiage } from "types"; import { Report } from "types/report"; import React from "react"; import { buildState, mergeAnswers, setPage } from "./management/reportState"; @@ -19,6 +19,37 @@ const userStore = (set: Function) => ({ set(() => ({ showLocalLogins: true }), false, { type: "showLocalLogins" }), }); +// BANNER STORE +const bannerStore = (set: Function) => ({ + // initial state + bannerData: undefined, + bannerActive: false, + bannerLoading: false, + bannerErrorMessage: undefined, + bannerDeleting: false, + // actions + setBannerData: (newBanner: AdminBannerData | undefined) => + set(() => ({ bannerData: newBanner }), false, { type: "setBannerData" }), + clearAdminBanner: () => + set(() => ({ bannerData: undefined }), false, { type: "clearAdminBanner" }), + setBannerActive: (bannerStatus: boolean) => + set(() => ({ bannerActive: bannerStatus }), false, { + type: "setBannerActive", + }), + setBannerLoading: (loading: boolean) => + set(() => ({ bannerLoading: loading }), false, { + type: "setBannerLoading", + }), + setBannerErrorMessage: (errorMessage: ErrorVerbiage | undefined) => + set(() => ({ bannerErrorMessage: errorMessage }), false, { + type: "setBannerErrorMessage", + }), + setBannerDeleting: (deleting: boolean) => + set(() => ({ bannerDeleting: deleting }), false, { + type: "setBannerDeleting", + }), +}); + // REPORT STORE const reportStore = (set: Function): HcbsReportState => ({ // initial state @@ -56,6 +87,7 @@ export const useStore = create( persist( devtools((set) => ({ ...userStore(set), + ...bannerStore(set), ...reportStore(set), })), { diff --git a/services/ui-src/src/utils/testing/mockBanner.tsx b/services/ui-src/src/utils/testing/mockBanner.tsx new file mode 100644 index 0000000..4170535 --- /dev/null +++ b/services/ui-src/src/utils/testing/mockBanner.tsx @@ -0,0 +1,9 @@ +import { bannerId } from "../../constants"; + +export const mockBannerData = { + key: bannerId, + title: "Yes here I am, a banner", + description: "I have a description too thank you very much", + startDate: 1640995200000, // 1/1/2022 00:00:00 UTC + endDate: 1672531199000, // 12/31/2022 23:59:59 UTC +}; diff --git a/services/ui-src/src/utils/testing/setupJest.tsx b/services/ui-src/src/utils/testing/setupJest.tsx index 81ee20f..56c1400 100644 --- a/services/ui-src/src/utils/testing/setupJest.tsx +++ b/services/ui-src/src/utils/testing/setupJest.tsx @@ -3,7 +3,8 @@ import { BrowserRouter as Router } from "react-router-dom"; import "@testing-library/jest-dom"; import "jest-axe/extend-expect"; import { mockFlags, resetLDMocks } from "jest-launchdarkly-mock"; -import { UserRoles, HcbsUserState, UserContextShape } from "types"; +import { UserRoles, HcbsUserState, UserContextShape, AdminBannerState } from "types"; +import { mockBannerData } from "./mockBanner"; // GLOBALS global.React = React; @@ -81,6 +82,22 @@ jest.mock("aws-amplify/auth", () => ({ signInWithRedirect: () => {}, })); +// BANNER STATES / STORE + +export const mockBannerStore: AdminBannerState = { + bannerData: mockBannerData, + bannerActive: false, + bannerLoading: false, + bannerErrorMessage: { title: "", description: "" }, + bannerDeleting: false, + setBannerData: () => {}, + clearAdminBanner: () => {}, + setBannerActive: () => {}, + setBannerLoading: () => {}, + setBannerErrorMessage: () => {}, + setBannerDeleting: () => {}, +}; + // USER CONTEXT export const mockUserContext: UserContextShape = { @@ -184,6 +201,8 @@ export const mockLDClient = { // ASSET export * from "./mockAsset"; +// BANNER +export * from "./mockBanner"; // FORM export * from "./mockForm"; // ROUTER diff --git a/services/ui-src/src/verbiage/errors.ts b/services/ui-src/src/verbiage/errors.ts index 37ffe77..c16de18 100644 --- a/services/ui-src/src/verbiage/errors.ts +++ b/services/ui-src/src/verbiage/errors.ts @@ -19,3 +19,22 @@ export const genericErrorContent = [ content: ".", }, ]; + +export const bannerErrors = { + GET_BANNER_FAILED: { + title: "Banner could not be fetched", + description: genericErrorContent, + }, + REPLACE_BANNER_FAILED: { + title: "Current banner could not be replaced.", + description: genericErrorContent, + }, + DELETE_BANNER_FAILED: { + title: "Current banner could not be deleted", + description: genericErrorContent, + }, + CREATE_BANNER_FAILED: { + title: "Could not create a banner.", + description: genericErrorContent, + }, +};