From 095fc121035d1cc7d8cdbea42cf09f852a716f50 Mon Sep 17 00:00:00 2001 From: Toyo Date: Wed, 4 Sep 2024 10:33:28 -0500 Subject: [PATCH 01/17] fetch permissable values --- src/hooks/useBuildReduxStore.ts | 96 ++++++++++++++++++++++++++++++++- tsconfig.json | 1 + 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/hooks/useBuildReduxStore.ts b/src/hooks/useBuildReduxStore.ts index e1bf3feb..24a137b3 100644 --- a/src/hooks/useBuildReduxStore.ts +++ b/src/hooks/useBuildReduxStore.ts @@ -8,6 +8,8 @@ import { } from "data-model-navigator"; import ReduxThunk from "redux-thunk"; import { createLogger } from "redux-logger"; +// import { useLazyQuery } from "@apollo/client"; +import { defaultTo, noop } from "lodash"; import { baseConfiguration, defaultReadMeTitle, graphViewConfig } from "../config/ModelNavigator"; import { buildAssetUrls, buildBaseFilterContainers, buildFilterOptionsList } from "../utils"; @@ -31,6 +33,83 @@ const makeStore = (): Store => { return newStore; }; +/** + * A function to parse the datalist and reolace enums with those returned from retrieveCde query + * + * @params {void} + */ +const updateEnums = (cdeMap, dataList, response = []) => { + // const values = Array.from(cdeMap.values()); + + const responseMap = new Map(); + + defaultTo(response, []).forEach((item) => + responseMap.set(`${item.CDECode}.${item.CDEVersion}`, item) + ); + + const resultMap = new Map(); + + cdeMap.forEach((_, key) => { + const [, cdeCodeAndVersion] = key.split(";"); + const item = responseMap.get(cdeCodeAndVersion); + + if (item) { + resultMap.set(key, item); + } + }); + + const newObj = JSON.parse(JSON.stringify(dataList)); + + const mapKeyPrefixes = new Map(); + for (const mapKey of resultMap.keys()) { + const prefix = mapKey.split(";")[0]; + mapKeyPrefixes.set(prefix, mapKey); + } + + function traverseAndReplace(node, parentKey = "") { + if (typeof node !== "object" || node === null) { + return; + } + + if (node.properties) { + for (const key in node.properties) { + if (Object.hasOwn(node.properties, key)) { + const fullKey = `${parentKey}.${key}`.replace(/^\./, ""); + if (mapKeyPrefixes.has(fullKey)) { + const mapFullKey = mapKeyPrefixes.get(fullKey); + const mapData = resultMap.get(mapFullKey); + + if (mapData && mapData.permissibleValues && mapData.permissibleValues.length > 0) { + node.properties[key].enum = mapData.permissibleValues; + } else { + node.properties[key].enum = [ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]; + } + } else if ( + !Object.hasOwn(node.properties[key], "enum") || + node.properties[key].enum.length === 0 + ) { + node.properties[key].enum = [ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]; + } + } + } + } + + for (const subKey in node) { + if (Object.hasOwn(node, subKey)) { + traverseAndReplace(node[subKey], `${parentKey}.${subKey}`); + } + } + } + + traverseAndReplace(newObj); + + return newObj; +}; + /** * A hook to build and populate the Redux store with DMN data * @@ -44,6 +123,16 @@ const useBuildReduxStore = (): [ const [status, setStatus] = useState("waiting"); const [store, setStore] = useState(makeStore()); + // will call retrieveCDEs here + /* const [getInstituitions, { data, loading, error }] = useLazyQuery( + LIST_INSTITUTIONS, + { + context: { clientName: "backend" }, + fetchPolicy: "cache-and-network", + } + ); + console.log("data from fe -->", data); */ + /** * Rebuilds the store from scratch * @@ -73,7 +162,12 @@ const useBuildReduxStore = (): [ setStatus("loading"); const assets = buildAssetUrls(datacommon); - const response = await getModelExploreData(assets.model, assets.props)?.catch((e) => { + const response = await getModelExploreData( + assets.model, + assets.props, + noop, // retrieveCDEs lazyQuery + updateEnums + )?.catch((e) => { console.error(e); return null; }); diff --git a/tsconfig.json b/tsconfig.json index ca88fcbd..6adec678 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es5", + "downlevelIteration": true, // fix Type 'Element' is not assignable to type 'ReactNode TS warning "paths": { "react": ["./node_modules/@types/react"] From 9ede702a488110eb4158401a10ee2a822e8aa598 Mon Sep 17 00:00:00 2001 From: Toyo Date: Wed, 4 Sep 2024 11:47:25 -0500 Subject: [PATCH 02/17] fetch permissable values --- src/hooks/useBuildReduxStore.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/hooks/useBuildReduxStore.ts b/src/hooks/useBuildReduxStore.ts index 24a137b3..45734a77 100644 --- a/src/hooks/useBuildReduxStore.ts +++ b/src/hooks/useBuildReduxStore.ts @@ -9,9 +9,9 @@ import { import ReduxThunk from "redux-thunk"; import { createLogger } from "redux-logger"; // import { useLazyQuery } from "@apollo/client"; -import { defaultTo, noop } from "lodash"; import { baseConfiguration, defaultReadMeTitle, graphViewConfig } from "../config/ModelNavigator"; import { buildAssetUrls, buildBaseFilterContainers, buildFilterOptionsList } from "../utils"; +// import { LIST_INSTITUTIONS, ListInstitutionsResp } from "../graphql"; export type Status = "waiting" | "loading" | "error" | "success"; @@ -35,10 +35,10 @@ const makeStore = (): Store => { /** * A function to parse the datalist and reolace enums with those returned from retrieveCde query - * + * Commented out until api is ready * @params {void} */ -const updateEnums = (cdeMap, dataList, response = []) => { +/* const updateEnums = (cdeMap, dataList, response = []) => { // const values = Array.from(cdeMap.values()); const responseMap = new Map(); @@ -108,7 +108,7 @@ const updateEnums = (cdeMap, dataList, response = []) => { traverseAndReplace(newObj); return newObj; -}; +}; */ /** * A hook to build and populate the Redux store with DMN data @@ -130,8 +130,7 @@ const useBuildReduxStore = (): [ context: { clientName: "backend" }, fetchPolicy: "cache-and-network", } - ); - console.log("data from fe -->", data); */ + ); */ /** * Rebuilds the store from scratch @@ -162,12 +161,7 @@ const useBuildReduxStore = (): [ setStatus("loading"); const assets = buildAssetUrls(datacommon); - const response = await getModelExploreData( - assets.model, - assets.props, - noop, // retrieveCDEs lazyQuery - updateEnums - )?.catch((e) => { + const response = await getModelExploreData(assets.model, assets.props)?.catch((e) => { console.error(e); return null; }); @@ -176,11 +170,22 @@ const useBuildReduxStore = (): [ return; } + // let dictionary; + /* if (response.cdeMap) { + const deets = await getInstituitions();]] + if (deets?.data) { + dictionary = updateEnums(response?.cdeMap, response.data, []); + } + } else { + dictionary = response.data; + } */ + const dictionary = response.data; + store.dispatch({ type: "RECEIVE_VERSION_INFO", data: response.version }); store.dispatch({ type: "REACT_FLOW_GRAPH_DICTIONARY", - dictionary: response.data, + dictionary, pdfDownloadConfig: datacommon.configuration.pdfConfig, graphViewConfig, }); @@ -188,7 +193,7 @@ const useBuildReduxStore = (): [ store.dispatch({ type: "RECEIVE_DICTIONARY", payload: { - data: response.data, + data: dictionary, facetfilterConfig: { ...baseConfiguration, facetSearchData: datacommon.configuration.facetFilterSearchData, From 77ea1cdfe0d10121c391359e9263a4c84caf60bc Mon Sep 17 00:00:00 2001 From: Alec M Date: Mon, 23 Sep 2024 10:29:50 -0400 Subject: [PATCH 03/17] CRDCDH-1612 Init Version History page --- .../DataSubmissions/UserGuide.test.tsx | 4 +- src/components/DataSubmissions/UserGuide.tsx | 2 +- src/components/Footer/FooterDesktop.tsx | 64 ++++++----- src/components/Footer/FooterMobile.tsx | 34 +++--- src/components/Footer/FooterTablet.tsx | 63 +++++----- src/components/Footer/index.test.tsx | 18 ++- .../components/HeaderTabletAndMobile.tsx | 2 +- .../Header/components/LogoDesktop.tsx | 2 +- .../Header/components/LogoMobile.tsx | 2 +- .../Header/components/NavbarDesktop.tsx | 2 +- .../Header/components/USABanner.tsx | 2 +- .../{globalFooterData.tsx => FooterConfig.ts} | 16 ++- .../{globalHeaderData.tsx => HeaderConfig.ts} | 0 src/layouts/index.tsx | 2 +- src/types/Navigation.d.ts | 72 ++++++++++++ src/utils/envUtils.test.ts | 108 ++++++++++++++++++ src/utils/envUtils.ts | 35 ++++++ src/utils/index.ts | 1 + 18 files changed, 343 insertions(+), 86 deletions(-) rename src/config/{globalFooterData.tsx => FooterConfig.ts} (91%) rename src/config/{globalHeaderData.tsx => HeaderConfig.ts} (100%) create mode 100644 src/utils/envUtils.test.ts create mode 100644 src/utils/envUtils.ts diff --git a/src/components/DataSubmissions/UserGuide.test.tsx b/src/components/DataSubmissions/UserGuide.test.tsx index c41ebe6b..7f6a3b9a 100644 --- a/src/components/DataSubmissions/UserGuide.test.tsx +++ b/src/components/DataSubmissions/UserGuide.test.tsx @@ -4,8 +4,8 @@ import { BrowserRouter as Router } from "react-router-dom"; import { axe } from "jest-axe"; import { UserGuide } from "./UserGuide"; -jest.mock("../../config/globalHeaderData", () => ({ - ...jest.requireActual("../../config/globalHeaderData"), +jest.mock("../../config/HeaderConfig", () => ({ + ...jest.requireActual("../../config/HeaderConfig"), DataSubmissionInstructionsLink: "/data-submission-instructions", })); diff --git a/src/components/DataSubmissions/UserGuide.tsx b/src/components/DataSubmissions/UserGuide.tsx index 65babb10..32c0306a 100644 --- a/src/components/DataSubmissions/UserGuide.tsx +++ b/src/components/DataSubmissions/UserGuide.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { Typography, styled } from "@mui/material"; import { Link } from "react-router-dom"; -import { DataSubmissionInstructionsLink } from "../../config/globalHeaderData"; +import { DataSubmissionInstructionsLink } from "../../config/HeaderConfig"; const StyledText = styled(Typography)({ fontFamily: "Nunito", diff --git a/src/components/Footer/FooterDesktop.tsx b/src/components/Footer/FooterDesktop.tsx index 3fef3496..84e816e7 100644 --- a/src/components/Footer/FooterDesktop.tsx +++ b/src/components/Footer/FooterDesktop.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from "react"; import { styled } from "@mui/material"; -import FooterData from "../../config/globalFooterData"; +import FooterData from "../../config/FooterConfig"; const FooterStyled = styled("div")({ backgroundColor: "#1B496E", @@ -83,13 +83,14 @@ const FooterLinksContainer = styled("div")({ "& .footItemSubtitle": { marginBottom: "10px", maxWidth: "290px", - }, - "& .footItemLink": { fontFamily: "Open Sans", color: "#FFFFFF", fontWeight: 400, fontSize: "16px", lineHeight: "22px", + }, + "& .footItemLink": { + color: "inherit", textDecoration: "none", "&:hover": { textDecoration: "underline", @@ -217,40 +218,45 @@ const FooterDesktop = () => { const handleChange = (e) => { setEmailContent(e.target.value); }; + return ( <> - {FooterData.link_sections.map((linkItem, linkidx) => { - const linkkey = `link_${linkidx}`; - return ( -
-
{linkItem.title}
- {linkItem.items.map((item, itemidx) => { - const itemkey = `item_${itemidx}`; + {FooterData.link_sections.map((linkItem) => ( +
+
{linkItem.title}
+ {linkItem.items.map((item) => { + if (typeof item?.link !== "string") { return ( -
- {item.link.includes("http") ? ( - - {item.text} - - ) : ( - - {item.text} - - )} +
+ {item.text}
); - })} -
- ); - })} + } + + return ( +
+ {item.link.includes("http") ? ( + + {item.text} + + ) : ( + + {item.text} + + )} +
+ ); + })} +
+ ))} { - {FooterData.link_sections.map((linkItem, linkidx) => { - const linkkey = `link_${linkidx}`; + {FooterData.link_sections.map((linkItem) => { + const linkKey = `link_${linkItem.title}`; return ( -
- -
- {linkItem.items.map((item, itemidx) => { - const itemkey = `item_${itemidx}`; +
+ {linkItem.items.map((item) => { + const itemKey = `item_${item.text}`; + if (typeof item?.link !== "string") { + return ( + + {item.text} + + ); + } + return item.link.includes("http") ? ( { {item.text} ) : ( - + {item.text} ); diff --git a/src/components/Footer/FooterTablet.tsx b/src/components/Footer/FooterTablet.tsx index edd8441d..1b2a2fb8 100644 --- a/src/components/Footer/FooterTablet.tsx +++ b/src/components/Footer/FooterTablet.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from "react"; import { styled } from "@mui/material"; -import FooterData from "../../config/globalFooterData"; +import FooterData from "../../config/FooterConfig"; const FooterStyled = styled("footer")({ backgroundColor: "#1B496E", @@ -85,13 +85,14 @@ const FooterLinksContainer = styled("div")({ "& .footItemSubtitle": { marginBottom: "10px", maxWidth: "290px", - }, - "& .footItemLink": { fontFamily: "Open Sans", color: "#FFFFFF", fontWeight: 400, fontSize: "16px", lineHeight: "22px", + }, + "& .footItemLink": { + color: "inherit", textDecoration: "none", "&:hover": { textDecoration: "underline", @@ -233,35 +234,39 @@ const FooterTablet = () => { - {FooterData.link_sections.map((linkItem, linkidx) => { - const linkkey = `link_${linkidx}`; - return ( -
-
{linkItem.title}
- {linkItem.items.map((item, itemidx) => { - const itemkey = `item_${itemidx}`; + {FooterData.link_sections.map((linkItem) => ( +
+
{linkItem.title}
+ {linkItem.items.map((item) => { + if (typeof item?.link !== "string") { return ( -
- {item.link.includes("http") ? ( - - {item.text} - - ) : ( - - {item.text} - - )} +
+ {item.text}
); - })} -
- ); - })} + } + + return ( +
+ {item.link.includes("http") ? ( + + {item.text} + + ) : ( + + {item.text} + + )} +
+ ); + })} +
+ ))} { - const { container } = render(
); - const results = await axe(container); +describe("Footer", () => { + beforeEach(() => { + jest.spyOn(console, "error").mockImplementation(); + }); - expect(results).toHaveNoViolations(); + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should not have any accessibility violations", async () => { + const { container } = render(
); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); }); diff --git a/src/components/Header/components/HeaderTabletAndMobile.tsx b/src/components/Header/components/HeaderTabletAndMobile.tsx index 6a1202c4..b330d675 100644 --- a/src/components/Header/components/HeaderTabletAndMobile.tsx +++ b/src/components/Header/components/HeaderTabletAndMobile.tsx @@ -5,7 +5,7 @@ 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 { navMobileList, navbarSublists } from "../../../config/HeaderConfig"; import { GenerateApiTokenRoles } from "../../../config/AuthRoles"; import { useAuthContext } from "../../Contexts/AuthContext"; import GenericAlert from "../../GenericAlert"; diff --git a/src/components/Header/components/LogoDesktop.tsx b/src/components/Header/components/LogoDesktop.tsx index 02565f56..519cd5ef 100644 --- a/src/components/Header/components/LogoDesktop.tsx +++ b/src/components/Header/components/LogoDesktop.tsx @@ -1,7 +1,7 @@ import React from "react"; import { styled } from "@mui/material"; import { Link } from "react-router-dom"; -import { headerData } from "../../../config/globalHeaderData"; +import { headerData } from "../../../config/HeaderConfig"; const LogoArea = styled("div")({ display: "flex", diff --git a/src/components/Header/components/LogoMobile.tsx b/src/components/Header/components/LogoMobile.tsx index b294811c..f779462c 100644 --- a/src/components/Header/components/LogoMobile.tsx +++ b/src/components/Header/components/LogoMobile.tsx @@ -1,7 +1,7 @@ import React from "react"; import { styled } from "@mui/material"; import { Link } from "react-router-dom"; -import { headerData } from "../../../config/globalHeaderData"; +import { headerData } from "../../../config/HeaderConfig"; const LogoArea = styled("div")({ display: "flex", diff --git a/src/components/Header/components/NavbarDesktop.tsx b/src/components/Header/components/NavbarDesktop.tsx index 6f845db6..adf057f2 100644 --- a/src/components/Header/components/NavbarDesktop.tsx +++ b/src/components/Header/components/NavbarDesktop.tsx @@ -3,7 +3,7 @@ import { NavLink, Link, useNavigate, useLocation } from "react-router-dom"; import { Button, styled } from "@mui/material"; import { useAuthContext } from "../../Contexts/AuthContext"; import GenericAlert from "../../GenericAlert"; -import { navMobileList, navbarSublists } from "../../../config/globalHeaderData"; +import { navMobileList, navbarSublists } from "../../../config/HeaderConfig"; import { GenerateApiTokenRoles } from "../../../config/AuthRoles"; import APITokenDialog from "../../APITokenDialog"; import UploaderToolDialog from "../../UploaderToolDialog"; diff --git a/src/components/Header/components/USABanner.tsx b/src/components/Header/components/USABanner.tsx index f0a364b7..9a1673ba 100644 --- a/src/components/Header/components/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/HeaderConfig"; const BannerArea = styled("div")({ flexDirection: "row", diff --git a/src/config/globalFooterData.tsx b/src/config/FooterConfig.ts similarity index 91% rename from src/config/globalFooterData.tsx rename to src/config/FooterConfig.ts index a9ea7fcf..b3a8e15b 100644 --- a/src/config/globalFooterData.tsx +++ b/src/config/FooterConfig.ts @@ -3,9 +3,9 @@ import twitterIcon from "../assets/footer/Twitter_Logo.svg"; import facebookIcon from "../assets/footer/Facebook_Logo.svg"; import youtubeIcon from "../assets/footer/Youtube_Logo.svg"; import linkedInIcon from "../assets/footer/LinkedIn_Logo.svg"; -// footerLogoImage ideal image size 310x80 px +import { parseReleaseVersion } from "../utils"; -const FooterConfig = { +const FooterConfig: FooterConfiguration = { footerLogoImage: "https://raw.githubusercontent.com/cbiit/datacommons-assets/main/bento/images/icons/png/footerlogo.png", footerLogoAltText: "Footer Logo", @@ -62,6 +62,18 @@ const FooterConfig = { }, ], }, + { + title: "System Info", + items: [ + { + text: "Release Notes", + link: "/release-notes", + }, + { + text: `Current Version: ${parseReleaseVersion()}`, + }, + ], + }, ], followUs_links: [ { diff --git a/src/config/globalHeaderData.tsx b/src/config/HeaderConfig.ts similarity index 100% rename from src/config/globalHeaderData.tsx rename to src/config/HeaderConfig.ts diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index b3e77666..b4abdcc0 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -25,7 +25,7 @@ const Layout: FC = ({ children }) => ( void; }; + +type FooterConfiguration = { + /** + * The image source of the footer logo + * + * @note The ideal image size 310x80 px + */ + footerLogoImage: string; + /** + * The alt text of the footer logo + */ + footerLogoAltText: string; + /** + * The hyperlink of the footer logo + */ + footerLogoHyperlink: string; + /** + * The static text displayed on the footer + */ + footerStaticText: string; + /** + * An array of footer columns + */ + link_sections: FooterColumnSection[]; + /** + * The follow us links + * + * @TODO Refactor this to use {@link FooterLinkItem} + */ + followUs_links: FooterFollowUsLink[]; + /** + * An array of Contact Us links + */ + contact_links: FooterLinkItem[]; + /** + * An array of Anchor Links + */ + global_footer_links: FooterLinkItem[]; +}; + +/** + * Represents a independent column section of the footer + */ +type FooterColumnSection = { + /** + * Section title + */ + title: string; + /** + * The items in the section + */ + items: FooterLinkItem[]; +}; + +type FooterLinkItem = { + /** + * The text to be displayed on the link + */ + text: string; + /** + * The fully-qualified URL of the link or the relative path + * + * @note Omission of this field will render the item as a non-clickable text + */ + link?: string; +}; + +type FooterFollowUsLink = { + img: string; + link: string; + description: string; +}; diff --git a/src/utils/envUtils.test.ts b/src/utils/envUtils.test.ts new file mode 100644 index 00000000..696e73ea --- /dev/null +++ b/src/utils/envUtils.test.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable global-require */ +describe("parseReleaseVersion cases", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.spyOn(console, "error").mockImplementation(); + jest.resetModules(); + + // Reset the environment variables back to their original values + process.env = { ...originalEnv }; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should return the correct version when REACT_APP_FE_VERSION is valid", () => { + process.env.REACT_APP_FE_VERSION = "3.1.0.472"; + const { parseReleaseVersion } = require("./envUtils"); + expect(parseReleaseVersion()).toBe("3.1.0"); + }); + + it.each<[version: string, expected: string]>([ + // Real tags + ["3.1.0", "3.1.0.405"], + ["3.0.0", "3.0.0.402"], + ["2.1.0", "2.1.0.339"], + ["2.0.0", "2.0.0.213"], + ["1.0.1", "1.0.1.180"], + // Future proofing + ["10.0.1", "10.0.1.1293"], + ["24.19.11", "24.19.11.3456"], + ["9999.0.9999", "9999.0.9999.9999"], + ])( + "should correctly parse the release version of %p from the value of %p", + (expected, version) => { + process.env.REACT_APP_FE_VERSION = version; + const { parseReleaseVersion } = require("./envUtils"); + expect(parseReleaseVersion()).toBe(expected); + } + ); + + it("should return N/A when REACT_APP_FE_VERSION is not set", () => { + delete process.env.REACT_APP_FE_VERSION; + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/is not set or is not a string/i) + ); + }); + + it("should return N/A when REACT_APP_FE_VERSION is not a string", () => { + process.env.REACT_APP_FE_VERSION = 0 as unknown as string; // NOTE: Env variables can officially only be strings + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/is not set or is not a string/i) + ); + }); + + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (1/3)", () => { + process.env.REACT_APP_FE_VERSION = "invalid"; + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/is not in the expected format/i) + ); + }); + + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (2/3)", () => { + process.env.REACT_APP_FE_VERSION = "mvp-2.213"; + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/is not in the expected format/i) + ); + }); + + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (3/3)", () => { + process.env.REACT_APP_FE_VERSION = "test-branch.214"; + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/is not in the expected format/i) + ); + }); + + it("should return N/A when unable to get release version from build tag", () => { + process.env.REACT_APP_FE_VERSION = "0.0.0.000"; + + // NOTE: Previous safety checks should prevent this from happening, + // so we're just mocking some improper match behavior here + jest.spyOn(String.prototype, "match").mockReturnValueOnce([undefined, undefined]); + + const { parseReleaseVersion } = require("./envUtils"); + + expect(parseReleaseVersion()).toBe("N/A"); + expect(console.error).toHaveBeenCalledWith( + expect.stringMatching(/unable to get release version from build tag/i) + ); + }); +}); diff --git a/src/utils/envUtils.ts b/src/utils/envUtils.ts new file mode 100644 index 00000000..20de2c1c --- /dev/null +++ b/src/utils/envUtils.ts @@ -0,0 +1,35 @@ +import env from "../env"; + +/** + * Safely parse the release version from the environment variable. + * If it is not set, return an empty string. + * + * @note This utility expects the current tag to be defined as `X.X.X.buildNumber`. + * @see {@link AppEnv.REACT_APP_FE_VERSION} + * @returns The parsed release version or "N/A" if not set. + */ +export const parseReleaseVersion = (): string => { + const { REACT_APP_FE_VERSION } = env || {}; + if (!REACT_APP_FE_VERSION || typeof REACT_APP_FE_VERSION !== "string") { + console.error("parseReleaseVersion: REACT_APP_FE_VERSION is not set or is not a string"); + return "N/A"; + } + + const ReleaseRegex = /^(\d{1,4}\.\d{1,4}\.\d{1,4}).*/; + if (!ReleaseRegex.test(REACT_APP_FE_VERSION)) { + console.error( + `parseReleaseVersion: REACT_APP_FE_VERSION is not in the expected format: ${REACT_APP_FE_VERSION}` + ); + return "N/A"; + } + + const splitVersion = REACT_APP_FE_VERSION.match(ReleaseRegex); + if (!splitVersion || splitVersion.length < 2 || !splitVersion[1]) { + console.error( + `parseReleaseVersion: Unable to get release version from build tag: ${REACT_APP_FE_VERSION}` + ); + return "N/A"; + } + + return splitVersion[1]; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index c8edbf09..ce8ee628 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,3 +12,4 @@ export * from "./tableUtils"; export * from "./statisticUtils"; export * from "./jsonUtils"; export * from "./searchParamUtils"; +export * from "./envUtils"; From e12df6d8255bd110d786fc0005f0d5ee0cf66c5d Mon Sep 17 00:00:00 2001 From: Alec M Date: Mon, 23 Sep 2024 10:46:03 -0400 Subject: [PATCH 04/17] fix: Action missing env context --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd67c350..52b55e05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,8 @@ name: Jest +env: + REACT_APP_FE_VERSION: ${{ vars.REACT_APP_FE_VERSION }} + on: workflow_dispatch: push: From 32a9ef5f8eb89a8eb5b26fa1ae3da52f83da42e1 Mon Sep 17 00:00:00 2001 From: Alec M Date: Mon, 23 Sep 2024 11:04:33 -0400 Subject: [PATCH 05/17] fix: Local footer link causing UI rebuild --- src/components/Footer/FooterDesktop.tsx | 5 +++-- src/components/Footer/FooterMobile.tsx | 5 +++-- src/components/Footer/FooterTablet.tsx | 5 +++-- src/components/Footer/index.test.tsx | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Footer/FooterDesktop.tsx b/src/components/Footer/FooterDesktop.tsx index 84e816e7..e78ccc44 100644 --- a/src/components/Footer/FooterDesktop.tsx +++ b/src/components/Footer/FooterDesktop.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef } from "react"; import { styled } from "@mui/material"; +import { Link } from "react-router-dom"; import FooterData from "../../config/FooterConfig"; const FooterStyled = styled("div")({ @@ -248,9 +249,9 @@ const FooterDesktop = () => { {item.text} ) : ( - + {item.text} - + )}
); diff --git a/src/components/Footer/FooterMobile.tsx b/src/components/Footer/FooterMobile.tsx index 6e1aab34..99b0a379 100644 --- a/src/components/Footer/FooterMobile.tsx +++ b/src/components/Footer/FooterMobile.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef } from "react"; import { styled } from "@mui/material"; +import { Link } from "react-router-dom"; import FooterData from "../../config/FooterConfig"; const FooterStyled = styled("footer")({ @@ -320,9 +321,9 @@ const FooterMobile = () => { {item.text} ) : ( - + {item.text} - + ); })}
diff --git a/src/components/Footer/FooterTablet.tsx b/src/components/Footer/FooterTablet.tsx index 1b2a2fb8..9cb41bde 100644 --- a/src/components/Footer/FooterTablet.tsx +++ b/src/components/Footer/FooterTablet.tsx @@ -1,5 +1,6 @@ import React, { useState, useRef } from "react"; import { styled } from "@mui/material"; +import { Link } from "react-router-dom"; import FooterData from "../../config/FooterConfig"; const FooterStyled = styled("footer")({ @@ -258,9 +259,9 @@ const FooterTablet = () => { {item.text} ) : ( - + {item.text} - + )}
); diff --git a/src/components/Footer/index.test.tsx b/src/components/Footer/index.test.tsx index 2d73f562..1a830c42 100644 --- a/src/components/Footer/index.test.tsx +++ b/src/components/Footer/index.test.tsx @@ -1,5 +1,6 @@ import { axe } from "jest-axe"; import { render } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; import Footer from "./index"; describe("Footer", () => { @@ -12,7 +13,7 @@ describe("Footer", () => { }); it("should not have any accessibility violations", async () => { - const { container } = render(
); + const { container } = render(
, { wrapper: (p) => }); const results = await axe(container); expect(results).toHaveNoViolations(); From bf0968f9d8ef3f6341e6a9f3ee0eb0c9312c3d8f Mon Sep 17 00:00:00 2001 From: Alec M Date: Mon, 23 Sep 2024 14:36:18 -0400 Subject: [PATCH 06/17] feat: Implement Logger utility --- .github/workflows/test.yml | 3 -- src/hooks/useBuildReduxStore.ts | 9 ++++- src/types/AppEnv.d.ts | 4 ++ src/utils/envUtils.test.ts | 70 ++++++++++++++++++++++++--------- src/utils/envUtils.ts | 36 +++++++++++++++-- src/utils/index.ts | 1 + src/utils/logger.test.ts | 65 ++++++++++++++++++++++++++++++ src/utils/logger.ts | 49 +++++++++++++++++++++++ 8 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 src/utils/logger.test.ts create mode 100644 src/utils/logger.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52b55e05..dd67c350 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,5 @@ name: Jest -env: - REACT_APP_FE_VERSION: ${{ vars.REACT_APP_FE_VERSION }} - on: workflow_dispatch: push: diff --git a/src/hooks/useBuildReduxStore.ts b/src/hooks/useBuildReduxStore.ts index e1bf3feb..adeed94d 100644 --- a/src/hooks/useBuildReduxStore.ts +++ b/src/hooks/useBuildReduxStore.ts @@ -9,7 +9,12 @@ import { import ReduxThunk from "redux-thunk"; import { createLogger } from "redux-logger"; import { baseConfiguration, defaultReadMeTitle, graphViewConfig } from "../config/ModelNavigator"; -import { buildAssetUrls, buildBaseFilterContainers, buildFilterOptionsList } from "../utils"; +import { + buildAssetUrls, + buildBaseFilterContainers, + buildFilterOptionsList, + Logger, +} from "../utils"; export type Status = "waiting" | "loading" | "error" | "success"; @@ -74,7 +79,7 @@ const useBuildReduxStore = (): [ const assets = buildAssetUrls(datacommon); const response = await getModelExploreData(assets.model, assets.props)?.catch((e) => { - console.error(e); + Logger.error(e); return null; }); if (!response?.data || !response?.version) { diff --git a/src/types/AppEnv.d.ts b/src/types/AppEnv.d.ts index 92b89141..7187c19c 100644 --- a/src/types/AppEnv.d.ts +++ b/src/types/AppEnv.d.ts @@ -60,4 +60,8 @@ type AppEnv = { * @example "mvp-2.213" */ REACT_APP_FE_VERSION: string; + /** + * The deployment environment the app is running in + */ + NODE_ENV?: "test" | "development" | "production"; }; diff --git a/src/utils/envUtils.test.ts b/src/utils/envUtils.test.ts index 696e73ea..a374ffe8 100644 --- a/src/utils/envUtils.test.ts +++ b/src/utils/envUtils.test.ts @@ -4,7 +4,6 @@ describe("parseReleaseVersion cases", () => { const originalEnv = process.env; beforeEach(() => { - jest.spyOn(console, "error").mockImplementation(); jest.resetModules(); // Reset the environment variables back to their original values @@ -46,9 +45,6 @@ describe("parseReleaseVersion cases", () => { const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/is not set or is not a string/i) - ); }); it("should return N/A when REACT_APP_FE_VERSION is not a string", () => { @@ -56,9 +52,6 @@ describe("parseReleaseVersion cases", () => { const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/is not set or is not a string/i) - ); }); it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (1/3)", () => { @@ -66,9 +59,6 @@ describe("parseReleaseVersion cases", () => { const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/is not in the expected format/i) - ); }); it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (2/3)", () => { @@ -76,9 +66,6 @@ describe("parseReleaseVersion cases", () => { const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/is not in the expected format/i) - ); }); it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (3/3)", () => { @@ -86,23 +73,68 @@ describe("parseReleaseVersion cases", () => { const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/is not in the expected format/i) - ); }); it("should return N/A when unable to get release version from build tag", () => { process.env.REACT_APP_FE_VERSION = "0.0.0.000"; // NOTE: Previous safety checks should prevent this from happening, - // so we're just mocking some improper match behavior here + // so we're just mocking some improper `match` behavior here jest.spyOn(String.prototype, "match").mockReturnValueOnce([undefined, undefined]); const { parseReleaseVersion } = require("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); - expect(console.error).toHaveBeenCalledWith( - expect.stringMatching(/unable to get release version from build tag/i) + }); +}); + +describe("buildReleaseNotesUrl cases", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + + // Reset the environment variables back to their original values + process.env = { ...originalEnv }; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should return the correct URL when REACT_APP_FE_VERSION is valid", () => { + process.env.REACT_APP_FE_VERSION = "3.1.0.472"; + const { buildReleaseNotesUrl } = require("./envUtils"); + + expect(buildReleaseNotesUrl()).toBe( + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/3.1.0.472/README.md" + ); + }); + + it("should return the fallback URL when REACT_APP_FE_VERSION is not set", () => { + delete process.env.REACT_APP_FE_VERSION; + const { buildReleaseNotesUrl } = require("./envUtils"); + + expect(buildReleaseNotesUrl()).toBe( + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" + ); + }); + + it("should return the fallback URL when REACT_APP_FE_VERSION is not a string", () => { + process.env.REACT_APP_FE_VERSION = 0 as unknown as string; // NOTE: Env variables can officially only be strings + const { buildReleaseNotesUrl } = require("./envUtils"); + + expect(buildReleaseNotesUrl()).toBe( + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" + ); + }); + + it("should return the fallback URL when REACT_APP_FE_VERSION is not in the expected format", () => { + process.env.REACT_APP_FE_VERSION = "invalid"; + const { buildReleaseNotesUrl } = require("./envUtils"); + + expect(buildReleaseNotesUrl()).toBe( + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" ); }); }); diff --git a/src/utils/envUtils.ts b/src/utils/envUtils.ts index 20de2c1c..784f7fe5 100644 --- a/src/utils/envUtils.ts +++ b/src/utils/envUtils.ts @@ -1,4 +1,12 @@ import env from "../env"; +import { Logger } from "./logger"; + +/** + * A regular expression to match the release version from the build tag. + * + * @see {@link parseReleaseVersion} for a usage example. + */ +export const ReleaseRegex = /^(\d{1,4}\.\d{1,4}\.\d{1,4}).*/; /** * Safely parse the release version from the environment variable. @@ -10,14 +18,14 @@ import env from "../env"; */ export const parseReleaseVersion = (): string => { const { REACT_APP_FE_VERSION } = env || {}; + if (!REACT_APP_FE_VERSION || typeof REACT_APP_FE_VERSION !== "string") { - console.error("parseReleaseVersion: REACT_APP_FE_VERSION is not set or is not a string"); + Logger.error("parseReleaseVersion: REACT_APP_FE_VERSION is not set or is not a string"); return "N/A"; } - const ReleaseRegex = /^(\d{1,4}\.\d{1,4}\.\d{1,4}).*/; if (!ReleaseRegex.test(REACT_APP_FE_VERSION)) { - console.error( + Logger.error( `parseReleaseVersion: REACT_APP_FE_VERSION is not in the expected format: ${REACT_APP_FE_VERSION}` ); return "N/A"; @@ -25,7 +33,7 @@ export const parseReleaseVersion = (): string => { const splitVersion = REACT_APP_FE_VERSION.match(ReleaseRegex); if (!splitVersion || splitVersion.length < 2 || !splitVersion[1]) { - console.error( + Logger.error( `parseReleaseVersion: Unable to get release version from build tag: ${REACT_APP_FE_VERSION}` ); return "N/A"; @@ -33,3 +41,23 @@ export const parseReleaseVersion = (): string => { return splitVersion[1]; }; + +/** + * A utility to build the Release Notes Markdown URL based on the current build tag. + * + * @note If the build tag is not set or is not in the expected format, it will default to the main branch instead of a tag. + * @returns The URL to the Release Notes Markdown file. + */ +export const buildReleaseNotesUrl = (): string => { + const { REACT_APP_FE_VERSION } = env || {}; + + if ( + REACT_APP_FE_VERSION && + typeof REACT_APP_FE_VERSION === "string" && + ReleaseRegex.test(REACT_APP_FE_VERSION) + ) { + return `https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/${REACT_APP_FE_VERSION}/README.md`; // TODO: Change to the release notes file + } + + return "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md"; // TODO: Change to the release notes file +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index ce8ee628..c09d2e2a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,4 @@ export * from "./statisticUtils"; export * from "./jsonUtils"; export * from "./searchParamUtils"; export * from "./envUtils"; +export * from "./logger"; diff --git a/src/utils/logger.test.ts b/src/utils/logger.test.ts new file mode 100644 index 00000000..bdd96baf --- /dev/null +++ b/src/utils/logger.test.ts @@ -0,0 +1,65 @@ +import { Logger } from "./logger"; +import env from "../env"; + +describe("Logger", () => { + const originalEnv = process.env; + + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + // Reset the environment variables back to their original values + process.env = { ...originalEnv }; + + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should log an error message with the correct format", () => { + env.NODE_ENV = "development"; // Override 'test' to log the message + + Logger.error("Test error message"); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringMatching( + /\[ERROR\] \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] Test error message/ + ) + ); + }); + + it("should support logging additional parameters", () => { + env.NODE_ENV = "development"; // Override 'test' to log the message + + Logger.error("Test error message", "Additional parameter"); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringMatching( + /\[ERROR\] \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] Test error message/ + ), + "Additional parameter" + ); + }); + + it("should support logging Error objects", () => { + env.NODE_ENV = "development"; // Override 'test' to log the message + + Logger.error("Test error message", new Error("Test error")); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringMatching( + /\[ERROR\] \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] Test error message/ + ), + expect.any(Error) + ); + }); + + it('should not log anything if NODE_ENV is "test"', () => { + env.NODE_ENV = "test"; // This is the default value, but explicitly setting it here just in case + + Logger.error("Test error message"); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 00000000..61dc03ab --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,49 @@ +import env from "../env"; + +/** + * Represents the different log levels that can be used. + * + * @note Currently only supports "error" levels. + */ +export type LogLevel = "error"; + +/** + * A simple wrapper for the `console.log` function. + * + * @note Do not use this function directly. Use the `Logger` object instead. + * @param level The log level to use. + * @param message The message to log to the console. + * @param optionalParams Any additional parameters to log with the message. + * @returns void + */ +const LoggingWrapper = (level: LogLevel, message: string, ...optionalParams: unknown[]): void => { + // Skip logging in a test environment. + if (env?.NODE_ENV === "test") { + return; + } + + const timestamp = new Date().toISOString(); + switch (level) { + case "error": + console.error(`[ERROR] [${timestamp}] ${message}`, ...optionalParams); + break; + } +}; + +/** + * Represents the definition of a logger function. + */ +export type LoggerFunction = (message: string, ...optionalParams: unknown[]) => void; + +/** + * Provides a simple logging interface for the application. + */ +export const Logger: Readonly> = { + /** + * A simple error logging function. + * + * @see {@link LoggingWrapper} for more information. + */ + error: (message: string, ...optionalParams: unknown[]) => + LoggingWrapper("error", message, ...optionalParams), +}; From 9ef070d0f1e832ce056007dce3a0bdebca6576c4 Mon Sep 17 00:00:00 2001 From: Alec M Date: Mon, 23 Sep 2024 17:42:48 -0400 Subject: [PATCH 07/17] CRDCDH-1612 init Release Notes page --- package-lock.json | 1757 ++++++++++++++---- package.json | 1 + src/content/ReleaseNotes/Controller.test.tsx | 27 + src/content/ReleaseNotes/Controller.tsx | 54 + src/content/ReleaseNotes/NotesView.tsx | 34 + src/router.tsx | 13 +- src/utils/fetchUtils.test.tsx | 92 + src/utils/fetchUtils.ts | 32 + src/utils/index.ts | 1 + 9 files changed, 1662 insertions(+), 349 deletions(-) create mode 100644 src/content/ReleaseNotes/Controller.test.tsx create mode 100644 src/content/ReleaseNotes/Controller.tsx create mode 100644 src/content/ReleaseNotes/NotesView.tsx create mode 100644 src/utils/fetchUtils.test.tsx create mode 100644 src/utils/fetchUtils.ts diff --git a/package-lock.json b/package-lock.json index 7cc6d415..48049471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "react-ga4": "^2.1.0", "react-helmet-async": "^1.3.0", "react-hook-form": "^7.45.4", + "react-markdown": "^9.0.1", "react-multi-carousel": "^2.8.4", "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", @@ -5860,8 +5861,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.9", - "license": "MIT", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dependencies": { "@types/ms": "*" } @@ -5887,6 +5889,14 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "4.17.17", "license": "MIT", @@ -5919,10 +5929,11 @@ } }, "node_modules/@types/hast": { - "version": "2.3.6", - "license": "MIT", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "dependencies": { - "@types/unist": "^2" + "@types/unist": "*" } }, "node_modules/@types/hoist-non-react-statics": { @@ -6177,10 +6188,11 @@ "license": "MIT" }, "node_modules/@types/mdast": { - "version": "3.0.13", - "license": "MIT", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": { - "@types/unist": "^2" + "@types/unist": "*" } }, "node_modules/@types/mime": { @@ -6188,8 +6200,9 @@ "license": "MIT" }, "node_modules/@types/ms": { - "version": "0.7.32", - "license": "MIT" + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { "version": "20.4.0", @@ -6384,8 +6397,9 @@ "license": "MIT" }, "node_modules/@types/unist": { - "version": "2.0.8", - "license": "MIT" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/@types/ws": { "version": "8.5.4", @@ -6594,6 +6608,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "license": "MIT", @@ -7486,7 +7505,8 @@ }, "node_modules/bail": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7852,6 +7872,15 @@ "node": ">=4" } }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "3.0.0", "license": "MIT", @@ -7870,6 +7899,42 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-types": { "version": "11.2.2", "license": "MIT" @@ -8105,7 +8170,8 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8864,6 +8930,27 @@ "underscore": "^1.13.3" } }, + "node_modules/data-model-navigator/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/data-model-navigator/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/data-model-navigator/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, "node_modules/data-model-navigator/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -8872,6 +8959,31 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/data-model-navigator/node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, + "node_modules/data-model-navigator/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/data-model-navigator/node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -8884,71 +8996,715 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/data-model-navigator/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "node_modules/data-model-navigator/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "dependencies": { - "glob": "^7.1.3" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, - "bin": { - "rimraf": "bin.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/data-urls": { - "version": "2.0.0", - "license": "MIT", + "node_modules/data-model-navigator/node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/dayjs": { - "version": "1.11.8", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", + "node_modules/data-model-navigator/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" + "@types/mdast": "^3.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "license": "MIT" + "node_modules/data-model-navigator/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + "node_modules/data-model-navigator/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "license": "MIT", + "node_modules/data-model-navigator/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/decode-named-character-reference/node_modules/character-entities": { - "version": "2.0.2", - "license": "MIT", + "node_modules/data-model-navigator/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/data-model-navigator/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/data-model-navigator/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/data-model-navigator/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/data-model-navigator/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/data-model-navigator/node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/data-model-navigator/node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/data-model-navigator/node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, + "node_modules/data-model-navigator/node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-model-navigator/node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/data-urls": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/dayjs": { + "version": "1.11.8", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -9083,6 +9839,18 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dfa": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", @@ -9093,8 +9861,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "5.1.0", - "license": "BSD-3-Clause", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "engines": { "node": ">=0.3.1" } @@ -10205,6 +10974,15 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "1.0.1", "license": "MIT" @@ -10331,7 +11109,8 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -11178,9 +11957,39 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "license": "MIT", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -11292,6 +12101,15 @@ "node": ">=12" } }, + "node_modules/html-url-attributes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", + "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/html-webpack-plugin": { "version": "5.5.1", "license": "MIT", @@ -11595,8 +12413,9 @@ "license": "ISC" }, "node_modules/inline-style-parser": { - "version": "0.1.1", - "license": "MIT" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -11628,6 +12447,28 @@ "node": ">= 10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "license": "MIT", @@ -11692,6 +12533,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "license": "MIT", @@ -11725,6 +12588,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "license": "MIT", @@ -11794,6 +12666,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-in-browser": { "version": "1.1.3", "license": "MIT" @@ -13686,6 +14567,15 @@ "version": "4.5.0", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -13763,7 +14653,8 @@ }, "node_modules/mdast-util-definitions": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", @@ -13774,40 +14665,184 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "license": "MIT", + "node_modules/mdast-util-definitions/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mdast-util-definitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", "dependencies": { - "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "license": "MIT", + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", + "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", @@ -13815,10 +14850,11 @@ } }, "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "license": "MIT", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "@types/mdast": "^3.0.0" + "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", @@ -13874,7 +14910,9 @@ } }, "node_modules/micromark": { - "version": "3.2.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13885,29 +14923,30 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-core-commonmark": { - "version": "1.1.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", "funding": [ { "type": "GitHub Sponsors", @@ -13918,28 +14957,29 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-destination": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", "funding": [ { "type": "GitHub Sponsors", @@ -13950,15 +14990,16 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-label": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", "funding": [ { "type": "GitHub Sponsors", @@ -13969,16 +15010,17 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-space": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", "funding": [ { "type": "GitHub Sponsors", @@ -13989,14 +15031,15 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-title": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", "funding": [ { "type": "GitHub Sponsors", @@ -14007,16 +15050,17 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", "funding": [ { "type": "GitHub Sponsors", @@ -14027,16 +15071,17 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-character": { - "version": "1.2.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", "funding": [ { "type": "GitHub Sponsors", @@ -14047,14 +15092,15 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-chunked": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", "funding": [ { "type": "GitHub Sponsors", @@ -14065,13 +15111,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", "funding": [ { "type": "GitHub Sponsors", @@ -14082,15 +15129,16 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", "funding": [ { "type": "GitHub Sponsors", @@ -14101,14 +15149,15 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", "funding": [ { "type": "GitHub Sponsors", @@ -14119,13 +15168,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", "funding": [ { "type": "GitHub Sponsors", @@ -14136,16 +15186,17 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-encode": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", "funding": [ { "type": "GitHub Sponsors", @@ -14155,11 +15206,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", "funding": [ { "type": "GitHub Sponsors", @@ -14169,11 +15221,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", "funding": [ { "type": "GitHub Sponsors", @@ -14184,13 +15237,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-symbol": "^1.0.0" + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", "funding": [ { "type": "GitHub Sponsors", @@ -14201,13 +15255,14 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-types": "^1.0.0" + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", "funding": [ { "type": "GitHub Sponsors", @@ -14218,15 +15273,16 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -14237,16 +15293,17 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-symbol": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", "funding": [ { "type": "GitHub Sponsors", @@ -14256,11 +15313,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-types": { - "version": "1.1.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", "funding": [ { "type": "GitHub Sponsors", @@ -14270,8 +15328,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromatch": { "version": "4.0.5", @@ -14411,7 +15468,8 @@ }, "node_modules/mri": { "version": "1.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "engines": { "node": ">=4" } @@ -14889,6 +15947,30 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, "node_modules/parse-json": { "version": "5.2.0", "license": "MIT", @@ -16333,8 +17415,9 @@ "license": "MIT" }, "node_modules/property-information": { - "version": "6.3.0", - "license": "MIT", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -16608,32 +17691,28 @@ "license": "MIT" }, "node_modules/react-markdown": { - "version": "8.0.7", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/prop-types": "^15.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "prop-types": "^15.0.0", - "property-information": "^6.0.0", - "react-is": "^18.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "@types/react": ">=18", + "react": ">=18" } }, "node_modules/react-multi-carousel": { @@ -17055,12 +18134,14 @@ } }, "node_modules/remark-parse": { - "version": "10.0.2", - "license": "MIT", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -17068,13 +18149,15 @@ } }, "node_modules/remark-rehype": { - "version": "10.1.0", - "license": "MIT", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", @@ -17329,7 +18412,8 @@ }, "node_modules/sade": { "version": "1.8.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dependencies": { "mri": "^1.1.0" }, @@ -17806,7 +18890,8 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -17984,6 +19069,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "license": "BSD-2-Clause", @@ -18068,10 +19166,11 @@ } }, "node_modules/style-to-object": { - "version": "0.4.2", - "license": "MIT", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", "dependencies": { - "inline-style-parser": "0.1.1" + "inline-style-parser": "0.2.4" } }, "node_modules/stylehacks": { @@ -18699,15 +19798,17 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/trough": { - "version": "2.1.0", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -18967,46 +20068,27 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" }, "node_modules/unified": { - "version": "10.1.2", - "license": "MIT", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "dependencies": { - "@types/unist": "^2.0.0", + "@types/unist": "^3.0.0", "bail": "^2.0.0", + "devlop": "^1.0.0", "extend": "^3.0.0", - "is-buffer": "^2.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", - "vfile": "^5.0.0" + "vfile": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unified/node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/unified/node_modules/is-plain-obj": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "engines": { "node": ">=12" }, @@ -19026,75 +20108,70 @@ }, "node_modules/unist-util-generated": { "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "license": "MIT", + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit": { - "version": "4.1.2", - "license": "MIT", + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "license": "MIT", + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" + "@types/unist": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { - "version": "5.2.1", - "license": "MIT", + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/unist-util-visit/node_modules/unist-util-is": { - "version": "5.2.1", - "license": "MIT", + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", "dependencies": { - "@types/unist": "^2.0.0" + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" }, "funding": { "type": "opencollective", @@ -19221,7 +20298,8 @@ }, "node_modules/uvu": { "version": "0.5.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", "dependencies": { "dequal": "^2.0.0", "diff": "^5.0.0", @@ -19237,7 +20315,8 @@ }, "node_modules/uvu/node_modules/kleur": { "version": "4.1.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "engines": { "node": ">=6" } @@ -19269,13 +20348,12 @@ } }, "node_modules/vfile": { - "version": "5.3.7", - "license": "MIT", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", @@ -19283,38 +20361,18 @@ } }, "node_modules/vfile-message": { - "version": "3.1.4", - "license": "MIT", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/vfile/node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/victory-vendor": { "version": "36.7.0", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", @@ -20182,6 +21240,15 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 139d8366..03f126fd 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-ga4": "^2.1.0", "react-helmet-async": "^1.3.0", "react-hook-form": "^7.45.4", + "react-markdown": "^9.0.1", "react-multi-carousel": "^2.8.4", "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", diff --git a/src/content/ReleaseNotes/Controller.test.tsx b/src/content/ReleaseNotes/Controller.test.tsx new file mode 100644 index 00000000..601aefb5 --- /dev/null +++ b/src/content/ReleaseNotes/Controller.test.tsx @@ -0,0 +1,27 @@ +const mockFetchReleaseNotes = jest.fn(); +jest.mock("../../utils", () => ({ + ...jest.requireActual("../../utils"), + fetchReleaseNotes: (...p) => mockFetchReleaseNotes(...p), +})); + +// Note: We're mocking the NotesView because Jest doesn't support the `react-markdown` package. +// This is a workaround to prevent the test suite from failing. +jest.mock("./NotesView", () => jest.fn(() =>

Notes

)); + +describe("Controller", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.todo("should render the loader when fetching release notes"); + + it.todo("should render the notes when release notes are fetched"); + + it.todo("should render an error message when release notes fail to fetch"); + + it.todo("should navigate to the home page when release notes fail to fetch"); + + it.todo("should fetch release notes on mount"); + + it.todo("should not fetch release notes if they are already loaded (double render)"); +}); diff --git a/src/content/ReleaseNotes/Controller.tsx b/src/content/ReleaseNotes/Controller.tsx new file mode 100644 index 00000000..830d07ba --- /dev/null +++ b/src/content/ReleaseNotes/Controller.tsx @@ -0,0 +1,54 @@ +import { memo, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useSnackbar } from "notistack"; +import usePageTitle from "../../hooks/usePageTitle"; +import SuspenseLoader from "../../components/SuspenseLoader"; +import NotesView from "./NotesView"; +import { Logger, fetchReleaseNotes } from "../../utils"; + +/** + * Controller component for the Release Notes page. + * + * @returns {React.ReactNode} The Release Notes page. + */ +const ReleaseNotesController = (): React.ReactNode => { + usePageTitle("Release Notes"); + + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + const [document, setDocument] = useState(null); + const isFetchingRef = useRef(false); + + useEffect(() => { + if (document && document.length > 0) { + return; + } + + if (isFetchingRef.current) { + return; + } + + (async () => { + isFetchingRef.current = true; + const result = await fetchReleaseNotes(); + isFetchingRef.current = false; + + if (result instanceof Error) { + Logger.error("ReleaseNotesController:", result); + enqueueSnackbar("Unable to load release notes.", { variant: "error" }); + navigate("/"); + return; + } + + setDocument(result); + })(); + }, []); + + if (!document) { + return ; + } + + return ; +}; + +export default memo(ReleaseNotesController); diff --git a/src/content/ReleaseNotes/NotesView.tsx b/src/content/ReleaseNotes/NotesView.tsx new file mode 100644 index 00000000..9bd7036e --- /dev/null +++ b/src/content/ReleaseNotes/NotesView.tsx @@ -0,0 +1,34 @@ +import { Box, styled } from "@mui/material"; +import { memo } from "react"; +import Markdown from "react-markdown"; + +const StyledFrameContainer = styled(Box)({ + borderRadius: "6px", + border: "1px solid #E0E0E0", + background: "#fff", + position: "relative", + padding: "0 12px", + margin: "30px auto", + maxWidth: "calc(100% - 64px)", +}); + +type NotesViewProps = { + /** + * A valid markdown string to render as the Release Notes. + */ + md: string | null; +}; + +/** + * The view for the ReleaseNotes component. + * + * @param {NotesViewProps} props The props for the component. + * @returns {JSX.Element} The ReleaseNotes component. + */ +const NotesView = ({ md }: NotesViewProps): JSX.Element => ( + + {md} + +); + +export default memo(NotesView, (prev, next) => prev.md === next.md); diff --git a/src/router.tsx b/src/router.tsx index 1c165b52..53ca303a 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -15,6 +15,7 @@ const Questionnaire = LazyLoader(lazy(() => import("./content/questionnaire/Cont const DataSubmissions = LazyLoader(lazy(() => import("./content/dataSubmissions/Controller"))); const Users = LazyLoader(lazy(() => import("./content/users/Controller"))); const DMN = LazyLoader(lazy(() => import("./content/modelNavigator/Controller"))); +const ReleaseNotes = LazyLoader(lazy(() => import("./content/ReleaseNotes/Controller"))); const Organizations = LazyLoader(lazy(() => import("./content/organizations/Controller"))); const Status404 = LazyLoader(lazy(() => import("./content/status/Page404"))); const OperationDashboard = LazyLoader( @@ -34,6 +35,14 @@ const routes: RouteObject[] = [ path: "/login", element: , }, + { + path: "/model-navigator/:dataCommon", + element: , + }, + { + path: "/release-notes", + element: , + }, { path: "/submissions", element: ( @@ -94,10 +103,6 @@ const routes: RouteObject[] = [ /> ), }, - { - path: "/model-navigator/:dataCommon", - element: , - }, { path: "/organizations/:orgId?", element: ( diff --git a/src/utils/fetchUtils.test.tsx b/src/utils/fetchUtils.test.tsx new file mode 100644 index 00000000..a73aa128 --- /dev/null +++ b/src/utils/fetchUtils.test.tsx @@ -0,0 +1,92 @@ +import * as utils from "./fetchUtils"; + +describe("fetchReleaseNotes", () => { + const mockReleaseNotes = "# Release Notes\n\n- Feature 1\n- Feature 2"; + + beforeEach(() => { + sessionStorage.clear(); + jest.clearAllMocks(); + }); + + it("should successfully handle fetching of release notes", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + text: jest.fn().mockResolvedValue(mockReleaseNotes), + }); + + expect(await utils.fetchReleaseNotes()).toBe(mockReleaseNotes); + }); + + it("should cache successful responses in sessionStorage", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + text: jest.fn().mockResolvedValue(mockReleaseNotes), + }); + + expect(sessionStorage.getItem("releaseNotes")).toBeNull(); + + await utils.fetchReleaseNotes(); + + expect(sessionStorage.getItem("releaseNotes")).toBe(mockReleaseNotes); + }); + + it("should return cached release notes from sessionStorage", async () => { + const fetchSpy = jest.spyOn(global, "fetch"); + + sessionStorage.setItem("releaseNotes", mockReleaseNotes); + + const result = await utils.fetchReleaseNotes(); + + expect(result).toBe(mockReleaseNotes); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("should handle fetch errors", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("Network error")); + + const result = await utils.fetchReleaseNotes(); + + expect(result).toBeInstanceOf(Error); + expect((result as Error).message).toBe("Network error"); + }); + + it("should handle non-200 HTTP responses", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 404, + }); + + const result = await utils.fetchReleaseNotes(); + + expect(result).toBeInstanceOf(Error); + expect((result as Error).message).toBe("Failed to fetch release notes: 404"); + }); + + it("should handle an empty release notes document", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + text: jest.fn().mockResolvedValue(""), + }); + + const result = await utils.fetchReleaseNotes(); + + expect(result).toBeInstanceOf(Error); + expect((result as Error).message).toBe("Release notes document is empty."); + }); + + it("should handle an error thrown while retrieving the response text", async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + status: 200, + text: jest.fn().mockRejectedValue(new Error("some mock text error")), + }); + + const result = await utils.fetchReleaseNotes(); + + expect(result).toBeInstanceOf(Error); + expect((result as Error).message).toBe("Release notes document is empty."); + }); +}); diff --git a/src/utils/fetchUtils.ts b/src/utils/fetchUtils.ts new file mode 100644 index 00000000..0ae3cb39 --- /dev/null +++ b/src/utils/fetchUtils.ts @@ -0,0 +1,32 @@ +import { buildReleaseNotesUrl } from "./envUtils"; + +/** + * A utility function to fetch the release notes document from GitHub. + * + * @see Utilizes {@link buildReleaseNotesUrl} to build the URL to fetch the release notes document. + * @note Handles caching the release notes document in {@link sessionStorage}. + * @param signal An optional AbortSignal to cancel the fetch request + * @returns The release notes document as a string or an Error object + */ +export const fetchReleaseNotes = async (signal?: AbortSignal): Promise => { + if (sessionStorage.getItem("releaseNotes")) { + return sessionStorage.getItem("releaseNotes"); + } + + const url: string = buildReleaseNotesUrl(); + const response = await fetch(url, { method: "GET", signal }).catch((err: Error) => err); + if (response instanceof Error) { + return response; + } + if (!response.ok || response.status !== 200) { + return new Error(`Failed to fetch release notes: ${response.status}`); + } + + const md = await response.text().catch(() => null); + if (!md || md.length === 0) { + return new Error("Release notes document is empty."); + } + + sessionStorage.setItem("releaseNotes", md); + return md; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index c09d2e2a..740ade86 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,3 +14,4 @@ export * from "./jsonUtils"; export * from "./searchParamUtils"; export * from "./envUtils"; export * from "./logger"; +export * from "./fetchUtils"; From 2c025fed6c9e2e2d6f4712f4fcd94a0c6439c3a0 Mon Sep 17 00:00:00 2001 From: Alec M Date: Tue, 24 Sep 2024 09:11:26 -0400 Subject: [PATCH 08/17] Prevent direct console.error --- .eslintrc.cjs | 8 ++++---- src/utils/logger.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 8950f1f5..1fc8a1c9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -39,13 +39,13 @@ const config = { "prettier/prettier": "error", "max-len": "off", - "no-console": ["warn", { allow: ["error"] }], "no-param-reassign": "off", "object-curly-newline": "off", "no-underscore-dangle": ["off"], - "arrow-body-style": ["warn"], - "eol-last": ["warn"], - "no-unreachable": ["warn"], + "no-console": "warn", + "arrow-body-style": "warn", + "eol-last": "warn", + "no-unreachable": "warn", /* typescript-eslint overwritten rules */ "no-use-before-define": "off", diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 61dc03ab..92f9f33f 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -25,6 +25,7 @@ const LoggingWrapper = (level: LogLevel, message: string, ...optionalParams: unk const timestamp = new Date().toISOString(); switch (level) { case "error": + // eslint-disable-next-line no-console console.error(`[ERROR] [${timestamp}] ${message}`, ...optionalParams); break; } From ce13e54fc6111cd1f558c0c59e92e7d9c7bf0d9e Mon Sep 17 00:00:00 2001 From: Alec M Date: Tue, 24 Sep 2024 09:32:57 -0400 Subject: [PATCH 09/17] revert: Unnecessary console spy --- src/components/Footer/index.test.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/Footer/index.test.tsx b/src/components/Footer/index.test.tsx index 1a830c42..5baa75c1 100644 --- a/src/components/Footer/index.test.tsx +++ b/src/components/Footer/index.test.tsx @@ -4,14 +4,6 @@ import { BrowserRouter } from "react-router-dom"; import Footer from "./index"; describe("Footer", () => { - beforeEach(() => { - jest.spyOn(console, "error").mockImplementation(); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - it("should not have any accessibility violations", async () => { const { container } = render(
, { wrapper: (p) => }); const results = await axe(container); From a07728255d438a31e0ae757bba56ef0e324a7fb3 Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 26 Sep 2024 13:53:30 -0400 Subject: [PATCH 10/17] fix: Using require instead of import and update document URL --- src/utils/envUtils.test.ts | 58 ++++++++++++++++++-------------------- src/utils/envUtils.ts | 4 +-- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/utils/envUtils.test.ts b/src/utils/envUtils.test.ts index a374ffe8..1c586eed 100644 --- a/src/utils/envUtils.test.ts +++ b/src/utils/envUtils.test.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable global-require */ describe("parseReleaseVersion cases", () => { const originalEnv = process.env; @@ -14,9 +12,9 @@ describe("parseReleaseVersion cases", () => { jest.restoreAllMocks(); }); - it("should return the correct version when REACT_APP_FE_VERSION is valid", () => { + it("should return the correct version when REACT_APP_FE_VERSION is valid", async () => { process.env.REACT_APP_FE_VERSION = "3.1.0.472"; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("3.1.0"); }); @@ -33,56 +31,56 @@ describe("parseReleaseVersion cases", () => { ["9999.0.9999", "9999.0.9999.9999"], ])( "should correctly parse the release version of %p from the value of %p", - (expected, version) => { + async (expected, version) => { process.env.REACT_APP_FE_VERSION = version; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe(expected); } ); - it("should return N/A when REACT_APP_FE_VERSION is not set", () => { + it("should return N/A when REACT_APP_FE_VERSION is not set", async () => { delete process.env.REACT_APP_FE_VERSION; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); - it("should return N/A when REACT_APP_FE_VERSION is not a string", () => { + it("should return N/A when REACT_APP_FE_VERSION is not a string", async () => { process.env.REACT_APP_FE_VERSION = 0 as unknown as string; // NOTE: Env variables can officially only be strings - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); - it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (1/3)", () => { + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (1/3)", async () => { process.env.REACT_APP_FE_VERSION = "invalid"; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); - it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (2/3)", () => { + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (2/3)", async () => { process.env.REACT_APP_FE_VERSION = "mvp-2.213"; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); - it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (3/3)", () => { + it("should return N/A when REACT_APP_FE_VERSION is not in the expected format (3/3)", async () => { process.env.REACT_APP_FE_VERSION = "test-branch.214"; - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); - it("should return N/A when unable to get release version from build tag", () => { + it("should return N/A when unable to get release version from build tag", async () => { process.env.REACT_APP_FE_VERSION = "0.0.0.000"; // NOTE: Previous safety checks should prevent this from happening, // so we're just mocking some improper `match` behavior here jest.spyOn(String.prototype, "match").mockReturnValueOnce([undefined, undefined]); - const { parseReleaseVersion } = require("./envUtils"); + const { parseReleaseVersion } = await import("./envUtils"); expect(parseReleaseVersion()).toBe("N/A"); }); @@ -102,39 +100,39 @@ describe("buildReleaseNotesUrl cases", () => { jest.restoreAllMocks(); }); - it("should return the correct URL when REACT_APP_FE_VERSION is valid", () => { + it("should return the correct URL when REACT_APP_FE_VERSION is valid", async () => { process.env.REACT_APP_FE_VERSION = "3.1.0.472"; - const { buildReleaseNotesUrl } = require("./envUtils"); + const { buildReleaseNotesUrl } = await import("./envUtils"); expect(buildReleaseNotesUrl()).toBe( - "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/3.1.0.472/README.md" + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/3.1.0.472/CHANGELOG.md" ); }); - it("should return the fallback URL when REACT_APP_FE_VERSION is not set", () => { + it("should return the fallback URL when REACT_APP_FE_VERSION is not set", async () => { delete process.env.REACT_APP_FE_VERSION; - const { buildReleaseNotesUrl } = require("./envUtils"); + const { buildReleaseNotesUrl } = await import("./envUtils"); expect(buildReleaseNotesUrl()).toBe( - "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/CHANGELOG.md" ); }); - it("should return the fallback URL when REACT_APP_FE_VERSION is not a string", () => { + it("should return the fallback URL when REACT_APP_FE_VERSION is not a string", async () => { process.env.REACT_APP_FE_VERSION = 0 as unknown as string; // NOTE: Env variables can officially only be strings - const { buildReleaseNotesUrl } = require("./envUtils"); + const { buildReleaseNotesUrl } = await import("./envUtils"); expect(buildReleaseNotesUrl()).toBe( - "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/CHANGELOG.md" ); }); - it("should return the fallback URL when REACT_APP_FE_VERSION is not in the expected format", () => { + it("should return the fallback URL when REACT_APP_FE_VERSION is not in the expected format", async () => { process.env.REACT_APP_FE_VERSION = "invalid"; - const { buildReleaseNotesUrl } = require("./envUtils"); + const { buildReleaseNotesUrl } = await import("./envUtils"); expect(buildReleaseNotesUrl()).toBe( - "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md" + "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/CHANGELOG.md" ); }); }); diff --git a/src/utils/envUtils.ts b/src/utils/envUtils.ts index 784f7fe5..c34cf574 100644 --- a/src/utils/envUtils.ts +++ b/src/utils/envUtils.ts @@ -56,8 +56,8 @@ export const buildReleaseNotesUrl = (): string => { typeof REACT_APP_FE_VERSION === "string" && ReleaseRegex.test(REACT_APP_FE_VERSION) ) { - return `https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/${REACT_APP_FE_VERSION}/README.md`; // TODO: Change to the release notes file + return `https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/tags/${REACT_APP_FE_VERSION}/CHANGELOG.md`; } - return "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/README.md"; // TODO: Change to the release notes file + return "https://raw.githubusercontent.com/CBIIT/crdc-datahub-ui/refs/heads/main/CHANGELOG.md"; }; From 41df5b2127ad2ff9cac466584822be30e096e95e Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 26 Sep 2024 15:18:35 -0400 Subject: [PATCH 11/17] Add basic coverage for NotesView and mock react-markdown globally --- src/content/ReleaseNotes/NotesView.test.tsx | 23 +++++++++++++++++++ src/content/ReleaseNotes/NotesView.tsx | 25 ++++++++++++++------- src/setupTests.tsx | 8 +++++++ 3 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 src/content/ReleaseNotes/NotesView.test.tsx diff --git a/src/content/ReleaseNotes/NotesView.test.tsx b/src/content/ReleaseNotes/NotesView.test.tsx new file mode 100644 index 00000000..fc5e83f0 --- /dev/null +++ b/src/content/ReleaseNotes/NotesView.test.tsx @@ -0,0 +1,23 @@ +import { render } from "@testing-library/react"; +import { axe } from "jest-axe"; +import NotesView from "./NotesView"; + +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 render the markdown content", () => { + const { getByText } = render(); + + expect(getByText("# Test markdown")).toBeInTheDocument(); + }); +}); diff --git a/src/content/ReleaseNotes/NotesView.tsx b/src/content/ReleaseNotes/NotesView.tsx index 9bd7036e..4aa533a1 100644 --- a/src/content/ReleaseNotes/NotesView.tsx +++ b/src/content/ReleaseNotes/NotesView.tsx @@ -1,15 +1,22 @@ -import { Box, styled } from "@mui/material"; +import { Container, styled } from "@mui/material"; import { memo } from "react"; import Markdown from "react-markdown"; -const StyledFrameContainer = styled(Box)({ +const StyledFrameContainer = styled(Container)({ + marginTop: "30px", + marginBottom: "30px", +}); + +const StyledMarkdownBox = styled("div")({ borderRadius: "6px", border: "1px solid #E0E0E0", background: "#fff", + boxShadow: "0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)", position: "relative", - padding: "0 12px", - margin: "30px auto", - maxWidth: "calc(100% - 64px)", + padding: "6px 72px", + "& h1:first-of-type": { + textAlign: "center", + }, }); type NotesViewProps = { @@ -26,9 +33,11 @@ type NotesViewProps = { * @returns {JSX.Element} The ReleaseNotes component. */ const NotesView = ({ md }: NotesViewProps): JSX.Element => ( - - {md} + + + {md} + ); -export default memo(NotesView, (prev, next) => prev.md === next.md); +export default memo(NotesView); diff --git a/src/setupTests.tsx b/src/setupTests.tsx index 3b8faa79..424da242 100644 --- a/src/setupTests.tsx +++ b/src/setupTests.tsx @@ -74,6 +74,14 @@ jest.mock("recharts", () => ({ ResponsiveContainer: MockResponsiveContainer, })); +/** + * Mocks the react-markdown package for testing + * as Jest does not support ESM modules by default + */ +jest.mock("react-markdown", () => ({ children }: { children: string }) => ( +
{children}
+)); + /** * Prevents the console.error and console.warn from silently failing * in tests by throwing an error when called From afb618a7ccd8469e030830155fddd726860bcc1a Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 26 Sep 2024 15:19:32 -0400 Subject: [PATCH 12/17] Rewrite fetchReleaseNotes error handling --- src/content/ReleaseNotes/Controller.tsx | 34 +++++++++++-------------- src/utils/fetchUtils.test.tsx | 28 +++++++------------- src/utils/fetchUtils.ts | 16 +++++------- 3 files changed, 31 insertions(+), 47 deletions(-) diff --git a/src/content/ReleaseNotes/Controller.tsx b/src/content/ReleaseNotes/Controller.tsx index 830d07ba..4f1a71c4 100644 --- a/src/content/ReleaseNotes/Controller.tsx +++ b/src/content/ReleaseNotes/Controller.tsx @@ -19,29 +19,25 @@ const ReleaseNotesController = (): React.ReactNode => { const [document, setDocument] = useState(null); const isFetchingRef = useRef(false); - useEffect(() => { - if (document && document.length > 0) { - return; - } + const fetchNotes = async () => { + isFetchingRef.current = true; - if (isFetchingRef.current) { - return; - } - - (async () => { - isFetchingRef.current = true; + try { const result = await fetchReleaseNotes(); + setDocument(result); + } catch (error) { + Logger.error("ReleaseNotesController:", error); + enqueueSnackbar("Unable to load release notes.", { variant: "error" }); + navigate("/"); + } finally { isFetchingRef.current = false; + } + }; - if (result instanceof Error) { - Logger.error("ReleaseNotesController:", result); - enqueueSnackbar("Unable to load release notes.", { variant: "error" }); - navigate("/"); - return; - } - - setDocument(result); - })(); + useEffect(() => { + if (!isFetchingRef.current) { + fetchNotes(); + } }, []); if (!document) { diff --git a/src/utils/fetchUtils.test.tsx b/src/utils/fetchUtils.test.tsx index a73aa128..4007bec0 100644 --- a/src/utils/fetchUtils.test.tsx +++ b/src/utils/fetchUtils.test.tsx @@ -43,13 +43,10 @@ describe("fetchReleaseNotes", () => { expect(fetchSpy).not.toHaveBeenCalled(); }); - it("should handle fetch errors", async () => { - global.fetch = jest.fn().mockRejectedValue(new Error("Network error")); + it("should forward fetch errors", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("Test network error")); - const result = await utils.fetchReleaseNotes(); - - expect(result).toBeInstanceOf(Error); - expect((result as Error).message).toBe("Network error"); + await expect(utils.fetchReleaseNotes()).rejects.toThrow("Test network error"); }); it("should handle non-200 HTTP responses", async () => { @@ -58,10 +55,9 @@ describe("fetchReleaseNotes", () => { status: 404, }); - const result = await utils.fetchReleaseNotes(); - - expect(result).toBeInstanceOf(Error); - expect((result as Error).message).toBe("Failed to fetch release notes: 404"); + await expect(utils.fetchReleaseNotes()).rejects.toThrow( + "Failed to fetch release notes: HTTP Error 404" + ); }); it("should handle an empty release notes document", async () => { @@ -71,22 +67,16 @@ describe("fetchReleaseNotes", () => { text: jest.fn().mockResolvedValue(""), }); - const result = await utils.fetchReleaseNotes(); - - expect(result).toBeInstanceOf(Error); - expect((result as Error).message).toBe("Release notes document is empty."); + await expect(utils.fetchReleaseNotes()).rejects.toThrow("Release notes document is empty."); }); - it("should handle an error thrown while retrieving the response text", async () => { + it("should handle an error thrown while retrieving the response text", async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, status: 200, text: jest.fn().mockRejectedValue(new Error("some mock text error")), }); - const result = await utils.fetchReleaseNotes(); - - expect(result).toBeInstanceOf(Error); - expect((result as Error).message).toBe("Release notes document is empty."); + await expect(utils.fetchReleaseNotes()).rejects.toThrow("some mock text error"); }); }); diff --git a/src/utils/fetchUtils.ts b/src/utils/fetchUtils.ts index 0ae3cb39..1da0d60a 100644 --- a/src/utils/fetchUtils.ts +++ b/src/utils/fetchUtils.ts @@ -6,25 +6,23 @@ import { buildReleaseNotesUrl } from "./envUtils"; * @see Utilizes {@link buildReleaseNotesUrl} to build the URL to fetch the release notes document. * @note Handles caching the release notes document in {@link sessionStorage}. * @param signal An optional AbortSignal to cancel the fetch request - * @returns The release notes document as a string or an Error object + * @returns The release notes document as a string + * @throws An error if the fetch request fails */ -export const fetchReleaseNotes = async (signal?: AbortSignal): Promise => { +export const fetchReleaseNotes = async (signal?: AbortSignal): Promise => { if (sessionStorage.getItem("releaseNotes")) { return sessionStorage.getItem("releaseNotes"); } const url: string = buildReleaseNotesUrl(); - const response = await fetch(url, { method: "GET", signal }).catch((err: Error) => err); - if (response instanceof Error) { - return response; - } + const response = await fetch(url, { method: "GET", signal }); if (!response.ok || response.status !== 200) { - return new Error(`Failed to fetch release notes: ${response.status}`); + throw new Error(`Failed to fetch release notes: HTTP Error ${response.status}`); } - const md = await response.text().catch(() => null); + const md = await response.text(); if (!md || md.length === 0) { - return new Error("Release notes document is empty."); + throw new Error("Release notes document is empty."); } sessionStorage.setItem("releaseNotes", md); From 24abb3e35f53f06b98e91f42c7fd4ebccb76cd17 Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 26 Sep 2024 16:17:36 -0400 Subject: [PATCH 13/17] Add Controller test coverage --- src/content/ReleaseNotes/Controller.test.tsx | 201 +++++++++++++++++-- 1 file changed, 189 insertions(+), 12 deletions(-) diff --git a/src/content/ReleaseNotes/Controller.test.tsx b/src/content/ReleaseNotes/Controller.test.tsx index 601aefb5..2dc93181 100644 --- a/src/content/ReleaseNotes/Controller.test.tsx +++ b/src/content/ReleaseNotes/Controller.test.tsx @@ -1,27 +1,204 @@ +import { act, render, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { FC } from "react"; +import Controller from "./Controller"; + const mockFetchReleaseNotes = jest.fn(); jest.mock("../../utils", () => ({ ...jest.requireActual("../../utils"), - fetchReleaseNotes: (...p) => mockFetchReleaseNotes(...p), + fetchReleaseNotes: async (...p) => mockFetchReleaseNotes(...p), +})); + +const mockUsePageTitle = jest.fn(); +jest.mock("../../hooks/usePageTitle", () => ({ + ...jest.requireActual("../../hooks/usePageTitle"), + __esModule: true, + default: (...p) => mockUsePageTitle(...p), })); -// Note: We're mocking the NotesView because Jest doesn't support the `react-markdown` package. -// This is a workaround to prevent the test suite from failing. -jest.mock("./NotesView", () => jest.fn(() =>

Notes

)); +type ParentProps = { + initialEntry?: string; + children: React.ReactNode; +}; -describe("Controller", () => { - beforeEach(() => { +const TestParent: FC = ({ + initialEntry = "/release-notes", + children, +}: ParentProps) => ( + + + + Root Page
} /> + + +); + +describe("Basic Functionality", () => { + afterEach(() => { jest.resetAllMocks(); + jest.clearAllTimers(); + }); + + it("should set the page title 'Release Notes'", async () => { + render(, { + wrapper: TestParent, + }); + + await waitFor(() => { + expect(mockUsePageTitle).toHaveBeenCalledWith("Release Notes"); + }); + }); + + it("should render the loader when fetching release notes", async () => { + mockFetchReleaseNotes.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve("# Mock Markdown Data"); + }, 20000); // This never really needs to resolve for the test + }) + ); + + const { getByLabelText } = render(, { + wrapper: TestParent, + }); + + await waitFor(() => { + expect(getByLabelText("Content Loader")).toBeInTheDocument(); + }); + }); + + it("should fetch release notes on mount", async () => { + render(, { + wrapper: TestParent, + }); + + await waitFor(() => { + expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); + }); + }); + + it("should render the notes when release notes are fetched", async () => { + jest.useFakeTimers(); + + mockFetchReleaseNotes.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve("# Mock Markdown Data"); + }, 500); + }) + ); + + const { getByText, queryByText } = render(, { + wrapper: TestParent, + }); + + expect(queryByText(/Mock Markdown Data/)).not.toBeInTheDocument(); + + act(() => { + jest.advanceTimersByTime(500); // Trigger promise resolution + }); + + await waitFor(() => { + expect(getByText(/Mock Markdown Data/)).toBeInTheDocument(); + }); }); - it.todo("should render the loader when fetching release notes"); + it("should show an error message banner when release notes fail to fetch", async () => { + jest.useFakeTimers(); - it.todo("should render the notes when release notes are fetched"); + mockFetchReleaseNotes.mockImplementation( + () => + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Some error from a utility function")); + }, 500); + }) + ); - it.todo("should render an error message when release notes fail to fetch"); + const { queryByText } = render(, { + wrapper: TestParent, + }); - it.todo("should navigate to the home page when release notes fail to fetch"); + expect(queryByText(/Mock Error/)).not.toBeInTheDocument(); - it.todo("should fetch release notes on mount"); + act(() => { + jest.advanceTimersByTime(500); // Trigger promise rejection + }); - it.todo("should not fetch release notes if they are already loaded (double render)"); + await waitFor(() => { + expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to load release notes.", { + variant: "error", + }); + }); + }); + + it("should navigate to the home page when release notes fail to fetch", async () => { + jest.useFakeTimers(); + + mockFetchReleaseNotes.mockImplementation( + () => + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error("Mock fetch error")); + }, 500); + }) + ); + + const { getByText } = render(, { + wrapper: TestParent, + }); + + act(() => { + jest.advanceTimersByTime(500); // Trigger promise rejection + }); + + await waitFor(() => { + expect(getByText(/Root Page/)).toBeInTheDocument(); + }); + }); + + it("should not fetch release notes if they are already loaded (double render)", async () => { + const { rerender } = render(, { + wrapper: TestParent, + }); + + await waitFor(() => { + expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); + }); + + rerender(); + + expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); // One initial fetch only + }); + + it("should not fetch release notes if they are currently being fetched", async () => { + jest.useFakeTimers(); + + mockFetchReleaseNotes.mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve("# Mock Markdown Data"); + }, 500); + }) + ); + + const { rerender } = render(, { + wrapper: TestParent, + }); + + await waitFor(() => { + expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); + }); + + act(() => { + jest.advanceTimersByTime(250); // Halfway through the fetch + }); + + rerender(); + + expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); // Still only one fetch + }); }); From 233f9e1cc9939606aa839f0d8483b315b524e624 Mon Sep 17 00:00:00 2001 From: Alec M Date: Thu, 26 Sep 2024 16:45:30 -0400 Subject: [PATCH 14/17] Move error display to NotesView --- src/content/ReleaseNotes/Controller.test.tsx | 119 +++---------------- src/content/ReleaseNotes/Controller.tsx | 21 ++-- src/content/ReleaseNotes/NotesView.test.tsx | 14 ++- src/content/ReleaseNotes/NotesView.tsx | 17 ++- 4 files changed, 55 insertions(+), 116 deletions(-) diff --git a/src/content/ReleaseNotes/Controller.test.tsx b/src/content/ReleaseNotes/Controller.test.tsx index 2dc93181..3d04ec67 100644 --- a/src/content/ReleaseNotes/Controller.test.tsx +++ b/src/content/ReleaseNotes/Controller.test.tsx @@ -1,8 +1,9 @@ import { act, render, waitFor } from "@testing-library/react"; -import { MemoryRouter, Route, Routes } from "react-router-dom"; -import { FC } from "react"; +import { Logger } from "../../utils"; import Controller from "./Controller"; +jest.spyOn(Logger, "error").mockImplementation(() => jest.fn()); + const mockFetchReleaseNotes = jest.fn(); jest.mock("../../utils", () => ({ ...jest.requireActual("../../utils"), @@ -16,23 +17,6 @@ jest.mock("../../hooks/usePageTitle", () => ({ default: (...p) => mockUsePageTitle(...p), })); -type ParentProps = { - initialEntry?: string; - children: React.ReactNode; -}; - -const TestParent: FC = ({ - initialEntry = "/release-notes", - children, -}: ParentProps) => ( - - - - Root Page
} /> - - -); - describe("Basic Functionality", () => { afterEach(() => { jest.resetAllMocks(); @@ -40,9 +24,7 @@ describe("Basic Functionality", () => { }); it("should set the page title 'Release Notes'", async () => { - render(, { - wrapper: TestParent, - }); + render(); await waitFor(() => { expect(mockUsePageTitle).toHaveBeenCalledWith("Release Notes"); @@ -50,6 +32,8 @@ describe("Basic Functionality", () => { }); it("should render the loader when fetching release notes", async () => { + jest.useFakeTimers(); + mockFetchReleaseNotes.mockImplementation( () => new Promise((resolve) => { @@ -59,9 +43,7 @@ describe("Basic Functionality", () => { }) ); - const { getByLabelText } = render(, { - wrapper: TestParent, - }); + const { getByLabelText } = render(); await waitFor(() => { expect(getByLabelText("Content Loader")).toBeInTheDocument(); @@ -69,9 +51,7 @@ describe("Basic Functionality", () => { }); it("should fetch release notes on mount", async () => { - render(, { - wrapper: TestParent, - }); + render(); await waitFor(() => { expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); @@ -90,9 +70,7 @@ describe("Basic Functionality", () => { }) ); - const { getByText, queryByText } = render(, { - wrapper: TestParent, - }); + const { getByText, queryByText } = render(); expect(queryByText(/Mock Markdown Data/)).not.toBeInTheDocument(); @@ -105,64 +83,30 @@ describe("Basic Functionality", () => { }); }); - it("should show an error message banner when release notes fail to fetch", async () => { + it("should report the error if fetching release notes fails", async () => { jest.useFakeTimers(); mockFetchReleaseNotes.mockImplementation( () => new Promise((_, reject) => { setTimeout(() => { - reject(new Error("Some error from a utility function")); + reject(new Error("Something bad happened")); }, 500); }) ); - const { queryByText } = render(, { - wrapper: TestParent, - }); - - expect(queryByText(/Mock Error/)).not.toBeInTheDocument(); - - act(() => { - jest.advanceTimersByTime(500); // Trigger promise rejection - }); + render(); await waitFor(() => { - expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to load release notes.", { - variant: "error", - }); - }); - }); - - it("should navigate to the home page when release notes fail to fetch", async () => { - jest.useFakeTimers(); - - mockFetchReleaseNotes.mockImplementation( - () => - new Promise((_, reject) => { - setTimeout(() => { - reject(new Error("Mock fetch error")); - }, 500); - }) - ); - - const { getByText } = render(, { - wrapper: TestParent, - }); - - act(() => { - jest.advanceTimersByTime(500); // Trigger promise rejection - }); - - await waitFor(() => { - expect(getByText(/Root Page/)).toBeInTheDocument(); + expect(Logger.error).toHaveBeenCalledWith( + "ReleaseNotesController: Unable to fetch release notes.", + new Error("Something bad happened") + ); }); }); it("should not fetch release notes if they are already loaded (double render)", async () => { - const { rerender } = render(, { - wrapper: TestParent, - }); + const { rerender } = render(); await waitFor(() => { expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); @@ -172,33 +116,4 @@ describe("Basic Functionality", () => { expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); // One initial fetch only }); - - it("should not fetch release notes if they are currently being fetched", async () => { - jest.useFakeTimers(); - - mockFetchReleaseNotes.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => { - resolve("# Mock Markdown Data"); - }, 500); - }) - ); - - const { rerender } = render(, { - wrapper: TestParent, - }); - - await waitFor(() => { - expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); - }); - - act(() => { - jest.advanceTimersByTime(250); // Halfway through the fetch - }); - - rerender(); - - expect(mockFetchReleaseNotes).toHaveBeenCalledTimes(1); // Still only one fetch - }); }); diff --git a/src/content/ReleaseNotes/Controller.tsx b/src/content/ReleaseNotes/Controller.tsx index 4f1a71c4..0fad3ee0 100644 --- a/src/content/ReleaseNotes/Controller.tsx +++ b/src/content/ReleaseNotes/Controller.tsx @@ -1,6 +1,4 @@ import { memo, useEffect, useRef, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { useSnackbar } from "notistack"; import usePageTitle from "../../hooks/usePageTitle"; import SuspenseLoader from "../../components/SuspenseLoader"; import NotesView from "./NotesView"; @@ -14,33 +12,36 @@ import { Logger, fetchReleaseNotes } from "../../utils"; const ReleaseNotesController = (): React.ReactNode => { usePageTitle("Release Notes"); - const navigate = useNavigate(); - const { enqueueSnackbar } = useSnackbar(); const [document, setDocument] = useState(null); + const [loading, setLoading] = useState(true); const isFetchingRef = useRef(false); const fetchNotes = async () => { + if (isFetchingRef.current) { + return; + } + isFetchingRef.current = true; + setLoading(true); try { const result = await fetchReleaseNotes(); setDocument(result); + isFetchingRef.current = false; } catch (error) { - Logger.error("ReleaseNotesController:", error); - enqueueSnackbar("Unable to load release notes.", { variant: "error" }); - navigate("/"); + Logger.error("ReleaseNotesController: Unable to fetch release notes.", error); } finally { - isFetchingRef.current = false; + setLoading(false); } }; useEffect(() => { - if (!isFetchingRef.current) { + if (!document) { fetchNotes(); } }, []); - if (!document) { + if (loading) { return ; } diff --git a/src/content/ReleaseNotes/NotesView.test.tsx b/src/content/ReleaseNotes/NotesView.test.tsx index fc5e83f0..9b1407c4 100644 --- a/src/content/ReleaseNotes/NotesView.test.tsx +++ b/src/content/ReleaseNotes/NotesView.test.tsx @@ -3,11 +3,17 @@ import { axe } from "jest-axe"; import NotesView from "./NotesView"; describe("Accessibility", () => { - it("should have no violations", async () => { + it("should have no violations (nominal)", async () => { const { container } = render(); expect(await axe(container)).toHaveNoViolations(); }); + + it("should have no violations (error)", async () => { + const { container } = render(); + + expect(await axe(container)).toHaveNoViolations(); + }); }); describe("Basic Functionality", () => { @@ -20,4 +26,10 @@ describe("Basic Functionality", () => { expect(getByText("# Test markdown")).toBeInTheDocument(); }); + + it("should render an error message when no markdown is provided", () => { + const { getByText } = render(); + + expect(getByText("An error occurred while loading the Release Notes")).toBeInTheDocument(); + }); }); diff --git a/src/content/ReleaseNotes/NotesView.tsx b/src/content/ReleaseNotes/NotesView.tsx index 4aa533a1..4bdeb6c8 100644 --- a/src/content/ReleaseNotes/NotesView.tsx +++ b/src/content/ReleaseNotes/NotesView.tsx @@ -1,4 +1,4 @@ -import { Container, styled } from "@mui/material"; +import { Alert, Container, styled } from "@mui/material"; import { memo } from "react"; import Markdown from "react-markdown"; @@ -19,9 +19,14 @@ const StyledMarkdownBox = styled("div")({ }, }); +const StyledError = styled(Alert)({ + marginTop: "20px", + marginBottom: "20px", +}); + type NotesViewProps = { /** - * A valid markdown string to render as the Release Notes. + * A valid markdown string to render as the Release Notes or `null` if the markdown is not available. */ md: string | null; }; @@ -35,7 +40,13 @@ type NotesViewProps = { const NotesView = ({ md }: NotesViewProps): JSX.Element => ( - {md} + {md ? ( + {md} + ) : ( + + An error occurred while loading the Release Notes + + )} ); From e318bb9bf09ed7fbffca33a4a0be6c5e013860f5 Mon Sep 17 00:00:00 2001 From: Toyo Date: Mon, 30 Sep 2024 14:35:17 -0500 Subject: [PATCH 15/17] integrate retrieveCDEs --- src/graphql/index.ts | 3 + src/graphql/retrieveCDEs.ts | 31 +++ src/hooks/useBuildReduxStore.ts | 135 +++------- src/types/CDEs.d.ts | 4 + src/utils/dataModelUtils.test.ts | 441 +++++++++++++++++++++++++++++++ src/utils/dataModelUtils.ts | 98 +++++++ 6 files changed, 616 insertions(+), 96 deletions(-) create mode 100644 src/graphql/retrieveCDEs.ts create mode 100644 src/types/CDEs.d.ts diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 6eb9bd55..2c0f9eed 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -32,6 +32,9 @@ export type { Input as UpdateMyUserInput, Response as UpdateMyUserResp } from ". export { query as LIST_INSTITUTIONS } from "./listInstitutions"; export type { Response as ListInstitutionsResp } from "./listInstitutions"; +export { query as RETRIEVE_CDEs } from "./retrieveCDEs"; +export type { Response as RetrieveCDEsResp, Input as RetrieveCDEsInput } from "./retrieveCDEs"; + // Data Submissions export { mutation as CREATE_SUBMISSION } from "./createSubmission"; export type { diff --git a/src/graphql/retrieveCDEs.ts b/src/graphql/retrieveCDEs.ts new file mode 100644 index 00000000..23e71bd7 --- /dev/null +++ b/src/graphql/retrieveCDEs.ts @@ -0,0 +1,31 @@ +import gql from "graphql-tag"; + +export const query = gql` + query retrieveCDEs($cdeInfo: [CDEInput!]!) { + retrieveCDEs(CDEInfo: $cdeInfo) { + _id + CDEFullName + CDECode + CDEVersion + PermissibleValues + createdAt + updatedAt + } + } +`; + +export type Input = { + cdeInfo: CDEInfo[]; +}; + +export type Response = { + retrieveCDEs: { + _id: string; + CDEFullName: string; + CDECode: string; + CDEVersion: string; + PermissibleValues: string[]; + createdAt: string; + updatedAt: string; + }[]; +}; diff --git a/src/hooks/useBuildReduxStore.ts b/src/hooks/useBuildReduxStore.ts index 45734a77..101e268d 100644 --- a/src/hooks/useBuildReduxStore.ts +++ b/src/hooks/useBuildReduxStore.ts @@ -8,10 +8,16 @@ import { } from "data-model-navigator"; import ReduxThunk from "redux-thunk"; import { createLogger } from "redux-logger"; -// import { useLazyQuery } from "@apollo/client"; +import { useLazyQuery } from "@apollo/client"; +import { defaultTo } from "lodash"; import { baseConfiguration, defaultReadMeTitle, graphViewConfig } from "../config/ModelNavigator"; -import { buildAssetUrls, buildBaseFilterContainers, buildFilterOptionsList } from "../utils"; -// import { LIST_INSTITUTIONS, ListInstitutionsResp } from "../graphql"; +import { + buildAssetUrls, + buildBaseFilterContainers, + buildFilterOptionsList, + updateEnums, +} from "../utils"; +import { RETRIEVE_CDEs, RetrieveCDEsInput, RetrieveCDEsResp } from "../graphql"; export type Status = "waiting" | "loading" | "error" | "success"; @@ -33,83 +39,6 @@ const makeStore = (): Store => { return newStore; }; -/** - * A function to parse the datalist and reolace enums with those returned from retrieveCde query - * Commented out until api is ready - * @params {void} - */ -/* const updateEnums = (cdeMap, dataList, response = []) => { - // const values = Array.from(cdeMap.values()); - - const responseMap = new Map(); - - defaultTo(response, []).forEach((item) => - responseMap.set(`${item.CDECode}.${item.CDEVersion}`, item) - ); - - const resultMap = new Map(); - - cdeMap.forEach((_, key) => { - const [, cdeCodeAndVersion] = key.split(";"); - const item = responseMap.get(cdeCodeAndVersion); - - if (item) { - resultMap.set(key, item); - } - }); - - const newObj = JSON.parse(JSON.stringify(dataList)); - - const mapKeyPrefixes = new Map(); - for (const mapKey of resultMap.keys()) { - const prefix = mapKey.split(";")[0]; - mapKeyPrefixes.set(prefix, mapKey); - } - - function traverseAndReplace(node, parentKey = "") { - if (typeof node !== "object" || node === null) { - return; - } - - if (node.properties) { - for (const key in node.properties) { - if (Object.hasOwn(node.properties, key)) { - const fullKey = `${parentKey}.${key}`.replace(/^\./, ""); - if (mapKeyPrefixes.has(fullKey)) { - const mapFullKey = mapKeyPrefixes.get(fullKey); - const mapData = resultMap.get(mapFullKey); - - if (mapData && mapData.permissibleValues && mapData.permissibleValues.length > 0) { - node.properties[key].enum = mapData.permissibleValues; - } else { - node.properties[key].enum = [ - "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", - ]; - } - } else if ( - !Object.hasOwn(node.properties[key], "enum") || - node.properties[key].enum.length === 0 - ) { - node.properties[key].enum = [ - "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", - ]; - } - } - } - } - - for (const subKey in node) { - if (Object.hasOwn(node, subKey)) { - traverseAndReplace(node[subKey], `${parentKey}.${subKey}`); - } - } - } - - traverseAndReplace(newObj); - - return newObj; -}; */ - /** * A hook to build and populate the Redux store with DMN data * @@ -123,14 +52,13 @@ const useBuildReduxStore = (): [ const [status, setStatus] = useState("waiting"); const [store, setStore] = useState(makeStore()); - // will call retrieveCDEs here - /* const [getInstituitions, { data, loading, error }] = useLazyQuery( - LIST_INSTITUTIONS, - { - context: { clientName: "backend" }, - fetchPolicy: "cache-and-network", - } - ); */ + const [retrieveCDEs, { error: retrieveCDEsError }] = useLazyQuery< + RetrieveCDEsResp, + RetrieveCDEsInput + >(RETRIEVE_CDEs, { + context: { clientName: "backend" }, + fetchPolicy: "cache-and-network", + }); /** * Rebuilds the store from scratch @@ -161,6 +89,7 @@ const useBuildReduxStore = (): [ setStatus("loading"); const assets = buildAssetUrls(datacommon); + const response = await getModelExploreData(assets.model, assets.props)?.catch((e) => { console.error(e); return null; @@ -170,16 +99,30 @@ const useBuildReduxStore = (): [ return; } - // let dictionary; - /* if (response.cdeMap) { - const deets = await getInstituitions();]] - if (deets?.data) { - dictionary = updateEnums(response?.cdeMap, response.data, []); + let dictionary; + const { cdeMap, data: dataList } = response; + + if (cdeMap) { + const cdeInfo: CDEInfo[] = Array.from(response.cdeMap.values()); + try { + const CDEs = await retrieveCDEs({ + variables: { + cdeInfo, + }, + }); + + if (retrieveCDEsError) { + dictionary = updateEnums(cdeMap, dataList, [], true); + } else { + const retrievedCDEs = defaultTo(CDEs.data.retrieveCDEs, []); + dictionary = updateEnums(cdeMap, dataList, retrievedCDEs); + } + } catch (error) { + dictionary = updateEnums(cdeMap, dataList, [], true); } } else { - dictionary = response.data; - } */ - const dictionary = response.data; + dictionary = dataList; + } store.dispatch({ type: "RECEIVE_VERSION_INFO", data: response.version }); diff --git a/src/types/CDEs.d.ts b/src/types/CDEs.d.ts new file mode 100644 index 00000000..75b8754c --- /dev/null +++ b/src/types/CDEs.d.ts @@ -0,0 +1,4 @@ +type CDEInfo = { + CDECode: string; + CDEVersion: string; +}; diff --git a/src/utils/dataModelUtils.test.ts b/src/utils/dataModelUtils.test.ts index 6c0d2cfd..ab4a1ffb 100644 --- a/src/utils/dataModelUtils.test.ts +++ b/src/utils/dataModelUtils.test.ts @@ -249,3 +249,444 @@ describe("buildFilterOptionsList tests", () => { expect(result).toEqual(["item 1", "item 2", "item 3", "item 4"]); }); }); + +describe("updateEnums", () => { + const cdeMap = new Map([ + [ + "program.program_name;11444542.1.00", + [ + { + CDECode: "11444542", + CDEVersion: "1.00", + }, + ], + ], + ]); + + const dataList = { + program: { + $schema: "http://json-schema.org/draft-06/schema#", + id: "program", + title: "program", + category: "study", + program: "*", + project: "*", + additionalProperties: false, + submittable: true, + constraints: null, + type: "object", + assignment: "core", + class: "primary", + desc: "Program in the Cancer Data Service refer to a broad framework of goals under which related projects or other research activities are grouped. Example - Clinical Proteomic Tumor Analysis Consortium (CPTAC)", + description: + "Program in the Cancer Data Service refer to a broad framework of goals under which related projects or other research activities are grouped. Example - Clinical Proteomic Tumor Analysis Consortium (CPTAC)", + template: "Yes", + properties: { + program_name: { + category: "program", + description: + "The name of the program under which related studies will be grouped, in full text and unabbreviated form, exactly as it will be displayed within the UI.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "required", + display: "no", + }, + program_acronym: { + category: "program", + description: + "The name of the program under which related studies will be grouped, expressed in the form of the acronym by which it will identified within the UI.
This property is used as the key via which study records can be associated with the appropriate program during data loading, and to identify the correct records during data updates.", + type: "string", + src: "Internally-curated", + key: true, + isIncludedInTemplate: true, + propertyType: "required", + display: "no", + }, + program_short_description: { + category: "program", + description: "An abbreviated, single sentence description of the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_full_description: { + category: "program", + description: "A more detailed, multiple sentence description of the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_external_url: { + category: "program", + description: + "The external url to which users should be directed in order to learn more about the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_sort_order: { + category: "program", + description: + "An arbitrarily-assigned value used to dictate the order in which programs are displayed within the application's UI.", + type: "integer", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "optional", + display: "no", + }, + program_short_name: { + category: "program", + description: + "An acronym or abbreviated form of the title of a broad framework of goals under which related projects or other research activities are grouped. Example - CPTAC", + type: "String", + isIncludedInTemplate: true, + propertyType: "optional", + display: "no", + }, + institution: { + category: "program", + description: "TBD", + type: "String", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + crdc_id: { + category: "program", + description: "The crdc_id is a unique identifier that is generated by Data Hub", + type: "string", + isIncludedInTemplate: false, + propertyType: "optional", + display: "no", + }, + }, + inclusion: { + required: ["program_name", "program_acronym"], + optional: ["program_sort_order", "program_short_name", "crdc_id"], + preferred: [ + "program_short_description", + "program_full_description", + "program_external_url", + "institution", + ], + }, + uiDisplay: { + no: [ + "program_name", + "program_acronym", + "program_short_description", + "program_full_description", + "program_external_url", + "program_sort_order", + "program_short_name", + "institution", + "crdc_id", + ], + }, + required: ["program_name", "program_acronym"], + preferred: [ + "program_short_description", + "program_full_description", + "program_external_url", + "institution", + ], + optional: ["program_sort_order", "program_short_name", "crdc_id"], + yes: [], + no: [ + "program_name", + "program_acronym", + "program_short_description", + "program_full_description", + "program_external_url", + "program_sort_order", + "program_short_name", + "institution", + "crdc_id", + ], + multiplicity: "Many To One", + links: [ + { + Dst: "program", + Src: "study", + multiplicity: "many_to_one", + }, + ], + }, + }; + + const CDEresponse = { + _id: "967c20fd-8980-44ec-aa3e-e9647e4f6b26", + CDEFullName: "Subject Legal Adult Or Pediatric Participant Type", + CDECode: "11444542", + CDEVersion: "1.00", + PermissibleValues: ["Pediatric", "Adult - legal age"], + createdAt: "2024-09-24T11:45:42.313Z", + updatedAt: "2024-09-24T11:45:42.313Z", + }; + + it("should update dataList with permissible values from the response", () => { + const response = [CDEresponse]; + + const result = utils.updateEnums(cdeMap, dataList, response); + + expect(result.program.properties["program_name"].enum).toEqual([ + "Pediatric", + "Adult - legal age", + ]); + }); + + it("should use fallback message if permissible values are empty", () => { + const response = [ + { + ...CDEresponse, + PermissibleValues: [], + }, + ]; + + const result = utils.updateEnums(cdeMap, dataList, response); + + expect(result.program.properties["program_name"].enum).toEqual([ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]); + }); + + it("should apply fallback message when response is empty and apiError is true", () => { + const result = utils.updateEnums(cdeMap, dataList, [], true); + + expect(result.program.properties["program_name"].enum).toEqual([ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]); + }); +}); + +describe("traverseAndReplace", () => { + const node = { + program: { + $schema: "http://json-schema.org/draft-06/schema#", + id: "program", + title: "program", + category: "study", + program: "*", + project: "*", + additionalProperties: false, + submittable: true, + constraints: null, + type: "object", + assignment: "core", + class: "primary", + desc: "Program in the Cancer Data Service refer to a broad framework of goals under which related projects or other research activities are grouped. Example - Clinical Proteomic Tumor Analysis Consortium (CPTAC)", + description: + "Program in the Cancer Data Service refer to a broad framework of goals under which related projects or other research activities are grouped. Example - Clinical Proteomic Tumor Analysis Consortium (CPTAC)", + template: "Yes", + properties: { + program_name: { + category: "program", + description: + "The name of the program under which related studies will be grouped, in full text and unabbreviated form, exactly as it will be displayed within the UI.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "required", + display: "no", + enum: [ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ], + }, + program_acronym: { + category: "program", + description: + "The name of the program under which related studies will be grouped, expressed in the form of the acronym by which it will identified within the UI.
This property is used as the key via which study records can be associated with the appropriate program during data loading, and to identify the correct records during data updates.", + type: "string", + src: "Internally-curated", + key: true, + isIncludedInTemplate: true, + propertyType: "required", + display: "no", + }, + program_short_description: { + category: "program", + description: "An abbreviated, single sentence description of the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_full_description: { + category: "program", + description: "A more detailed, multiple sentence description of the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_external_url: { + category: "program", + description: + "The external url to which users should be directed in order to learn more about the program.", + type: "string", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + program_sort_order: { + category: "program", + description: + "An arbitrarily-assigned value used to dictate the order in which programs are displayed within the application's UI.", + type: "integer", + src: "Internally-curated", + isIncludedInTemplate: true, + propertyType: "optional", + display: "no", + }, + program_short_name: { + category: "program", + description: + "An acronym or abbreviated form of the title of a broad framework of goals under which related projects or other research activities are grouped. Example - CPTAC", + type: "String", + isIncludedInTemplate: true, + propertyType: "optional", + display: "no", + enum: [ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ], + }, + institution: { + category: "program", + description: "TBD", + type: "String", + isIncludedInTemplate: true, + propertyType: "preferred", + display: "no", + }, + crdc_id: { + category: "program", + description: "The crdc_id is a unique identifier that is generated by Data Hub", + type: "string", + isIncludedInTemplate: false, + propertyType: "optional", + display: "no", + }, + }, + inclusion: { + required: ["program_name", "program_acronym"], + optional: ["program_sort_order", "program_short_name", "crdc_id"], + preferred: [ + "program_short_description", + "program_full_description", + "program_external_url", + "institution", + ], + }, + uiDisplay: { + no: [ + "program_name", + "program_acronym", + "program_short_description", + "program_full_description", + "program_external_url", + "program_sort_order", + "program_short_name", + "institution", + "crdc_id", + ], + }, + required: ["program_name", "program_acronym"], + preferred: [ + "program_short_description", + "program_full_description", + "program_external_url", + "institution", + ], + optional: ["program_sort_order", "program_short_name", "crdc_id"], + yes: [], + no: [ + "program_name", + "program_acronym", + "program_short_description", + "program_full_description", + "program_external_url", + "program_sort_order", + "program_short_name", + "institution", + "crdc_id", + ], + multiplicity: "Many To One", + links: [ + { + Dst: "program", + Src: "study", + multiplicity: "many_to_one", + }, + ], + }, + }; + + const resultMap = new Map([ + [ + "program.program_name;11524549.1.00", + { + _id: "967c20fd-8980-44ec-aa3e-e9647e4f6b26", + CDEFullName: "Subject Legal Adult Or Pediatric Participant Type", + CDECode: "11524549", + CDEVersion: "1.00", + PermissibleValues: ["Pediatric", "Adult - legal age"], + createdAt: "2024-09-24T11:45:42.313Z", + updatedAt: "2024-09-24T11:45:42.313Z", + }, + ], + ]); + + const mapKeyPrefixes = new Map([["program.program_name", "program.program_name;11524549.1.00"]]); + + it("should replace permissible values using mapKeyPrefixes", () => { + const mapKeyPrefixesNoValues = new Map(); + const apiError = false; + + utils.traverseAndReplace(node, resultMap, mapKeyPrefixes, mapKeyPrefixesNoValues, apiError); + + expect(node["program"].properties["program_name"].enum).toEqual([ + "Pediatric", + "Adult - legal age", + ]); + }); + + it("should use fallback message when permissible values are empty and apiError is true", () => { + const resultMap = new Map(); + const mapKeyPrefixes = new Map(); + const mapKeyPrefixesNoValues = new Map([ + ["program.program_name", "program.program_name;11524549.1.00"], + ]); + const apiError = true; + + utils.traverseAndReplace(node, resultMap, mapKeyPrefixes, mapKeyPrefixesNoValues, apiError); + + expect(node["program"].properties["program_name"].enum).toEqual([ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]); + }); + + it("should use fallback message if resultMap has no matching entry", () => { + const resultMap = new Map(); + const mapKeyPrefixes = new Map(); + const mapKeyPrefixesNoValues = new Map([ + ["program.program_name", "program.program_name;11524549.1.00"], + ]); + const apiError = false; + + utils.traverseAndReplace(node, resultMap, mapKeyPrefixes, mapKeyPrefixesNoValues, apiError); + + expect(node["program"].properties["program_name"].enum).toEqual([ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]); + }); +}); diff --git a/src/utils/dataModelUtils.ts b/src/utils/dataModelUtils.ts index 34d5ce5a..feb61b08 100644 --- a/src/utils/dataModelUtils.ts +++ b/src/utils/dataModelUtils.ts @@ -1,5 +1,7 @@ +import { defaultTo } from "lodash"; import { MODEL_FILE_REPO } from "../config/DataCommons"; import env from "../env"; +import { RetrieveCDEsResp } from "../graphql"; /** * Fetch the tracked Data Model content manifest. @@ -102,3 +104,99 @@ export const buildFilterOptionsList = (dc: DataCommon): string[] => { return [...a, ...searchData.checkboxItems.map((item) => item?.name?.toLowerCase())]; }, []); }; + +/** + * A function to parse the datalist and reolace enums with those returned from retrieveCde query + * Commented out until api is ready + * @params {void} + */ +export const updateEnums = ( + cdeMap: Map, + dataList, + response: RetrieveCDEsResp["retrieveCDEs"] = [], + apiError = false +) => { + const responseMap: Map = new Map(); + + defaultTo(response, []).forEach((item) => + responseMap.set(`${item.CDECode}.${item.CDEVersion}`, item) + ); + + const resultMap: Map = new Map(); + const mapKeyPrefixes: Map = new Map(); + const mapKeyPrefixesNoValues: Map = new Map(); + + cdeMap.forEach((_, key) => { + const [prefix, cdeCodeAndVersion] = key.split(";"); + const item = responseMap.get(cdeCodeAndVersion); + + if (item) { + resultMap.set(key, item); + mapKeyPrefixes.set(prefix, key); + } else { + mapKeyPrefixesNoValues.set(prefix, key); + } + }); + + const newObj = JSON.parse(JSON.stringify(dataList)); + + traverseAndReplace(newObj, resultMap, mapKeyPrefixes, mapKeyPrefixesNoValues, apiError); + + return newObj; +}; + +export const traverseAndReplace = ( + node, + resultMap: Map, + mapKeyPrefixes: Map, + mapKeyPrefixesNoValues: Map, + apiError: boolean, + parentKey = "" +) => { + if (typeof node !== "object" || node === null) return; + + if (node.properties) { + for (const key in node.properties) { + if (Object.hasOwn(node.properties, key)) { + const fullKey = `${parentKey}.${key}`.replace(/^\./, ""); + const prefixMatch = mapKeyPrefixes.get(fullKey); + const noValuesMatch = mapKeyPrefixesNoValues.get(fullKey); + const fallbackMessage = [ + "Permissible values are currently not available. Please contact the Data Hub HelpDesk at NCICRDCHelpDesk@mail.nih.gov", + ]; + + if (prefixMatch) { + const mapFullKey = resultMap.get(prefixMatch); + if ( + mapFullKey && + mapFullKey.PermissibleValues && + mapFullKey.PermissibleValues.length > 0 + ) { + node.properties[key].enum = mapFullKey.PermissibleValues; + } else { + node.properties[key].enum = fallbackMessage; + } + } + + if (noValuesMatch) { + if (apiError || !node.properties[key].enum) { + node.properties[key].enum = fallbackMessage; + } + } + } + } + } + + for (const subKey in node) { + if (Object.hasOwn(node, subKey)) { + traverseAndReplace( + node[subKey], + resultMap, + mapKeyPrefixes, + mapKeyPrefixesNoValues, + apiError, + `${parentKey}.${subKey}` + ); + } + } +}; From eee7a8b214ca6cc6732014a1c6fcf3e7ec359e56 Mon Sep 17 00:00:00 2001 From: Alec M Date: Tue, 1 Oct 2024 14:19:01 -0400 Subject: [PATCH 16/17] Address PR comments --- src/utils/fetchUtils.ts | 5 +++-- src/utils/logger.test.ts | 13 +++++++++++++ src/utils/logger.ts | 7 ++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/utils/fetchUtils.ts b/src/utils/fetchUtils.ts index 1da0d60a..c3d352b8 100644 --- a/src/utils/fetchUtils.ts +++ b/src/utils/fetchUtils.ts @@ -10,8 +10,9 @@ import { buildReleaseNotesUrl } from "./envUtils"; * @throws An error if the fetch request fails */ export const fetchReleaseNotes = async (signal?: AbortSignal): Promise => { - if (sessionStorage.getItem("releaseNotes")) { - return sessionStorage.getItem("releaseNotes"); + const cachedNotes = sessionStorage.getItem("releaseNotes"); + if (cachedNotes) { + return cachedNotes; } const url: string = buildReleaseNotesUrl(); diff --git a/src/utils/logger.test.ts b/src/utils/logger.test.ts index bdd96baf..ab8ebf0c 100644 --- a/src/utils/logger.test.ts +++ b/src/utils/logger.test.ts @@ -62,4 +62,17 @@ describe("Logger", () => { expect(consoleErrorSpy).not.toHaveBeenCalled(); }); + + it.each(["stage", "prod"])( + "should not log on the upper tier '%s'", + (tier) => { + env.NODE_ENV = "development"; // Override 'test' to log the message + + env.REACT_APP_DEV_TIER = tier; + + Logger.error("A message that should not be visible"); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + } + ); }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 92f9f33f..507c9e83 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -17,11 +17,16 @@ export type LogLevel = "error"; * @returns void */ const LoggingWrapper = (level: LogLevel, message: string, ...optionalParams: unknown[]): void => { - // Skip logging in a test environment. + // Skip logging in a testing context. if (env?.NODE_ENV === "test") { return; } + // Skip logging on stage or production environments. + if (env?.REACT_APP_DEV_TIER === "prod" || env?.REACT_APP_DEV_TIER === "stage") { + return; + } + const timestamp = new Date().toISOString(); switch (level) { case "error": From 85337f6478fce437470c659d3ee03dc7831826e2 Mon Sep 17 00:00:00 2001 From: Alec M Date: Tue, 1 Oct 2024 14:41:25 -0400 Subject: [PATCH 17/17] deps: Upgrade DMN --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7cc6d415..2ddd0d0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8832,7 +8832,7 @@ }, "node_modules/data-model-navigator": { "version": "1.1.33", - "resolved": "git+ssh://git@github.com/CBIIT/Data-Model-Navigator.git#333cc967cf4ed952ae5c8cc3bec56523e1d46c87", + "resolved": "git+ssh://git@github.com/CBIIT/Data-Model-Navigator.git#ce4d6f0a308194834cc568cdd932fab8f935270a", "license": "ISC", "dependencies": { "@material-ui/core": "^4.12.4",