From f8128b8d008d95db563e782418e1fa536b9e32eb Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Tue, 7 Feb 2023 16:56:42 -0500 Subject: [PATCH 01/22] feat: create GlobalContent. --- client/context/GlobalContext.test.tsx | 31 ++++++++++ client/context/GlobalContext.tsx | 82 +++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 client/context/GlobalContext.test.tsx create mode 100644 client/context/GlobalContext.tsx diff --git a/client/context/GlobalContext.test.tsx b/client/context/GlobalContext.test.tsx new file mode 100644 index 000000000..a03768a5b --- /dev/null +++ b/client/context/GlobalContext.test.tsx @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { renderHook, act } from "@testing-library/react-hooks"; +import { GlobalContextProvider, useGlobalContext } from "./GlobalContext"; + +describe("useGlobalContext", () => { + describe("setSnackbarState", () => { + it("sets the snackbar state with text and severity", async () => { + const { result } = await renderHook(() => useGlobalContext(), { wrapper: GlobalContextProvider }); + + expect(result.current.state.snackbarState).toEqual({ + open: false, + message: "", + severity: "info" + }); + + await act(async () => { + await result.current.action.setSnackbarState({ + open: true, + message: "oh no!", + severity: "error" + }); + }); + + expect(result.current.state.snackbarState).toEqual({ + open: true, + message: "oh no!", + severity: "error" + }); + }); + }); +}); diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx new file mode 100644 index 000000000..d090cfe72 --- /dev/null +++ b/client/context/GlobalContext.tsx @@ -0,0 +1,82 @@ +import React, { createContext, useContext, useReducer } from "react"; + +interface SnackbarState { + open: boolean, + message: string, + severity: string +} + +interface GlobalState { + snackbarState: SnackbarState; +} + +/** + * Pass a shared entity to react components, + * specifically a way to make api requests. + */ +const globalContextParams: GlobalState = { + snackbarState: { + open: false, + message: "", + severity: "info", + }, +}; + +const reducerMapping: Record = { + SET_SNACKBAR_STATE: "snackbarState", +}; + +/** + * Update the shared states of react components. + */ +const globalReducer = (state: GlobalState, { type, payload }: { type: string, payload: unknown}) => { + if(Object.keys(reducerMapping).includes(type)){ + console.log(type, payload); + + return { + ...state, + [reducerMapping[type]]: payload + }; + } else { + console.error(`global action type: ${type} does not exist`); + return state; + } +}; + +const GlobalContext = createContext({}); + +export const GlobalContextProvider = ({ children }) => { + const [state, dispatch] = useReducer(globalReducer, globalContextParams); + const value = { state, dispatch }; + return {children}; +}; + +export const useGlobalContext = () => { + const { + state: { + snackbarState, + }, + dispatch, + } = useContext(GlobalContext); + + const setSnackbarState = (snackbarState: SnackbarState) => { + dispatch({ + type: "SET_SNACKBAR_STATE", + payload: snackbarState + }); + }; + + return { + state: { + snackbarState, + }, + action: { + setSnackbarState, + }, + }; +} + +export default { + GlobalContextProvider, + useGlobalContext +} From 186bf233dfb40bb4aaf0d16c31a78ebd981e4e21 Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Tue, 7 Feb 2023 16:57:03 -0500 Subject: [PATCH 02/22] refactor: apply GlobalContext. --- .../components/edit/EditorSnackbar.test.tsx | 14 +++-- client/components/edit/EditorSnackbar.tsx | 4 +- client/components/edit/StateModal.test.tsx | 27 +++++++--- client/components/edit/StateModal.tsx | 6 ++- .../DatastreamDeleteModalContent.test.tsx | 14 ++++- .../edit/parents/ParentList.test.tsx | 24 ++++++--- client/components/edit/parents/ParentList.tsx | 7 ++- .../edit/parents/ParentPicker.test.tsx | 42 +++++++++------ .../components/edit/parents/ParentPicker.tsx | 7 ++- client/context/EditorContext.test.tsx | 27 ---------- client/context/EditorContext.tsx | 25 +-------- client/hooks/useDatastreamOperation.test.ts | 53 ++++++++++++------- client/hooks/useDatastreamOperation.ts | 6 ++- client/pages/_app.tsx | 19 ++++--- 14 files changed, 157 insertions(+), 118 deletions(-) diff --git a/client/components/edit/EditorSnackbar.test.tsx b/client/components/edit/EditorSnackbar.test.tsx index 1fd878c2e..7706a4210 100644 --- a/client/components/edit/EditorSnackbar.test.tsx +++ b/client/components/edit/EditorSnackbar.test.tsx @@ -4,6 +4,12 @@ import { shallow, mount } from "enzyme"; import toJson from "enzyme-to-json"; import EditorSnackbar from "./EditorSnackbar"; +const mockUseGlobalContext = jest.fn(); +jest.mock("../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseEditorContext = jest.fn(); jest.mock("../../context/EditorContext", () => ({ useEditorContext: () => { @@ -13,9 +19,9 @@ jest.mock("../../context/EditorContext", () => ({ jest.mock("./children/ChildList", () => () => "ChildList"); describe("EditorSnackbar", () => { - let editorValues; + let globalValues; beforeEach(() => { - editorValues = { + globalValues = { state: { snackbarState: { message: "test1", @@ -27,7 +33,7 @@ describe("EditorSnackbar", () => { setSnackbarState: jest.fn(), }, }; - mockUseEditorContext.mockReturnValue(editorValues); + mockUseGlobalContext.mockReturnValue(globalValues); }); it("renders", () => { @@ -40,7 +46,7 @@ describe("EditorSnackbar", () => { component.find("button.editorSnackBarAlertCloseButton").simulate("click"); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: false, message: "", severity: "info", diff --git a/client/components/edit/EditorSnackbar.tsx b/client/components/edit/EditorSnackbar.tsx index a24d14a3a..1c5c15dbf 100644 --- a/client/components/edit/EditorSnackbar.tsx +++ b/client/components/edit/EditorSnackbar.tsx @@ -3,7 +3,7 @@ import Alert from "@mui/material/Alert"; import IconButton from "@mui/material/IconButton"; import Snackbar from "@mui/material/Snackbar"; import CloseIcon from "@mui/icons-material/Close"; -import { useEditorContext } from "../../context/EditorContext"; +import { useGlobalContext } from "../../context/GlobalContext"; const EditorSnackbar = (): React.ReactElement => { const { @@ -11,7 +11,7 @@ const EditorSnackbar = (): React.ReactElement => { snackbarState: { message, open, severity }, }, action: { setSnackbarState }, - } = useEditorContext(); + } = useGlobalContext(); const handleClose = () => { setSnackbarState({ diff --git a/client/components/edit/StateModal.test.tsx b/client/components/edit/StateModal.test.tsx index 93dab6f32..e5bceaeca 100644 --- a/client/components/edit/StateModal.test.tsx +++ b/client/components/edit/StateModal.test.tsx @@ -8,6 +8,13 @@ import StateModal from "./StateModal"; import Checkbox from "@mui/material/Checkbox"; import RadioGroup from "@mui/material/RadioGroup"; +const mockUseGlobalContext = jest.fn(); +jest.mock("../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); + const mockUseEditorContext = jest.fn(); jest.mock("../../context/EditorContext", () => ({ useEditorContext: () => { @@ -23,10 +30,16 @@ jest.mock("../../context/FetchContext", () => ({ })); describe("StateModal", () => { + let globalValues; let editorValues; let fetchContextValues; const pid = "foo:123"; beforeEach(() => { + globalValues = { + action: { + setSnackbarState: jest.fn(), + }, + }; editorValues = { state: { stateModalActivePid: pid, @@ -35,17 +48,17 @@ describe("StateModal", () => { }, action: { removeFromObjectDetailsStorage: jest.fn(), - setSnackbarState: jest.fn(), toggleStateModal: jest.fn(), }, }; - mockUseEditorContext.mockReturnValue(editorValues); fetchContextValues = { action: { fetchJSON: jest.fn(), fetchText: jest.fn(), }, }; + mockUseGlobalContext.mockReturnValue(globalValues); + mockUseEditorContext.mockReturnValue(editorValues); mockUseFetchContext.mockReturnValue(fetchContextValues); }); @@ -109,7 +122,7 @@ describe("StateModal", () => { { body: "Active", method: "PUT" } ) ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Status saved successfully.", open: true, severity: "success", @@ -132,7 +145,7 @@ describe("StateModal", () => { wrapper.find("button").at(1).simulate("click"); }); await waitFor(() => - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "No changes were made.", open: true, severity: "info", @@ -168,7 +181,7 @@ describe("StateModal", () => { { body: "Active", method: "PUT" } ) ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: 'Status failed to save; "not ok"', open: true, severity: "error", @@ -208,7 +221,7 @@ describe("StateModal", () => { { body: "Active", method: "PUT" } ) ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: 'Status failed to save; "not ok"', open: true, severity: "error", @@ -248,7 +261,7 @@ describe("StateModal", () => { { body: "Active", method: "PUT" } ) ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Status saved successfully.", open: true, severity: "success", diff --git a/client/components/edit/StateModal.tsx b/client/components/edit/StateModal.tsx index 568752910..13a71a05a 100644 --- a/client/components/edit/StateModal.tsx +++ b/client/components/edit/StateModal.tsx @@ -11,15 +11,19 @@ import Radio from "@mui/material/Radio"; import RadioGroup from "@mui/material/RadioGroup"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; +import { useGlobalContext } from "../../context/GlobalContext"; import { useEditorContext } from "../../context/EditorContext"; import { getObjectRecursiveChildPidsUrl, getObjectStateUrl } from "../../util/routes"; import { useFetchContext } from "../../context/FetchContext"; import ObjectLoader from "./ObjectLoader"; const StateModal = (): React.ReactElement => { + const { + action: { setSnackbarState }, + } = useGlobalContext(); const { state: { isStateModalOpen, objectDetailsStorage, stateModalActivePid }, - action: { removeFromObjectDetailsStorage, setSnackbarState, toggleStateModal }, + action: { removeFromObjectDetailsStorage, toggleStateModal }, } = useEditorContext(); const { action: { fetchJSON, fetchText }, diff --git a/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx b/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx index 8d510bc4b..cfaa836f1 100644 --- a/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx +++ b/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx @@ -5,6 +5,12 @@ import { act } from "react-dom/test-utils"; import toJson from "enzyme-to-json"; import DatastreamDeleteModalContent from "./DatastreamDeleteModalContent"; +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseEditorContext = jest.fn(); jest.mock("../../../context/EditorContext", () => ({ useEditorContext: () => { @@ -14,9 +20,15 @@ jest.mock("../../../context/EditorContext", () => ({ const mockUseDatastreamOperation = jest.fn(); jest.mock("../../../hooks/useDatastreamOperation", () => () => mockUseDatastreamOperation()); describe("DatastreamDeleteModalContent", () => { + let globalValues; let editorValues; let datastreamOperationValues; beforeEach(() => { + globalValues = { + action: { + setSnackbarState: jest.fn(), + }, + }; editorValues = { state: { currentPid: "vudl:123", @@ -24,13 +36,13 @@ describe("DatastreamDeleteModalContent", () => { }, action: { loadCurrentObjectDetails: jest.fn().mockResolvedValue({}), - setSnackbarState: jest.fn(), toggleDatastreamModal: jest.fn(), }, }; datastreamOperationValues = { deleteDatastream: jest.fn(), }; + mockUseGlobalContext.mockReturnValue(globalValues); mockUseEditorContext.mockReturnValue(editorValues); mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); }); diff --git a/client/components/edit/parents/ParentList.test.tsx b/client/components/edit/parents/ParentList.test.tsx index e3a6486d2..0f2d5f7e8 100644 --- a/client/components/edit/parents/ParentList.test.tsx +++ b/client/components/edit/parents/ParentList.test.tsx @@ -5,6 +5,12 @@ import toJson from "enzyme-to-json"; import ParentList from "./ParentList"; import { waitFor } from "@testing-library/dom"; +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseEditorContext = jest.fn(); jest.mock("../../../context/EditorContext", () => ({ useEditorContext: () => { @@ -19,11 +25,17 @@ jest.mock("../../../context/FetchContext", () => ({ })); describe("ParentList", () => { + let globalValues; let editorValues; let fetchValues; let pid: string; beforeEach(() => { pid = "foo:123"; + globalValues = { + action: { + setSnackbarState: jest.fn(), + }, + }; editorValues = { state: { parentDetailsStorage: { @@ -66,7 +78,6 @@ describe("ParentList", () => { loadParentDetailsIntoStorage: jest.fn(), removeFromObjectDetailsStorage: jest.fn(), removeFromParentDetailsStorage: jest.fn(), - setSnackbarState: jest.fn(), }, }; fetchValues = { @@ -74,6 +85,7 @@ describe("ParentList", () => { fetchText: jest.fn(), }, }; + mockUseGlobalContext.mockReturnValue(globalValues); mockUseEditorContext.mockReturnValue(editorValues); mockUseFetchContext.mockReturnValue(fetchValues); }); @@ -124,7 +136,7 @@ describe("ParentList", () => { expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.removeFromParentDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.clearPidFromChildListStorage).toHaveBeenCalledWith("foo:122"); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Successfully removed foo:123 from foo:122", open: true, severity: "info", @@ -149,11 +161,11 @@ describe("ParentList", () => { "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", { method: "DELETE" } ); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.removeFromParentDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.clearPidFromChildListStorage).not.toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "not ok", open: true, severity: "error", @@ -172,11 +184,11 @@ describe("ParentList", () => { "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", { method: "DELETE" } ); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.removeFromParentDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.clearPidFromChildListStorage).not.toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "boom", open: true, severity: "error", diff --git a/client/components/edit/parents/ParentList.tsx b/client/components/edit/parents/ParentList.tsx index e1c45890e..6c874aa7e 100644 --- a/client/components/edit/parents/ParentList.tsx +++ b/client/components/edit/parents/ParentList.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useGlobalContext } from "../../../context/GlobalContext"; import { useEditorContext } from "../../../context/EditorContext"; import { useFetchContext } from "../../../context/FetchContext"; import { getParentUrl } from "../../../util/routes"; @@ -10,6 +11,11 @@ export interface ParentListProps { } const ParentList = ({ pid, initiallyShallow = true }: ParentListProps): React.ReactElement => { + const { + action: { + setSnackbarState, + }, + } = useGlobalContext(); const { state: { parentDetailsStorage }, action: { @@ -17,7 +23,6 @@ const ParentList = ({ pid, initiallyShallow = true }: ParentListProps): React.Re loadParentDetailsIntoStorage, removeFromObjectDetailsStorage, removeFromParentDetailsStorage, - setSnackbarState, }, } = useEditorContext(); const { diff --git a/client/components/edit/parents/ParentPicker.test.tsx b/client/components/edit/parents/ParentPicker.test.tsx index 50e064c69..a9e78a9e6 100644 --- a/client/components/edit/parents/ParentPicker.test.tsx +++ b/client/components/edit/parents/ParentPicker.test.tsx @@ -6,6 +6,12 @@ import toJson from "enzyme-to-json"; import ParentPicker from "./ParentPicker"; import { waitFor } from "@testing-library/dom"; +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseEditorContext = jest.fn(); jest.mock("../../../context/EditorContext", () => ({ useEditorContext: () => { @@ -35,11 +41,17 @@ jest.mock("../PidPicker", () => (args) => { }); describe("ParentPicker", () => { + let globalValues; let editorValues; let fetchValues; const pid = "foo:123"; const parentPid = "foo:122"; beforeEach(() => { + globalValues = { + action: { + setSnackbarState: jest.fn(), + }, + }; editorValues = { state: { objectDetailsStorage: {}, @@ -48,7 +60,6 @@ describe("ParentPicker", () => { clearPidFromChildListStorage: jest.fn(), removeFromObjectDetailsStorage: jest.fn(), removeFromParentDetailsStorage: jest.fn(), - setSnackbarState: jest.fn(), }, }; fetchValues = { @@ -56,6 +67,7 @@ describe("ParentPicker", () => { fetchText: jest.fn(), }, }; + mockUseGlobalContext.mockReturnValue(globalValues); mockUseEditorContext.mockReturnValue(editorValues); mockUseFetchContext.mockReturnValue(fetchValues); }); @@ -96,7 +108,7 @@ describe("ParentPicker", () => { wrapper.update(); await act(async () => { wrapper.find("button").simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", @@ -105,7 +117,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.removeFromParentDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.clearPidFromChildListStorage).toHaveBeenCalledWith(parentPid); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Successfully added foo:123 to foo:122", open: true, severity: "info", @@ -124,7 +136,7 @@ describe("ParentPicker", () => { wrapper.update(); await act(async () => { wrapper.find("button").simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", @@ -133,7 +145,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.removeFromParentDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.clearPidFromChildListStorage).not.toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "kaboom", open: true, severity: "error", @@ -150,7 +162,7 @@ describe("ParentPicker", () => { wrapper.update(); await act(async () => { wrapper.find("button").simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", @@ -159,7 +171,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.removeFromParentDetailsStorage).not.toHaveBeenCalled(); expect(editorValues.action.clearPidFromChildListStorage).not.toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "not ok", open: true, severity: "error", @@ -189,7 +201,7 @@ describe("ParentPicker", () => { await Promise.resolve(); wrapper.update(); wrapper.find("button").at(1).simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/foo%3A123/parent/foo%3A122", @@ -198,7 +210,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.removeFromParentDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.clearPidFromChildListStorage).toHaveBeenCalledWith(parentPid); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Successfully added foo:123 to foo:122", open: true, severity: "info", @@ -221,7 +233,7 @@ describe("ParentPicker", () => { wrapper.update(); await act(async () => { wrapper.find("button").at(1).simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenNthCalledWith( 1, @@ -236,7 +248,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.removeFromParentDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.clearPidFromChildListStorage).toHaveBeenCalledWith(parentPid); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Successfully added foo:123 to foo:122", open: true, severity: "info", @@ -261,7 +273,7 @@ describe("ParentPicker", () => { wrapper.update(); await act(async () => { wrapper.find("button").at(1).simulate("click"); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); expect(fetchValues.action.fetchText).toHaveBeenNthCalledWith( 1, @@ -276,7 +288,7 @@ describe("ParentPicker", () => { expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.removeFromParentDetailsStorage).toHaveBeenCalledWith(pid); expect(editorValues.action.clearPidFromChildListStorage).toHaveBeenCalledWith(parentPid); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Successfully added foo:123 to foo:122", open: true, severity: "info", @@ -289,10 +301,10 @@ describe("ParentPicker", () => { setSelected(parentPid); await Promise.resolve(); errorCallback(parentPid); - await waitFor(() => expect(editorValues.action.setSnackbarState).toHaveBeenCalled()); + await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalled()); }); wrapper.update(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ message: "Cannot load details for foo:122. Are you sure this is a valid PID?", open: true, severity: "error", diff --git a/client/components/edit/parents/ParentPicker.tsx b/client/components/edit/parents/ParentPicker.tsx index 7fdc70fe0..1fe669926 100644 --- a/client/components/edit/parents/ParentPicker.tsx +++ b/client/components/edit/parents/ParentPicker.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import ObjectLoader from "../ObjectLoader"; import PidPicker from "../PidPicker"; +import { useGlobalContext } from "../../../context/GlobalContext"; import { useEditorContext } from "../../../context/EditorContext"; import { useFetchContext } from "../../../context/FetchContext"; import { getObjectLastChildPositionUrl, getParentUrl } from "../../../util/routes"; @@ -10,13 +11,17 @@ interface ParentPickerProps { } const ParentPicker = ({ pid }: ParentPickerProps): React.ReactElement => { + const { + action: { + setSnackbarState, + }, + } = useGlobalContext(); const { state: { objectDetailsStorage }, action: { clearPidFromChildListStorage, removeFromObjectDetailsStorage, removeFromParentDetailsStorage, - setSnackbarState, }, } = useEditorContext(); const { diff --git a/client/context/EditorContext.test.tsx b/client/context/EditorContext.test.tsx index 792a41e26..9d1fecb6a 100644 --- a/client/context/EditorContext.test.tsx +++ b/client/context/EditorContext.test.tsx @@ -235,33 +235,6 @@ describe("useEditorContext", () => { }); }); - describe("setSnackbarState", () => { - it("sets the snackbar state with text and severity", async () => { - const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); - - expect(result.current.state.snackbarState).toEqual({ - open: false, - message: "", - severity: "info" - }); - - await act(async () => { - await result.current.action.setSnackbarState({ - open: true, - message: "oh no!", - severity: "error" - }); - }); - - expect(result.current.state.snackbarState).toEqual({ - open: true, - message: "oh no!", - severity: "error" - }); - }); - - }); - describe("extractFirstMetadataValue", () => { it("returns a default value if no matching field is found", async () => { const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); diff --git a/client/context/EditorContext.tsx b/client/context/EditorContext.tsx index 7dcb20711..479a618b3 100644 --- a/client/context/EditorContext.tsx +++ b/client/context/EditorContext.tsx @@ -19,12 +19,6 @@ interface ChildrenResultPage { docs?: Record[]; } -interface SnackbarState { - open: boolean, - message: string, - severity: string -} - export interface FedoraDatastream { mimetype?: { allowedType: string; @@ -59,7 +53,6 @@ interface EditorState { datastreamModalState: string | null; parentsModalActivePid: string | null; stateModalActivePid: string | null; - snackbarState: SnackbarState; objectDetailsStorage: Record; parentDetailsStorage: Record>; childListStorage: Record; @@ -88,11 +81,6 @@ const editorContextParams: EditorState = { datastreamModalState: null, parentsModalActivePid: null, stateModalActivePid: null, - snackbarState: { - open: false, - message: "", - severity: "info" - }, objectDetailsStorage: {}, parentDetailsStorage: {}, childListStorage: {}, @@ -128,14 +116,13 @@ const reducerMapping: Record = { SET_DATASTREAM_MODAL_STATE: "datastreamModalState", SET_PARENTS_MODAL_ACTIVE_PID: "parentsModalActivePid", SET_STATE_MODAL_ACTIVE_PID: "stateModalActivePid", - SET_SNACKBAR_STATE: "snackbarState", SET_TOP_LEVEL_PIDS: "topLevelPids", }; /** * Update the shared states of react components. */ -const editorReducer = (state: EditorState, { type, payload }: { type: string, payload: SnackbarState | unknown}) => { +const editorReducer = (state: EditorState, { type, payload }: { type: string, payload: unknown}) => { if (type === "ADD_TO_OBJECT_DETAILS_STORAGE") { const { key, details } = payload as { key: string; details: ObjectDetails }; const objectDetailsStorage = { @@ -243,7 +230,6 @@ export const useEditorContext = () => { vufindUrl, licensesCatalog, modelsCatalog, - snackbarState, objectDetailsStorage, parentDetailsStorage, childListStorage, @@ -481,13 +467,6 @@ export const useEditorContext = () => { }) }; - const setSnackbarState = (snackbarState: SnackbarState) => { - dispatch({ - type: "SET_SNACKBAR_STATE", - payload: snackbarState - }); - }; - const datastreamsCatalog = Object.values(modelsCatalog).reduce((acc: Record, model) => { return { ...acc, @@ -543,7 +522,6 @@ export const useEditorContext = () => { vufindUrl, modelsCatalog, licensesCatalog, - snackbarState, objectDetailsStorage, parentDetailsStorage, childListStorage, @@ -561,7 +539,6 @@ export const useEditorContext = () => { toggleDatastreamModal, toggleParentsModal, toggleStateModal, - setSnackbarState, extractFirstMetadataValue, getChildListStorageKey, loadObjectDetailsIntoStorage, diff --git a/client/hooks/useDatastreamOperation.test.ts b/client/hooks/useDatastreamOperation.test.ts index 90c204888..40dbd4fd6 100644 --- a/client/hooks/useDatastreamOperation.test.ts +++ b/client/hooks/useDatastreamOperation.test.ts @@ -1,6 +1,12 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import useDatastreamOperation from "./useDatastreamOperation"; +const mockUseGlobalwContext = jest.fn(); +jest.mock("../context/GlobalwContext", () => ({ + useGlobalwContext: () => { + return mockUseGlobalwContext(); + }, +})); const mockUseFetchContext = jest.fn(); jest.mock("../context/FetchContext", () => ({ useFetchContext: () => { @@ -15,6 +21,7 @@ jest.mock("../context/EditorContext", () => ({ })); describe("useDatastreamOperation", () => { + let globalValues; let fetchValues; let editorValues; let currentPid; @@ -33,6 +40,11 @@ describe("useDatastreamOperation", () => { }, }, }; + globalValues = { + action: { + setSnackbarState: jest.fn(), + }, + }; fetchValues = { action: { fetchBlob: jest.fn(), @@ -53,6 +65,7 @@ describe("useDatastreamOperation", () => { loadCurrentObjectDetails: jest.fn() }, }; + mockUseGlobalContext.mockReturnValue(globalValues); mockUseFetchContext.mockReturnValue(fetchValues); mockUseEditorContext.mockReturnValue(editorValues); }); @@ -75,7 +88,7 @@ describe("useDatastreamOperation", () => { }) ); expect(editorValues.action.loadCurrentObjectDetails).toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "upload worked", severity: "success", @@ -87,7 +100,7 @@ describe("useDatastreamOperation", () => { await uploadFile({ type: "image/illegaltype", }); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: expect.stringContaining("Illegal mime type"), severity: "error", @@ -103,7 +116,7 @@ describe("useDatastreamOperation", () => { type: "image/png", }); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: expect.stringContaining("Illegal mime type"), severity: "error", @@ -139,7 +152,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "upload agents worked", severity: "success", @@ -160,7 +173,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Upload failure!", @@ -194,7 +207,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "upload DC works", severity: "success", @@ -215,7 +228,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Upload failure!", @@ -245,7 +258,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "upload license works", severity: "success", @@ -266,7 +279,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Upload failure!", @@ -297,7 +310,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "upload works", severity: "success", @@ -319,7 +332,7 @@ describe("useDatastreamOperation", () => { }), { "Content-Type": "application/json"} ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Upload failure!", @@ -343,7 +356,7 @@ describe("useDatastreamOperation", () => { }) ); expect(editorValues.action.loadCurrentObjectDetails).toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Delete success!", @@ -365,7 +378,7 @@ describe("useDatastreamOperation", () => { }) ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Delete failure!", @@ -442,7 +455,7 @@ describe("useDatastreamOperation", () => { "http://localhost:9000/api/edit/object/vudl%3A123/datastream/test1/download" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Incorrect file format", @@ -463,7 +476,7 @@ describe("useDatastreamOperation", () => { "http://localhost:9000/api/edit/object/vudl%3A123/datastream/test1/download" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Incorrect file format", @@ -482,7 +495,7 @@ describe("useDatastreamOperation", () => { "http://localhost:9000/api/edit/object/vudl%3A123/datastream/test1/download" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, message: "Download failure!", @@ -545,7 +558,7 @@ describe("useDatastreamOperation", () => { await viewDatastream(); expect(fetchValues.action.fetchBlob).toHaveBeenCalled(); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith( + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith( expect.objectContaining({ open: true, severity: "error", @@ -583,7 +596,7 @@ describe("useDatastreamOperation", () => { expect(fetchValues.action.fetchText).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/vudl%3A123/datastream/LICENSE/license" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "fetch license failed", severity: "error" @@ -626,7 +639,7 @@ describe("useDatastreamOperation", () => { expect(fetchValues.action.fetchJSON).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/vudl%3A123/datastream/AGENTS/agents" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "fetch agents failed", severity: "error" @@ -664,7 +677,7 @@ describe("useDatastreamOperation", () => { expect(fetchValues.action.fetchJSON).toHaveBeenCalledWith( "http://localhost:9000/api/edit/object/vudl%3A123/datastream/PROCESS-MD/processMetadata" ); - expect(editorValues.action.setSnackbarState).toHaveBeenCalledWith({ + expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ open: true, message: "fetch process metadata failed", severity: "error" diff --git a/client/hooks/useDatastreamOperation.ts b/client/hooks/useDatastreamOperation.ts index befc966a0..4d97febe8 100644 --- a/client/hooks/useDatastreamOperation.ts +++ b/client/hooks/useDatastreamOperation.ts @@ -1,5 +1,6 @@ import { useFetchContext } from "../context/FetchContext"; import { useEditorContext } from "../context/EditorContext"; +import { useGlobalContext } from "../context/GlobalContext"; import { deleteObjectDatastreamUrl, downloadObjectDatastreamUrl, @@ -19,8 +20,11 @@ const useDatastreamOperation = () => { } = useFetchContext(); const { state: { currentPid, activeDatastream, datastreamsCatalog, currentDatastreams, processMetadataDefaults }, - action: { setSnackbarState, toggleDatastreamModal, loadCurrentObjectDetails }, + action: { toggleDatastreamModal, loadCurrentObjectDetails }, } = useEditorContext(); + const { + action: { setSnackbarState }, + } = useGlobalContext(); const isAllowedMimeType = (mimeType) => { if (!datastreamsCatalog[activeDatastream]) { diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 444d31784..b15a3e316 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -1,5 +1,6 @@ /* eslint react/prop-types: 0 */ import React from "react"; +import { GlobalContextProvider } from "../context/GlobalContext"; import { PaginatorContextProvider } from "../context/PaginatorContext"; import { FetchContextProvider } from "../context/FetchContext"; @@ -12,14 +13,16 @@ import LogoutButton from "../components/LogoutButton"; function MyApp({ Component, pageProps }: { Component: React.ReactNode }): React.ReactElement { return ( - -
- -
- - - -
+ + +
+ +
+ + + +
+
); } export default MyApp; From 454a2ccc4f6675cb03b729fedda028037006edcb Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Tue, 7 Feb 2023 16:58:27 -0500 Subject: [PATCH 03/22] refactor: remove unused mock context. --- client/components/edit/EditorSnackbar.test.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/components/edit/EditorSnackbar.test.tsx b/client/components/edit/EditorSnackbar.test.tsx index 7706a4210..a78513197 100644 --- a/client/components/edit/EditorSnackbar.test.tsx +++ b/client/components/edit/EditorSnackbar.test.tsx @@ -10,12 +10,6 @@ jest.mock("../../context/GlobalContext", () => ({ return mockUseGlobalContext(); }, })); -const mockUseEditorContext = jest.fn(); -jest.mock("../../context/EditorContext", () => ({ - useEditorContext: () => { - return mockUseEditorContext(); - }, -})); jest.mock("./children/ChildList", () => () => "ChildList"); describe("EditorSnackbar", () => { From 7dc0c55793faf7281358b8ca015b1964a3d337f4 Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 8 Feb 2023 13:48:23 -0500 Subject: [PATCH 04/22] feat: begin to move modal control to GlobalContext. --- .../edit/datastream/DatastreamModal.tsx | 13 +++-- .../components/edit/parents/ParentsModal.tsx | 13 +++-- client/context/EditorContext.tsx | 6 -- client/context/GlobalContext.tsx | 58 ++++++++++++++++++- client/hooks/useDatastreamOperation.ts | 16 ++--- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/client/components/edit/datastream/DatastreamModal.tsx b/client/components/edit/datastream/DatastreamModal.tsx index 9bb28b0ba..810d42898 100644 --- a/client/components/edit/datastream/DatastreamModal.tsx +++ b/client/components/edit/datastream/DatastreamModal.tsx @@ -4,6 +4,7 @@ import DialogTitle from "@mui/material/DialogTitle"; import Grid from "@mui/material/Grid"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; +import { useGlobalContext } from "../../../context/GlobalContext"; import { useEditorContext } from "../../../context/EditorContext"; import DatastreamUploadModalContent from "./DatastreamUploadModalContent"; import DatastreamDeleteModalContent from "./DatastreamDeleteModalContent"; @@ -23,15 +24,15 @@ const DatastreamModalContent = ({ datastreamModalState }: { datastreamModalState const DatastreamModal = (): React.ReactElement => { const { - state: { datastreamModalState, isDatastreamModalOpen }, - action: { toggleDatastreamModal }, - } = useEditorContext(); + state: { datastreamModalState }, + action: { isModalOpen, openModal, closeModal }, + } = useGlobalContext(); return ( @@ -41,7 +42,7 @@ const DatastreamModal = (): React.ReactElement => { {datastreamModalState} - + diff --git a/client/components/edit/parents/ParentsModal.tsx b/client/components/edit/parents/ParentsModal.tsx index 65e168171..6b04a12ce 100644 --- a/client/components/edit/parents/ParentsModal.tsx +++ b/client/components/edit/parents/ParentsModal.tsx @@ -5,6 +5,7 @@ import DialogTitle from "@mui/material/DialogTitle"; import Grid from "@mui/material/Grid"; import IconButton from "@mui/material/IconButton"; import CloseIcon from "@mui/icons-material/Close"; +import { useGlobalContext } from "../../../context/GlobalContext"; import { useEditorContext } from "../../../context/EditorContext"; import ObjectLoader from "../ObjectLoader"; import ParentList from "./ParentList"; @@ -12,9 +13,13 @@ import ParentPicker from "./ParentPicker"; const ParentsModal = (): React.ReactElement => { const { - state: { isParentsModalOpen, objectDetailsStorage, parentsModalActivePid }, - action: { toggleParentsModal }, + state: { parentsModalActivePid }, + action: { isModalOpen, openModal, closeModal }, + } = useGlobalContext(); + const { + state: { objectDetailsStorage }, } = useEditorContext(); + const loaded = Object.prototype.hasOwnProperty.call(objectDetailsStorage, parentsModalActivePid); const contents = ( @@ -26,14 +31,14 @@ const ParentsModal = (): React.ReactElement => { ); return ( - + Parents Editor ({parentsModalActivePid}) - + diff --git a/client/context/EditorContext.tsx b/client/context/EditorContext.tsx index 479a618b3..9c68e3716 100644 --- a/client/context/EditorContext.tsx +++ b/client/context/EditorContext.tsx @@ -216,9 +216,6 @@ export const useEditorContext = () => { currentAgents, currentPid, activeDatastream, - isDatastreamModalOpen, - isParentsModalOpen, - isStateModalOpen, datastreamModalState, parentsModalActivePid, stateModalActivePid, @@ -506,9 +503,6 @@ export const useEditorContext = () => { currentPid, currentDatastreams, activeDatastream, - isDatastreamModalOpen, - isParentsModalOpen, - isStateModalOpen, datastreamModalState, parentsModalActivePid, stateModalActivePid, diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index d090cfe72..2bfb26895 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -1,5 +1,11 @@ import React, { createContext, useContext, useReducer } from "react"; +export enum ModalName { + DatastreamModal = "datastream", + ParentModal = "parents", + StateModal = "state", +}; + interface SnackbarState { open: boolean, message: string, @@ -15,6 +21,9 @@ interface GlobalState { * specifically a way to make api requests. */ const globalContextParams: GlobalState = { + // Modal control + openModalState: new Set(), + // Snackbar snackbarState: { open: false, message: "", @@ -23,6 +32,7 @@ const globalContextParams: GlobalState = { }; const reducerMapping: Record = { + // Snackbar SET_SNACKBAR_STATE: "snackbarState", }; @@ -30,9 +40,16 @@ const reducerMapping: Record = { * Update the shared states of react components. */ const globalReducer = (state: GlobalState, { type, payload }: { type: string, payload: unknown}) => { - if(Object.keys(reducerMapping).includes(type)){ - console.log(type, payload); + if (type == "OPEN_MODAL") { + state.openModalState.add(payload); + return state; + } + if (type == "CLOSE_MODAL") { + state.openModalState.remove(payload); + return state; + } + if (Object.keys(reducerMapping).includes(type)){ return { ...state, [reducerMapping[type]]: payload @@ -54,11 +71,41 @@ export const GlobalContextProvider = ({ children }) => { export const useGlobalContext = () => { const { state: { + // Modal control + openModalState, + // Snackbar snackbarState, }, dispatch, } = useContext(GlobalContext); + // Modal control + + const isModalOpen = (modal: ModalName) => { + return openModalState.has(modal); + }; + const openModal = (modal: ModalName) => { + dispatch({ + type: "OPEN_MODAL", + payload: modal + }); + }; + const closeModal = (modal: ModalName) => { + dispatch({ + type: "CLOSE_MODAL", + payload: modal + }); + }; + const toggleModal = (modal: ModalName) => { + if (isModalOpen(modal)) { + closeModal(modal); + } else { + openModal(modal); + } + }; + + // Snackbar + const setSnackbarState = (snackbarState: SnackbarState) => { dispatch({ type: "SET_SNACKBAR_STATE", @@ -68,9 +115,16 @@ export const useGlobalContext = () => { return { state: { + // Snackbar snackbarState, }, action: { + // Modal control + isModalOpen, + openModal, + closeModal, + toggleModal, + // Snackbar setSnackbarState, }, }; diff --git a/client/hooks/useDatastreamOperation.ts b/client/hooks/useDatastreamOperation.ts index 4d97febe8..d04395429 100644 --- a/client/hooks/useDatastreamOperation.ts +++ b/client/hooks/useDatastreamOperation.ts @@ -20,10 +20,10 @@ const useDatastreamOperation = () => { } = useFetchContext(); const { state: { currentPid, activeDatastream, datastreamsCatalog, currentDatastreams, processMetadataDefaults }, - action: { toggleDatastreamModal, loadCurrentObjectDetails }, + action: { loadCurrentObjectDetails }, } = useEditorContext(); const { - action: { setSnackbarState }, + action: { setSnackbarState, toggleModal }, } = useGlobalContext(); const isAllowedMimeType = (mimeType) => { @@ -55,14 +55,14 @@ const useDatastreamOperation = () => { message: text, severity: "success", }); - toggleDatastreamModal(); + toggleModal(); } catch (err) { setSnackbarState({ open: true, message: err.message, severity: "error", }); - toggleDatastreamModal(); + toggleModal(); } }; @@ -110,7 +110,7 @@ const useDatastreamOperation = () => { severity: "error", }); } - toggleDatastreamModal(); + toggleModal(); }; const uploadLicense = async (licenseKey) => { @@ -134,7 +134,7 @@ const useDatastreamOperation = () => { severity: "error", }); } - toggleDatastreamModal(); + toggleModal(); }; const uploadProcessMetadata = async (processMetadata) => { @@ -158,7 +158,7 @@ const useDatastreamOperation = () => { severity: "error", }); } - toggleDatastreamModal(); + toggleModal(); }; const deleteDatastream = async () => { @@ -179,7 +179,7 @@ const useDatastreamOperation = () => { severity: "error", }); } - toggleDatastreamModal(); + toggleModal(); }; const downloadDatastream = async (datastream) => { From c2463eb11fb94180c0c7b8db02bb914f3c0ececa Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 8 Feb 2023 15:19:52 -0500 Subject: [PATCH 05/22] feat: add theme switcher to GlobalContext. --- client/components/ThemeMenu.tsx | 35 ++++++++++++++++ client/context/GlobalContext.tsx | 70 ++++++++++++++++++++++++++++++++ client/pages/_app.tsx | 8 +++- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 client/components/ThemeMenu.tsx diff --git a/client/components/ThemeMenu.tsx b/client/components/ThemeMenu.tsx new file mode 100644 index 000000000..9dc7687cc --- /dev/null +++ b/client/components/ThemeMenu.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; + +import { useGlobalContext } from "../context/GlobalContext"; + +export default function ThemeMenu() { + const { + state: { userTheme }, + action: { setUserTheme }, + } = useGlobalContext(); + + function changeTheme(e) { + setUserTheme(e.target.value); + } + + return ( + + Theme + + + ); +} diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index 2bfb26895..88a3fd9b1 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -12,6 +12,12 @@ interface SnackbarState { severity: string } +export enum ThemeOption { + system = "system", + light = "light", + dark = "dark", +}; + interface GlobalState { snackbarState: SnackbarState; } @@ -29,11 +35,15 @@ const globalContextParams: GlobalState = { message: "", severity: "info", }, + // User theme + userTheme: localLoadUserTheme(), }; const reducerMapping: Record = { // Snackbar SET_SNACKBAR_STATE: "snackbarState", + // User theme + SET_USER_THEME: "userTheme", }; /** @@ -75,6 +85,8 @@ export const useGlobalContext = () => { openModalState, // Snackbar snackbarState, + // User theme + userTheme, }, dispatch, } = useContext(GlobalContext); @@ -113,10 +125,23 @@ export const useGlobalContext = () => { }); }; + // User theme + + const setUserTheme = (userTheme: ThemeOption) => { + localSaveUserTheme(userTheme); + applyUserThemeToBody(userTheme); + dispatch({ + type: "SET_USER_THEME", + payload: userTheme, + }); + }; + return { state: { // Snackbar snackbarState, + // User theme + userTheme, }, action: { // Modal control @@ -126,6 +151,8 @@ export const useGlobalContext = () => { toggleModal, // Snackbar setSnackbarState, + // User theme + setUserTheme, }, }; } @@ -134,3 +161,46 @@ export default { GlobalContextProvider, useGlobalContext } + +/* User Theme */ + +// Get system theme from CSS media queries +function systemTheme() { + if (typeof window != "undefined") { + if (window.matchMedia("(prefers-color-scheme)").mediaTheme == "not all") { + return "light" + } + + const isDark = !window.matchMedia("(prefers-color-scheme: light)").matches; + return isDark ? "dark" : "light"; + } + + return "light"; +} + +function applyUserThemeToBody(userTheme) { + if (typeof window != "undefined") { + document.body.setAttribute( + "color-scheme", + userTheme == "system" ? systemTheme() : userTheme + ); + } +} + +// Get page theme from localStorage +function localSaveUserTheme(mediaTheme) { + if (typeof window != "undefined") { + localStorage.setItem("vudl-theme", mediaTheme); + } +} + +// Save page theme from localStorage +function localLoadUserTheme() { + if (typeof window != "undefined") { + let mediaTheme = localStorage.getItem("vudl-theme") ?? "system"; + + applyUserThemeToBody(mediaTheme); + + return mediaTheme; + } +} diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index b15a3e316..09dce2e0c 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -9,15 +9,21 @@ import "../styles/application.css"; import "../styles/client.css"; import "../styles/justgrid.css"; import "@fortawesome/fontawesome-free/css/all.min.css"; + import LogoutButton from "../components/LogoutButton"; +import ThemeMenu from "../components/ThemeMenu"; function MyApp({ Component, pageProps }: { Component: React.ReactNode }): React.ReactElement { return ( - +
+
+
+ + From d5a34332d47b2fb0f134cb5acbe70941f94a3bb3 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Thu, 9 Feb 2023 16:29:08 -0500 Subject: [PATCH 06/22] fix: StateModal and modal closing behavior. --- client/components/edit/ObjectStatus.tsx | 6 +++- client/components/edit/StateModal.tsx | 20 +++++++---- .../components/edit/parents/ParentsModal.tsx | 14 +++++--- client/context/EditorContext.tsx | 30 ++++------------ client/context/GlobalContext.tsx | 36 +++++++++---------- 5 files changed, 49 insertions(+), 57 deletions(-) diff --git a/client/components/edit/ObjectStatus.tsx b/client/components/edit/ObjectStatus.tsx index 6e230ea7d..bd560b9bc 100644 --- a/client/components/edit/ObjectStatus.tsx +++ b/client/components/edit/ObjectStatus.tsx @@ -1,5 +1,6 @@ import styles from "./ObjectStatus.module.css"; import React from "react"; +import { useGlobalContext } from "../../context/GlobalContext"; import { useEditorContext } from "../../context/EditorContext"; import ObjectLoader from "./ObjectLoader"; @@ -8,6 +9,9 @@ export interface ObjectStatusProps { } export const ObjectStatus = ({ pid }: ObjectStatusProps): React.ReactElement => { + const { + action: { toggleModal }, + } = useGlobalContext(); const { state: { objectDetailsStorage }, action: { setStateModalActivePid, toggleStateModal }, @@ -18,7 +22,7 @@ export const ObjectStatus = ({ pid }: ObjectStatusProps): React.ReactElement => const stateTxt = details.state ?? "Unknown"; const clickAction = () => { setStateModalActivePid(pid); - toggleStateModal(); + toggleModal("state"); }; const stateMsg = loaded ? ( ; }; diff --git a/client/components/edit/parents/ParentsModal.tsx b/client/components/edit/parents/ParentsModal.tsx index 806e17da1..9f31783e6 100644 --- a/client/components/edit/parents/ParentsModal.tsx +++ b/client/components/edit/parents/ParentsModal.tsx @@ -20,7 +20,7 @@ const ParentsModal = (): React.ReactElement => { } = useEditorContext(); function closeParentModal() { - closeModal("parent"); + closeModal("parents"); } const loaded = Object.prototype.hasOwnProperty.call(objectDetailsStorage, parentsModalActivePid); From b70f0e45b2b9b2c5a23a8a5ef9ff366d6a6d76b4 Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Tue, 23 May 2023 14:35:09 -0400 Subject: [PATCH 13/22] fix: explicitly handle undefined. --- client/context/GlobalContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index b7d2ae582..04edaed86 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -100,7 +100,7 @@ export const useGlobalContext = () => { }); }; const toggleModal = (modal: string) => { - if (modalOpenStates[modal]) { + if (modalOpenStates[modal] ?? false) { closeModal(modal); } else { openModal(modal); From 70c484efeb9777e97c5f02f10ee7d6f17b9017bb Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 23 May 2023 15:50:46 -0400 Subject: [PATCH 14/22] fix: modal integration updates --- client/components/edit/datastream/DatastreamModal.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/components/edit/datastream/DatastreamModal.tsx b/client/components/edit/datastream/DatastreamModal.tsx index a1cea1dd6..db92b513d 100644 --- a/client/components/edit/datastream/DatastreamModal.tsx +++ b/client/components/edit/datastream/DatastreamModal.tsx @@ -24,15 +24,17 @@ const DatastreamModalContent = ({ datastreamModalState }: { datastreamModalState const DatastreamModal = (): React.ReactElement => { const { - state: { isModalOpen, datastreamModalState }, - action: { closeModal }, + state: { datastreamModalState }, + } = useEditorContext(); + const { + action: { closeModal, isModalOpen }, } = useGlobalContext(); return ( closeModal("datastream")} fullWidth={true} maxWidth={"lg"} > @@ -42,7 +44,7 @@ const DatastreamModal = (): React.ReactElement => { {datastreamModalState}
- + closeModal("datastream")}> From af162ecaae1a0107e621ae4df44b3e621d49c0f5 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 23 May 2023 15:50:55 -0400 Subject: [PATCH 15/22] Fix broken tests. --- .../edit/EditParentsButton.test.tsx | 17 ++- client/components/edit/ObjectStatus.test.tsx | 13 +- client/components/edit/StateModal.test.tsx | 22 ++-- .../__snapshots__/ObjectStatus.test.tsx.snap | 84 ++++++------ .../__snapshots__/StateModal.test.tsx.snap | 120 +++++++++--------- .../edit/datastream/DatastreamModal.test.tsx | 25 +++- .../DatastreamModal.test.tsx.snap | 4 +- .../edit/parents/ParentsModal.test.tsx | 23 +++- .../__snapshots__/ParentsModal.test.tsx.snap | 12 +- client/context/EditorContext.test.tsx | 42 ------ client/context/GlobalContext.test.tsx | 26 ++++ 11 files changed, 214 insertions(+), 174 deletions(-) diff --git a/client/components/edit/EditParentsButton.test.tsx b/client/components/edit/EditParentsButton.test.tsx index c513aa367..a5997313a 100644 --- a/client/components/edit/EditParentsButton.test.tsx +++ b/client/components/edit/EditParentsButton.test.tsx @@ -11,18 +11,31 @@ jest.mock("../../context/EditorContext", () => ({ }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); + describe("EditParentsButton", () => { let editorValues; + let globalValues; let pid: string; beforeEach(() => { pid = "foo:123"; editorValues = { action: { setParentsModalActivePid: jest.fn(), - toggleParentsModal: jest.fn(), }, }; mockUseEditorContext.mockReturnValue(editorValues); + globalValues = { + action: { + openModal: jest.fn(), + } + } + mockUseGlobalContext.mockReturnValue(globalValues); }); it("renders", () => { @@ -35,7 +48,7 @@ describe("EditParentsButton", () => { component.find("button").simulate("click"); expect(editorValues.action.setParentsModalActivePid).toHaveBeenCalledWith(pid); - expect(editorValues.action.toggleParentsModal).toHaveBeenCalled(); + expect(globalValues.action.openModal).toHaveBeenCalledWith("parents"); component.unmount(); }); }); diff --git a/client/components/edit/ObjectStatus.test.tsx b/client/components/edit/ObjectStatus.test.tsx index 8ef63fcbd..87d5af885 100644 --- a/client/components/edit/ObjectStatus.test.tsx +++ b/client/components/edit/ObjectStatus.test.tsx @@ -6,14 +6,17 @@ import toJson from "enzyme-to-json"; import { ObjectStatusProps, ObjectStatus } from "./ObjectStatus"; import { EditorContextProvider, ObjectDetails } from "../../context/EditorContext"; import { FetchContextProvider } from "../../context/FetchContext"; +import { GlobalContextProvider } from "../../context/GlobalContext"; function getMountedObjectStatusComponent(props: ObjectStatusProps) { return mount( - - - - - + + + + + + + ); } diff --git a/client/components/edit/StateModal.test.tsx b/client/components/edit/StateModal.test.tsx index e5bceaeca..ca2287758 100644 --- a/client/components/edit/StateModal.test.tsx +++ b/client/components/edit/StateModal.test.tsx @@ -37,18 +37,19 @@ describe("StateModal", () => { beforeEach(() => { globalValues = { action: { + closeModal: jest.fn(), + isModalOpen: jest.fn(), + openModal: jest.fn(), setSnackbarState: jest.fn(), }, }; editorValues = { state: { stateModalActivePid: pid, - isStateModalOpen: true, objectDetailsStorage: {}, }, action: { removeFromObjectDetailsStorage: jest.fn(), - toggleStateModal: jest.fn(), }, }; fetchContextValues = { @@ -60,10 +61,15 @@ describe("StateModal", () => { mockUseGlobalContext.mockReturnValue(globalValues); mockUseEditorContext.mockReturnValue(editorValues); mockUseFetchContext.mockReturnValue(fetchContextValues); + globalValues.action.isModalOpen.mockReturnValue(true); + }); + + afterEach(() => { + jest.resetAllMocks(); }); it("renders correctly when closed", () => { - editorValues.state.isStateModalOpen = false; + globalValues.action.isModalOpen.mockReturnValue(false); const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -128,7 +134,7 @@ describe("StateModal", () => { severity: "success", }); expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenCalledWith(pid); - expect(editorValues.action.toggleStateModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("state"); }); it("does not save when nothing changes", async () => { @@ -153,7 +159,7 @@ describe("StateModal", () => { ); expect(fetchContextValues.action.fetchText).not.toHaveBeenCalled(); expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); - expect(editorValues.action.toggleStateModal).not.toHaveBeenCalled(); + expect(globalValues.action.openModal).not.toHaveBeenCalled(); }); it("handles save failure gracefully", async () => { @@ -187,7 +193,7 @@ describe("StateModal", () => { severity: "error", }); expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); - expect(editorValues.action.toggleStateModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("state"); }); it("handles child save failure gracefully", async () => { @@ -227,7 +233,7 @@ describe("StateModal", () => { severity: "error", }); expect(editorValues.action.removeFromObjectDetailsStorage).not.toHaveBeenCalled(); - expect(editorValues.action.toggleStateModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("state"); }); it("updates children correctly", async () => { @@ -274,6 +280,6 @@ describe("StateModal", () => { expect(fetchContextValues.action.fetchText).toHaveBeenCalledTimes(2); expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenNthCalledWith(1, "foo:125"); expect(editorValues.action.removeFromObjectDetailsStorage).toHaveBeenNthCalledWith(2, pid); - expect(editorValues.action.toggleStateModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("state"); }); }); diff --git a/client/components/edit/__snapshots__/ObjectStatus.test.tsx.snap b/client/components/edit/__snapshots__/ObjectStatus.test.tsx.snap index b4606fac2..30df9fbd4 100644 --- a/client/components/edit/__snapshots__/ObjectStatus.test.tsx.snap +++ b/client/components/edit/__snapshots__/ObjectStatus.test.tsx.snap @@ -1,53 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ObjectStatus defaults to unknown state 1`] = ` - - - - + + + - - - - + + ◉ + +   + Unknown + + + + + `; exports[`ObjectStatus displays the state found in the response 1`] = ` - - - - + + + - - - - + + ◉ + +   + Inactive + + + + + `; diff --git a/client/components/edit/__snapshots__/StateModal.test.tsx.snap b/client/components/edit/__snapshots__/StateModal.test.tsx.snap index 949aed387..42ccee116 100644 --- a/client/components/edit/__snapshots__/StateModal.test.tsx.snap +++ b/client/components/edit/__snapshots__/StateModal.test.tsx.snap @@ -5,7 +5,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` @@ -161,7 +161,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -1757,7 +1757,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` closeAfterTransition={true} disableEscapeKeyDown={false} onClick={[Function]} - onClose={[MockFunction]} + onClose={[Function]} open={true} > @@ -2312,7 +2312,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -2359,7 +2359,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -2442,7 +2442,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -2474,7 +2474,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -2591,7 +2591,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "hideBackdrop": false, "keepMounted": false, "onClick": [Function], - "onClose": [MockFunction], + "onClose": [Function], "onTransitionEnter": [Function], "onTransitionExited": [Function], "open": true, @@ -7701,7 +7701,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -7784,7 +7784,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -9395,7 +9395,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -9478,7 +9478,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -11067,7 +11067,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -12727,7 +12727,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -14310,7 +14310,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -15918,7 +15918,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > @@ -19083,7 +19083,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` Object { "children": , @@ -20653,14 +20653,14 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` > , @@ -20669,7 +20669,7 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` "disableFocusRipple": false, "disabled": false, "edge": false, - "onClick": [MockFunction], + "onClick": [Function], "size": "medium", } } @@ -22222,14 +22222,14 @@ exports[`StateModal renders correctly for a loaded object with children 1`] = ` className="MuiIconButton-root MuiIconButton-sizeMedium closeButton css-da3c9u-MuiIconButton-root" disabled={false} focusRipple={true} - onClick={[MockFunction]} + onClick={[Function]} > @@ -111274,7 +111274,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -112870,7 +112870,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = closeAfterTransition={true} disableEscapeKeyDown={false} onClick={[Function]} - onClose={[MockFunction]} + onClose={[Function]} open={true} > @@ -113667,7 +113667,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -113714,7 +113714,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -113784,7 +113784,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -113816,7 +113816,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -113920,7 +113920,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "hideBackdrop": false, "keepMounted": false, "onClick": [Function], - "onClose": [MockFunction], + "onClose": [Function], "onTransitionEnter": [Function], "onTransitionExited": [Function], "open": true, @@ -118992,7 +118992,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -119062,7 +119062,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -120673,7 +120673,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -120743,7 +120743,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "fullScreen": false, "fullWidth": true, "maxWidth": "sm", - "onClose": [MockFunction], + "onClose": [Function], "open": true, "scroll": "paper", } @@ -122332,7 +122332,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -123979,7 +123979,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -125562,7 +125562,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -127170,7 +127170,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > @@ -130335,7 +130335,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = Object { "children": , @@ -131905,14 +131905,14 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = > , @@ -131921,7 +131921,7 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = "disableFocusRipple": false, "disabled": false, "edge": false, - "onClick": [MockFunction], + "onClick": [Function], "size": "medium", } } @@ -133474,14 +133474,14 @@ exports[`StateModal renders correctly for a loaded object without children 1`] = className="MuiIconButton-root MuiIconButton-sizeMedium closeButton css-da3c9u-MuiIconButton-root" disabled={false} focusRipple={true} - onClick={[MockFunction]} + onClick={[Function]} > @@ -208031,7 +208031,7 @@ exports[`StateModal renders correctly for a pending object 1`] = ` > @@ -208106,7 +208106,7 @@ exports[`StateModal renders correctly when closed 1`] = ` @@ -208127,7 +208127,7 @@ exports[`StateModal renders correctly when closed 1`] = ` > diff --git a/client/components/edit/datastream/DatastreamModal.test.tsx b/client/components/edit/datastream/DatastreamModal.test.tsx index 2856e0157..e57410b83 100644 --- a/client/components/edit/datastream/DatastreamModal.test.tsx +++ b/client/components/edit/datastream/DatastreamModal.test.tsx @@ -10,34 +10,47 @@ jest.mock("../../../context/EditorContext", () => ({ return mockUseEditorContext(); }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); jest.mock("./DatastreamUploadModalContent", () => () => "DatastreamUploadModalContent"); jest.mock("./DatastreamDeleteModalContent", () => () => "DatastreamDeleteModalContent"); describe("DatastreamModal", () => { let editorValues; + let globalValues; beforeEach(() => { editorValues = { state: { datastreamModalState: "", - isDatastreamModalOpen: true, - }, - action: { - toggleDatastreamModal: jest.fn(), }, }; mockUseEditorContext.mockReturnValue(editorValues); + globalValues = { + action: { + closeModal: jest.fn(), + isModalOpen: jest.fn(), + } + } + mockUseGlobalContext.mockReturnValue(globalValues); + globalValues.action.isModalOpen.mockReturnValue(true); }); it("renders", () => { const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("datastream"); }); it("toggles the datastreamModal", () => { const component = mount(); component.find("button").simulate("click"); - expect(editorValues.action.toggleDatastreamModal).toHaveBeenCalled(); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("datastream"); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); component.unmount(); }); @@ -47,6 +60,7 @@ describe("DatastreamModal", () => { const component = mount(); expect(component.text()).toContain("DatastreamDeleteModalContent"); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("datastream"); }); it("switches to the upload modal content", () => { @@ -55,5 +69,6 @@ describe("DatastreamModal", () => { const component = mount(); expect(component.text()).toContain("DatastreamUploadModalContent"); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("datastream"); }); }); diff --git a/client/components/edit/datastream/__snapshots__/DatastreamModal.test.tsx.snap b/client/components/edit/datastream/__snapshots__/DatastreamModal.test.tsx.snap index efd992f1d..4a611a814 100644 --- a/client/components/edit/datastream/__snapshots__/DatastreamModal.test.tsx.snap +++ b/client/components/edit/datastream/__snapshots__/DatastreamModal.test.tsx.snap @@ -5,7 +5,7 @@ exports[`DatastreamModal renders 1`] = ` className="datastreamModal" fullWidth={true} maxWidth="lg" - onClose={[MockFunction]} + onClose={[Function]} open={true} > @@ -24,7 +24,7 @@ exports[`DatastreamModal renders 1`] = ` > diff --git a/client/components/edit/parents/ParentsModal.test.tsx b/client/components/edit/parents/ParentsModal.test.tsx index 8e8d828ba..5e3b7011a 100644 --- a/client/components/edit/parents/ParentsModal.test.tsx +++ b/client/components/edit/parents/ParentsModal.test.tsx @@ -10,12 +10,19 @@ jest.mock("../../../context/EditorContext", () => ({ return mockUseEditorContext(); }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); jest.mock("../ObjectLoader", () => () => "ObjectLoader"); jest.mock("./ParentList", () => () => "ParentList"); jest.mock("./ParentPicker", () => () => "ParentPicker"); describe("ParentsModal", () => { let editorValues; + let globalValues; let pid: string; beforeEach(() => { pid = "foo:123"; @@ -25,35 +32,43 @@ describe("ParentsModal", () => { isParentsModalOpen: true, parentsModalActivePid: pid, }, - action: { - toggleParentsModal: jest.fn(), - }, }; mockUseEditorContext.mockReturnValue(editorValues); + globalValues = { + action: { + closeModal: jest.fn(), + isModalOpen: jest.fn(), + } + } + mockUseGlobalContext.mockReturnValue(globalValues); + globalValues.action.isModalOpen.mockReturnValue(true); }); it("renders correctly for a non-loaded PID", () => { const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("parents"); }); it("renders correctly for a loaded PID", () => { editorValues.state.objectDetailsStorage[pid] = { pid }; const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("parents"); }); it("renders correctly when PID is unset", () => { editorValues.state.parentsModalActivePid = null; const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("parents"); }); it("toggles the modal", () => { const component = mount(); component.find("button").simulate("click"); - expect(editorValues.action.toggleParentsModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("parents"); component.unmount(); }); }); diff --git a/client/components/edit/parents/__snapshots__/ParentsModal.test.tsx.snap b/client/components/edit/parents/__snapshots__/ParentsModal.test.tsx.snap index b5ced4e9c..429642c8f 100644 --- a/client/components/edit/parents/__snapshots__/ParentsModal.test.tsx.snap +++ b/client/components/edit/parents/__snapshots__/ParentsModal.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ParentsModal renders correctly for a loaded PID 1`] = ` @@ -25,7 +25,7 @@ exports[`ParentsModal renders correctly for a loaded PID 1`] = ` > @@ -56,7 +56,7 @@ exports[`ParentsModal renders correctly for a non-loaded PID 1`] = ` @@ -77,7 +77,7 @@ exports[`ParentsModal renders correctly for a non-loaded PID 1`] = ` > @@ -96,7 +96,7 @@ exports[`ParentsModal renders correctly when PID is unset 1`] = ` @@ -116,7 +116,7 @@ exports[`ParentsModal renders correctly when PID is unset 1`] = ` > diff --git a/client/context/EditorContext.test.tsx b/client/context/EditorContext.test.tsx index 9d1fecb6a..4e8e473cc 100644 --- a/client/context/EditorContext.test.tsx +++ b/client/context/EditorContext.test.tsx @@ -53,34 +53,6 @@ describe("useEditorContext", () => { }); }); - describe("toggleDatastreamModal", () => { - it("toggles the modal", async () => { - const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); - - expect(result.current.state.isDatastreamModalOpen).toBeFalsy(); - - await act(async () => { - await result.current.action.toggleDatastreamModal(); - }); - - expect(result.current.state.isDatastreamModalOpen).toBeTruthy(); - }); - }); - - describe("toggleParentsModal", () => { - it("toggles the modal", async () => { - const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); - - expect(result.current.state.isParentsModalOpen).toBeFalsy(); - - await act(async () => { - await result.current.action.toggleParentsModal(); - }); - - expect(result.current.state.isParentsModalOpen).toBeTruthy(); - }); - }); - describe("setDatastreamModalState", () => { it("sets the modal state", async () => { const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); @@ -355,20 +327,6 @@ describe("useEditorContext", () => { }); }); - describe("toggleStateModal ", () => { - it("toggles the modal", async () => { - const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); - - expect(result.current.state.isStateModalOpen).toBeFalsy(); - - await act(async () => { - await result.current.action.toggleStateModal(); - }); - - expect(result.current.state.isStateModalOpen).toBeTruthy(); - }); - }); - describe("setStateModalActivePid", () => { it("sets the modal's active pid value", async () => { const { result } = await renderHook(() => useEditorContext(), { wrapper: EditorContextProvider }); diff --git a/client/context/GlobalContext.test.tsx b/client/context/GlobalContext.test.tsx index a03768a5b..3a6512a85 100644 --- a/client/context/GlobalContext.test.tsx +++ b/client/context/GlobalContext.test.tsx @@ -28,4 +28,30 @@ describe("useGlobalContext", () => { }); }); }); + + describe("toggleModal", () => { + it("toggles the modal", async () => { + const { result } = await renderHook(() => useGlobalContext(), { wrapper: GlobalContextProvider }); + + expect(result.current.action.isModalOpen("foo")).toEqual(false); + + await act(async () => { + await result.current.action.toggleModal("foo"); + }); + + expect(result.current.action.isModalOpen("foo")).toEqual(true); + + await act(async () => { + await result.current.action.toggleModal("foo"); + }); + + expect(result.current.action.isModalOpen("foo")).toEqual(false); + + await act(async () => { + await result.current.action.toggleModal("foo"); + }); + + expect(result.current.action.isModalOpen("foo")).toEqual(true); + }); + }); }); From 3d42d106b9920575a843967d23fd6c28f12ce63f Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 23 May 2023 15:53:36 -0400 Subject: [PATCH 16/22] lint. --- client/components/edit/EditParentsButton.test.tsx | 4 ++-- .../edit/datastream/DatastreamModal.test.tsx | 4 ++-- client/components/edit/parents/ParentList.tsx | 4 +--- client/components/edit/parents/ParentPicker.tsx | 10 ++-------- client/components/edit/parents/ParentsModal.test.tsx | 4 ++-- client/components/edit/parents/ParentsModal.tsx | 2 +- client/pages/_app.tsx | 2 +- 7 files changed, 11 insertions(+), 19 deletions(-) diff --git a/client/components/edit/EditParentsButton.test.tsx b/client/components/edit/EditParentsButton.test.tsx index a5997313a..80bf61b75 100644 --- a/client/components/edit/EditParentsButton.test.tsx +++ b/client/components/edit/EditParentsButton.test.tsx @@ -33,8 +33,8 @@ describe("EditParentsButton", () => { globalValues = { action: { openModal: jest.fn(), - } - } + }, + }; mockUseGlobalContext.mockReturnValue(globalValues); }); diff --git a/client/components/edit/datastream/DatastreamModal.test.tsx b/client/components/edit/datastream/DatastreamModal.test.tsx index e57410b83..020ff4f42 100644 --- a/client/components/edit/datastream/DatastreamModal.test.tsx +++ b/client/components/edit/datastream/DatastreamModal.test.tsx @@ -33,8 +33,8 @@ describe("DatastreamModal", () => { action: { closeModal: jest.fn(), isModalOpen: jest.fn(), - } - } + }, + }; mockUseGlobalContext.mockReturnValue(globalValues); globalValues.action.isModalOpen.mockReturnValue(true); }); diff --git a/client/components/edit/parents/ParentList.tsx b/client/components/edit/parents/ParentList.tsx index 6c874aa7e..a0b536108 100644 --- a/client/components/edit/parents/ParentList.tsx +++ b/client/components/edit/parents/ParentList.tsx @@ -12,9 +12,7 @@ export interface ParentListProps { const ParentList = ({ pid, initiallyShallow = true }: ParentListProps): React.ReactElement => { const { - action: { - setSnackbarState, - }, + action: { setSnackbarState }, } = useGlobalContext(); const { state: { parentDetailsStorage }, diff --git a/client/components/edit/parents/ParentPicker.tsx b/client/components/edit/parents/ParentPicker.tsx index 1fe669926..c4f35ff3c 100644 --- a/client/components/edit/parents/ParentPicker.tsx +++ b/client/components/edit/parents/ParentPicker.tsx @@ -12,17 +12,11 @@ interface ParentPickerProps { const ParentPicker = ({ pid }: ParentPickerProps): React.ReactElement => { const { - action: { - setSnackbarState, - }, + action: { setSnackbarState }, } = useGlobalContext(); const { state: { objectDetailsStorage }, - action: { - clearPidFromChildListStorage, - removeFromObjectDetailsStorage, - removeFromParentDetailsStorage, - }, + action: { clearPidFromChildListStorage, removeFromObjectDetailsStorage, removeFromParentDetailsStorage }, } = useEditorContext(); const { action: { fetchText }, diff --git a/client/components/edit/parents/ParentsModal.test.tsx b/client/components/edit/parents/ParentsModal.test.tsx index 5e3b7011a..2f337570c 100644 --- a/client/components/edit/parents/ParentsModal.test.tsx +++ b/client/components/edit/parents/ParentsModal.test.tsx @@ -38,8 +38,8 @@ describe("ParentsModal", () => { action: { closeModal: jest.fn(), isModalOpen: jest.fn(), - } - } + }, + }; mockUseGlobalContext.mockReturnValue(globalValues); globalValues.action.isModalOpen.mockReturnValue(true); }); diff --git a/client/components/edit/parents/ParentsModal.tsx b/client/components/edit/parents/ParentsModal.tsx index 9f31783e6..018786a1f 100644 --- a/client/components/edit/parents/ParentsModal.tsx +++ b/client/components/edit/parents/ParentsModal.tsx @@ -13,7 +13,7 @@ import ParentPicker from "./ParentPicker"; const ParentsModal = (): React.ReactElement => { const { - action: { isModalOpen, openModal, closeModal }, + action: { isModalOpen, closeModal }, } = useGlobalContext(); const { state: { parentsModalActivePid, objectDetailsStorage }, diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index d4e073e05..b15a3e316 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -25,4 +25,4 @@ function MyApp({ Component, pageProps }: { Component: React.ReactNode }): React. ); } -export default MyApp; \ No newline at end of file +export default MyApp; From d19afc76eb4ba544378be4fb5fe300b28a79ad1b Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Mon, 1 Jul 2024 09:24:31 -0400 Subject: [PATCH 17/22] Remove references to deleted toggle function; improve tests. --- client/components/edit/ObjectStatus.test.tsx | 5 ++-- .../DatastreamAgentsContent.test.tsx | 20 ++++++++++++--- .../datastream/DatastreamAgentsContent.tsx | 10 +++++--- .../DatastreamControlButton.test.tsx | 16 ++++++++++-- .../datastream/DatastreamControlButton.tsx | 8 ++++-- .../DatastreamDeleteModalContent.test.tsx | 25 ++++++------------- .../DatastreamDeleteModalContent.tsx | 8 +++--- .../DatastreamDublinCoreContent.test.tsx | 20 ++++++++++++++- .../DatastreamDublinCoreContent.tsx | 7 ++++-- .../DatastreamLicenseContent.test.tsx | 21 +++++++++++++++- .../datastream/DatastreamLicenseContent.tsx | 7 ++++-- .../DatastreamProcessMetadataContent.test.tsx | 21 ++++++++++------ .../DatastreamProcessMetadataContent.tsx | 8 +++--- ...DatastreamDeleteModalContent.test.tsx.snap | 2 +- .../DatastreamDublinCoreContent.test.tsx.snap | 1 + .../DatastreamLicenseContent.test.tsx.snap | 2 +- ...streamProcessMetadataContent.test.tsx.snap | 4 +-- 17 files changed, 129 insertions(+), 56 deletions(-) diff --git a/client/components/edit/ObjectStatus.test.tsx b/client/components/edit/ObjectStatus.test.tsx index 3d0a7e9d3..66e8ce92a 100644 --- a/client/components/edit/ObjectStatus.test.tsx +++ b/client/components/edit/ObjectStatus.test.tsx @@ -14,8 +14,9 @@ function getMountedObjectStatusComponent(props: ObjectStatusProps) { - , - + + , + , ); } diff --git a/client/components/edit/datastream/DatastreamAgentsContent.test.tsx b/client/components/edit/datastream/DatastreamAgentsContent.test.tsx index 2015c9a01..a00007663 100644 --- a/client/components/edit/datastream/DatastreamAgentsContent.test.tsx +++ b/client/components/edit/datastream/DatastreamAgentsContent.test.tsx @@ -22,6 +22,12 @@ jest.mock("../../../context/EditorContext", () => ({ return mockUseEditorContext(); }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseDatastreamOperation = jest.fn(); jest.mock("../../../hooks/useDatastreamOperation", () => () => { return mockUseDatastreamOperation(); @@ -30,6 +36,7 @@ jest.mock("@mui/material/Grid", () => (props) => props.children); describe("DatastreamAgentsContent", () => { let editorValues; + let globalValues; let datastreamOperationValues; beforeEach(() => { editorValues = { @@ -45,7 +52,11 @@ describe("DatastreamAgentsContent", () => { }, action: { setCurrentAgents: jest.fn(), - toggleDatastreamModal: jest.fn(), + }, + }; + globalValues = { + action: { + closeModal: jest.fn(), }, }; datastreamOperationValues = { @@ -53,6 +64,7 @@ describe("DatastreamAgentsContent", () => { getAgents: jest.fn(), }; mockUseEditorContext.mockReturnValue(editorValues); + mockUseGlobalContext.mockReturnValue(globalValues); mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); }); @@ -81,7 +93,7 @@ describe("DatastreamAgentsContent", () => { await userEvent.setup().click(screen.getByText("Save Changes")); expect(datastreamOperationValues.uploadAgents).toHaveBeenCalled(); - expect(editorValues.action.toggleDatastreamModal).not.toHaveBeenCalled(); + expect(globalValues.action.closeModal).not.toHaveBeenCalled(); }); it("saves current agents and closes the modal", async () => { @@ -94,7 +106,7 @@ describe("DatastreamAgentsContent", () => { await userEvent.setup().click(screen.getByText("Save And Close")); expect(datastreamOperationValues.uploadAgents).toHaveBeenCalled(); - expect(editorValues.action.toggleDatastreamModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); }); it("resets current agents on cancel", async () => { @@ -109,6 +121,6 @@ describe("DatastreamAgentsContent", () => { expect(datastreamOperationValues.uploadAgents).not.toHaveBeenCalled(); expect(datastreamOperationValues.getAgents).toHaveBeenCalled(); expect(editorValues.action.setCurrentAgents).toHaveBeenCalled(); - expect(editorValues.action.toggleDatastreamModal).toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); }); }); diff --git a/client/components/edit/datastream/DatastreamAgentsContent.tsx b/client/components/edit/datastream/DatastreamAgentsContent.tsx index 5ef90d239..e817f2b52 100644 --- a/client/components/edit/datastream/DatastreamAgentsContent.tsx +++ b/client/components/edit/datastream/DatastreamAgentsContent.tsx @@ -4,6 +4,7 @@ import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import Grid from "@mui/material/Grid"; import { useEditorContext } from "../../../context/EditorContext"; +import { useGlobalContext } from "../../../context/GlobalContext"; import useDatastreamOperation from "../../../hooks/useDatastreamOperation"; import CircularProgress from "@mui/material/CircularProgress"; import Divider from "@mui/material/Divider"; @@ -15,8 +16,11 @@ import Box from "@mui/material/Box"; const DatastreamAgentsContent = (): React.ReactElement => { const { state: { agentsCatalog, currentAgents }, - action: { setCurrentAgents, toggleDatastreamModal }, + action: { setCurrentAgents }, } = useEditorContext(); + const { + action: { closeModal }, + } = useGlobalContext(); const { uploadAgents, getAgents } = useDatastreamOperation(); const { defaults: { role, type, name }, @@ -150,7 +154,7 @@ const DatastreamAgentsContent = (): React.ReactElement => { disabled={!canSave} onClick={async () => { await saveCurrentAgents(); - toggleDatastreamModal(); + closeModal("datastream"); }} > Save And Close @@ -161,7 +165,7 @@ const DatastreamAgentsContent = (): React.ReactElement => { className="agentsCancelButton" onClick={async () => { setCurrentAgents(await getAgents()); - toggleDatastreamModal(); + closeModal("datastream"); }} > Cancel diff --git a/client/components/edit/datastream/DatastreamControlButton.test.tsx b/client/components/edit/datastream/DatastreamControlButton.test.tsx index fda675213..4ceb3b4a4 100644 --- a/client/components/edit/datastream/DatastreamControlButton.test.tsx +++ b/client/components/edit/datastream/DatastreamControlButton.test.tsx @@ -12,23 +12,35 @@ jest.mock("../../../context/EditorContext", () => ({ return mockUseEditorContext(); }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseDatastreamOperation = jest.fn(); jest.mock("../../../hooks/useDatastreamOperation", () => () => mockUseDatastreamOperation()); describe("DatastreamControlButton", () => { let editorValues; + let globalValues; let datastreamOperationValues; beforeEach(() => { editorValues = { action: { - toggleDatastreamModal: jest.fn(), setActiveDatastream: jest.fn(), setDatastreamModalState: jest.fn(), }, }; + globalValues = { + action: { + openModal: jest.fn(), + }, + }; datastreamOperationValues = { downloadDatastream: jest.fn(), }; mockUseEditorContext.mockReturnValue(editorValues); + mockUseGlobalContext.mockReturnValue(globalValues); mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); }); @@ -52,6 +64,6 @@ describe("DatastreamControlButton", () => { await userEvent.setup().click(screen.getByRole("button")); expect(editorValues.action.setActiveDatastream).toHaveBeenCalledWith("THUMBNAIL"); expect(editorValues.action.setDatastreamModalState).toHaveBeenCalledWith("View"); - expect(editorValues.action.toggleDatastreamModal).toHaveBeenCalled(); + expect(globalValues.action.openModal).toHaveBeenCalledWith("datastream"); }); }); diff --git a/client/components/edit/datastream/DatastreamControlButton.tsx b/client/components/edit/datastream/DatastreamControlButton.tsx index cb91f285b..e9a0374b4 100644 --- a/client/components/edit/datastream/DatastreamControlButton.tsx +++ b/client/components/edit/datastream/DatastreamControlButton.tsx @@ -7,6 +7,7 @@ import Delete from "@mui/icons-material/Delete"; import Preview from "@mui/icons-material/Preview"; import UploadFile from "@mui/icons-material/UploadFile"; import { useEditorContext } from "../../../context/EditorContext"; +import { useGlobalContext } from "../../../context/GlobalContext"; import useDatastreamOperation from "../../../hooks/useDatastreamOperation"; const Icons = { @@ -30,15 +31,18 @@ const DatastreamControlButton = ({ }: DatastreamControlButtonProps): React.ReactElement => { const [isLoading, setLoading] = useState(false); const { - action: { toggleDatastreamModal, setActiveDatastream, setDatastreamModalState }, + action: { setActiveDatastream, setDatastreamModalState }, } = useEditorContext(); + const { + action: { openModal }, + } = useGlobalContext(); const { downloadDatastream } = useDatastreamOperation(); const onClick = (modalState) => { if (modalState !== "Download") { return () => { setActiveDatastream(datastream); setDatastreamModalState(modalState); - toggleDatastreamModal(); + openModal("datastream"); }; } return async () => { diff --git a/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx b/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx index 8ce85571d..3670b006b 100644 --- a/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx +++ b/client/components/edit/datastream/DatastreamDeleteModalContent.test.tsx @@ -11,39 +11,22 @@ jest.mock("../../../context/GlobalContext", () => ({ return mockUseGlobalContext(); }, })); -const mockUseEditorContext = jest.fn(); -jest.mock("../../../context/EditorContext", () => ({ - useEditorContext: () => { - return mockUseEditorContext(); - }, -})); const mockUseDatastreamOperation = jest.fn(); jest.mock("../../../hooks/useDatastreamOperation", () => () => mockUseDatastreamOperation()); describe("DatastreamDeleteModalContent", () => { let globalValues; - let editorValues; let datastreamOperationValues; beforeEach(() => { globalValues = { action: { setSnackbarState: jest.fn(), - }, - }; - editorValues = { - state: { - currentPid: "vudl:123", - activeDatastream: "THUMBNAIL", - }, - action: { - loadCurrentObjectDetails: jest.fn().mockResolvedValue({}), - toggleDatastreamModal: jest.fn(), + closeModal: jest.fn(), }, }; datastreamOperationValues = { deleteDatastream: jest.fn(), }; mockUseGlobalContext.mockReturnValue(globalValues); - mockUseEditorContext.mockReturnValue(editorValues); mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); }); @@ -57,4 +40,10 @@ describe("DatastreamDeleteModalContent", () => { await userEvent.setup().click(screen.getByText("Yes")); expect(datastreamOperationValues.deleteDatastream).toHaveBeenCalled(); }); + + it("can be stopped", async () => { + render(); + await userEvent.setup().click(screen.getByText("No")); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); + }); }); diff --git a/client/components/edit/datastream/DatastreamDeleteModalContent.tsx b/client/components/edit/datastream/DatastreamDeleteModalContent.tsx index 03c6452a0..b6fed1c85 100644 --- a/client/components/edit/datastream/DatastreamDeleteModalContent.tsx +++ b/client/components/edit/datastream/DatastreamDeleteModalContent.tsx @@ -3,13 +3,13 @@ import Button from "@mui/material/Button"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import DialogContentText from "@mui/material/DialogContentText"; -import { useEditorContext } from "../../../context/EditorContext"; +import { useGlobalContext } from "../../../context/GlobalContext"; import useDatastreamOperation from "../../../hooks/useDatastreamOperation"; const DatastreamDeleteModalContent = (): React.ReactElement => { const { - action: { toggleDatastreamModal }, - } = useEditorContext(); + action: { closeModal }, + } = useGlobalContext(); const { deleteDatastream } = useDatastreamOperation(); return ( @@ -18,7 +18,7 @@ const DatastreamDeleteModalContent = (): React.ReactElement => { Are you sure you want to delete the datastream? - - + ); diff --git a/client/components/edit/datastream/DatastreamLicenseContent.test.tsx b/client/components/edit/datastream/DatastreamLicenseContent.test.tsx index 03d9395cb..bce7579c9 100644 --- a/client/components/edit/datastream/DatastreamLicenseContent.test.tsx +++ b/client/components/edit/datastream/DatastreamLicenseContent.test.tsx @@ -12,6 +12,12 @@ jest.mock("../../../context/EditorContext", () => ({ return mockUseEditorContext(); }, })); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); + }, +})); const mockUseDatastreamOperation = jest.fn(); jest.mock("../../../hooks/useDatastreamOperation", () => () => { return mockUseDatastreamOperation(); @@ -19,6 +25,7 @@ jest.mock("../../../hooks/useDatastreamOperation", () => () => { describe("DatastreamLicenseContent", () => { let editorValues; + let globalValues; let datastreamOperationValues; beforeEach(() => { editorValues = { @@ -29,8 +36,10 @@ describe("DatastreamLicenseContent", () => { }, }, }, + }; + globalValues = { action: { - toggleDatastreamModal: jest.fn(), + closeModal: jest.fn(), }, }; datastreamOperationValues = { @@ -38,6 +47,7 @@ describe("DatastreamLicenseContent", () => { getLicenseKey: jest.fn(), }; mockUseEditorContext.mockReturnValue(editorValues); + mockUseGlobalContext.mockReturnValue(globalValues); mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); }); @@ -55,4 +65,13 @@ describe("DatastreamLicenseContent", () => { expect(datastreamOperationValues.getLicenseKey).toHaveBeenCalled(); expect(datastreamOperationValues.uploadLicense).toHaveBeenCalled(); }); + + it("can be canceled", async () => { + await act(async () => { + await render(); + }); + await fireEvent.click(screen.getByText("Cancel")); + expect(datastreamOperationValues.uploadLicense).not.toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); + }); }); diff --git a/client/components/edit/datastream/DatastreamLicenseContent.tsx b/client/components/edit/datastream/DatastreamLicenseContent.tsx index 25c9eb9eb..4c546658d 100644 --- a/client/components/edit/datastream/DatastreamLicenseContent.tsx +++ b/client/components/edit/datastream/DatastreamLicenseContent.tsx @@ -9,12 +9,15 @@ import Radio from "@mui/material/Radio"; import RadioGroup from "@mui/material/RadioGroup"; import useDatastreamOperation from "../../../hooks/useDatastreamOperation"; import { useEditorContext } from "../../../context/EditorContext"; +import { useGlobalContext } from "../../../context/GlobalContext"; const DatastreamLicenseContent = (): React.ReactElement => { const { state: { licensesCatalog }, - action: { toggleDatastreamModal }, } = useEditorContext(); + const { + action: { closeModal }, + } = useGlobalContext(); const { uploadLicense, getLicenseKey } = useDatastreamOperation(); const [licenseKey, setLicenseKey] = useState(""); useEffect(() => { @@ -53,7 +56,7 @@ const DatastreamLicenseContent = (): React.ReactElement => { > Save - + ); diff --git a/client/components/edit/datastream/DatastreamProcessMetadataContent.test.tsx b/client/components/edit/datastream/DatastreamProcessMetadataContent.test.tsx index 953e2ad1a..3bbc07710 100644 --- a/client/components/edit/datastream/DatastreamProcessMetadataContent.test.tsx +++ b/client/components/edit/datastream/DatastreamProcessMetadataContent.test.tsx @@ -6,10 +6,10 @@ import renderer from "react-test-renderer"; import DatastreamProcessMetadataContent from "./DatastreamProcessMetadataContent"; import { waitFor } from "@testing-library/react"; -const mockUseEditorContext = jest.fn(); -jest.mock("../../../context/EditorContext", () => ({ - useEditorContext: () => { - return mockUseEditorContext(); +const mockUseGlobalContext = jest.fn(); +jest.mock("../../../context/GlobalContext", () => ({ + useGlobalContext: () => { + return mockUseGlobalContext(); }, })); @@ -39,7 +39,7 @@ jest.mock("@mui/x-date-pickers", () => ({ describe("DatastreamProcessMetadataContent", () => { let datastreamOperationValues; - let editorValues; + let globalValues; let processMetadataValues; const renderComponent = async (fakeData = {}) => { @@ -70,8 +70,8 @@ describe("DatastreamProcessMetadataContent", () => { getProcessMetadata: jest.fn(), }; mockUseDatastreamOperation.mockReturnValue(datastreamOperationValues); - editorValues = { action: { toggleDatastreamModal: jest.fn() } }; - mockUseEditorContext.mockReturnValue(editorValues); + globalValues = { action: { closeModal: jest.fn() } }; + mockUseGlobalContext.mockReturnValue(globalValues); mockDatastreamProcessMetadataTaskProps = []; processMetadataValues = { state: {}, @@ -141,4 +141,11 @@ describe("DatastreamProcessMetadataContent", () => { mockDatastreamProcessMetadataTaskProps[0].deleteTask(); expect(processMetadataValues.action.deleteTask).toHaveBeenCalledWith(0); }); + + it("can be canceled", async () => { + await renderComponent(); + await userEvent.setup().click(screen.getByText("Cancel")); + expect(datastreamOperationValues.uploadProcessMetadata).not.toHaveBeenCalled(); + expect(globalValues.action.closeModal).toHaveBeenCalledWith("datastream"); + }); }); diff --git a/client/components/edit/datastream/DatastreamProcessMetadataContent.tsx b/client/components/edit/datastream/DatastreamProcessMetadataContent.tsx index 8b0c3fccf..4b8f2c5c5 100644 --- a/client/components/edit/datastream/DatastreamProcessMetadataContent.tsx +++ b/client/components/edit/datastream/DatastreamProcessMetadataContent.tsx @@ -6,7 +6,7 @@ import FormControl from "@mui/material/FormControl"; import FormLabel from "@mui/material/FormLabel"; import TextField from "@mui/material/TextField"; import useDatastreamOperation from "../../../hooks/useDatastreamOperation"; -import { useEditorContext } from "../../../context/EditorContext"; +import { useGlobalContext } from "../../../context/GlobalContext"; import { useProcessMetadataContext } from "../../../context/ProcessMetadataContext"; import BlurSavingTextField from "../../shared/BlurSavingTextField"; import Grid from "@mui/material/Grid"; @@ -22,8 +22,8 @@ let taskKeyGeneration = 0; const DatastreamProcessMetadataContent = (): React.ReactElement => { const { - action: { toggleDatastreamModal }, - } = useEditorContext(); + action: { closeModal }, + } = useGlobalContext(); const { state: processMetadata, action: { @@ -133,7 +133,7 @@ const DatastreamProcessMetadataContent = (): React.ReactElement => { > Save - + ); diff --git a/client/components/edit/datastream/__snapshots__/DatastreamDeleteModalContent.test.tsx.snap b/client/components/edit/datastream/__snapshots__/DatastreamDeleteModalContent.test.tsx.snap index b2f7b39ee..58b3515c9 100644 --- a/client/components/edit/datastream/__snapshots__/DatastreamDeleteModalContent.test.tsx.snap +++ b/client/components/edit/datastream/__snapshots__/DatastreamDeleteModalContent.test.tsx.snap @@ -18,7 +18,7 @@ exports[`DatastreamDeleteModalContent renders 1`] = ` className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary noButton css-1e6y48t-MuiButtonBase-root-MuiButton-root" disabled={false} onBlur={[Function]} - onClick={[MockFunction]} + onClick={[Function]} onContextMenu={[Function]} onDragLeave={[Function]} onFocus={[Function]} diff --git a/client/components/edit/datastream/__snapshots__/DatastreamDublinCoreContent.test.tsx.snap b/client/components/edit/datastream/__snapshots__/DatastreamDublinCoreContent.test.tsx.snap index 63aae1860..1d374846d 100644 --- a/client/components/edit/datastream/__snapshots__/DatastreamDublinCoreContent.test.tsx.snap +++ b/client/components/edit/datastream/__snapshots__/DatastreamDublinCoreContent.test.tsx.snap @@ -52,6 +52,7 @@ exports[`DatastreamDublinCoreContent renders 1`] = ` className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-1e6y48t-MuiButtonBase-root-MuiButton-root" disabled={false} onBlur={[Function]} + onClick={[Function]} onContextMenu={[Function]} onDragLeave={[Function]} onFocus={[Function]} diff --git a/client/components/edit/datastream/__snapshots__/DatastreamLicenseContent.test.tsx.snap b/client/components/edit/datastream/__snapshots__/DatastreamLicenseContent.test.tsx.snap index a5e8c7363..c26a3bc72 100644 --- a/client/components/edit/datastream/__snapshots__/DatastreamLicenseContent.test.tsx.snap +++ b/client/components/edit/datastream/__snapshots__/DatastreamLicenseContent.test.tsx.snap @@ -44,7 +44,7 @@ exports[`DatastreamLicenseContent renders 1`] = ` className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-1e6y48t-MuiButtonBase-root-MuiButton-root" disabled={false} onBlur={[Function]} - onClick={[MockFunction]} + onClick={[Function]} onContextMenu={[Function]} onDragLeave={[Function]} onFocus={[Function]} diff --git a/client/components/edit/datastream/__snapshots__/DatastreamProcessMetadataContent.test.tsx.snap b/client/components/edit/datastream/__snapshots__/DatastreamProcessMetadataContent.test.tsx.snap index 43501f6eb..a76867301 100644 --- a/client/components/edit/datastream/__snapshots__/DatastreamProcessMetadataContent.test.tsx.snap +++ b/client/components/edit/datastream/__snapshots__/DatastreamProcessMetadataContent.test.tsx.snap @@ -223,7 +223,7 @@ exports[`DatastreamProcessMetadataContent renders a form when empty data is load className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-1e6y48t-MuiButtonBase-root-MuiButton-root" disabled={false} onBlur={[Function]} - onClick={[MockFunction]} + onClick={[Function]} onContextMenu={[Function]} onDragLeave={[Function]} onFocus={[Function]} @@ -465,7 +465,7 @@ exports[`DatastreamProcessMetadataContent renders a form when non-empty data is className="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-colorPrimary css-1e6y48t-MuiButtonBase-root-MuiButton-root" disabled={false} onBlur={[Function]} - onClick={[MockFunction]} + onClick={[Function]} onContextMenu={[Function]} onDragLeave={[Function]} onFocus={[Function]} From 62ade0b08f7f5f3c70c8ee147efb8a0c88509da2 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Mon, 1 Jul 2024 09:45:01 -0400 Subject: [PATCH 18/22] Review feedback. --- client/components/edit/StateModal.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/components/edit/StateModal.test.tsx b/client/components/edit/StateModal.test.tsx index b58930667..8d7aadc70 100644 --- a/client/components/edit/StateModal.test.tsx +++ b/client/components/edit/StateModal.test.tsx @@ -92,6 +92,7 @@ describe("StateModal", () => { renderer.act(() => { tree = renderer.create(); }); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -100,6 +101,7 @@ describe("StateModal", () => { renderer.act(() => { tree = renderer.create(); }); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -111,6 +113,7 @@ describe("StateModal", () => { tree = renderer.create(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -122,6 +125,7 @@ describe("StateModal", () => { tree = renderer.create(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); expect(tree.toJSON()).toMatchSnapshot(); }); @@ -133,6 +137,7 @@ describe("StateModal", () => { render(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); const user = userEvent.setup(); await user.click(screen.getByText("Active")); await user.click(screen.getByText("Save")); @@ -159,6 +164,7 @@ describe("StateModal", () => { render(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); await userEvent.setup().click(screen.getByText("Save")); await waitFor(() => expect(globalValues.action.setSnackbarState).toHaveBeenCalledWith({ @@ -180,6 +186,7 @@ describe("StateModal", () => { render(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); const user = userEvent.setup(); await user.click(screen.getByText("Active")); await user.click(screen.getByText("Save")); @@ -206,6 +213,7 @@ describe("StateModal", () => { render(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); const user = userEvent.setup(); await user.click(screen.getByText("Active")); await user.click(screen.getByText("Update 1 children to match")); @@ -233,6 +241,7 @@ describe("StateModal", () => { render(); }); await waitFor(() => expect(fetchContextValues.action.fetchJSON).toHaveBeenCalled()); + expect(globalValues.action.isModalOpen).toHaveBeenCalledWith("state"); const user = userEvent.setup(); await user.click(screen.getByText("Active")); await user.click(screen.getByText("Update 1 children to match")); From c0a1f6b9a698343b155197a52424028d2c64fbce Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Mon, 1 Jul 2024 16:50:12 -0400 Subject: [PATCH 19/22] improve types --- client/context/GlobalContext.tsx | 69 +++++++++++++++++++------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index 04edaed86..2d9887c1d 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useReducer } from "react"; +import React, { createContext, useContext, useReducer, Dispatch, ReactNode } from "react"; interface SnackbarState { open: boolean, @@ -28,49 +28,62 @@ const globalContextParams: GlobalState = { }, }; -const reducerMapping: Record = { - // Snackbar - SET_SNACKBAR_STATE: "snackbarState", -}; +type Action = + | { type: 'OPEN_MODAL'; payload: string } + | { type: 'CLOSE_MODAL'; payload: string } + | { type: 'SET_SNACKBAR_STATE'; payload: SnackbarState }; /** * Update the shared states of react components. */ -const globalReducer = (state: GlobalState, { type, payload }: { type: string, payload: unknown}) => { - function updateModalInState(payload, isOpen) { - const modalOpenStates = { +const globalReducer = (state: GlobalState, action: Action): GlobalState => { + switch (action.type) { + case 'OPEN_MODAL': + return { + ...state, + modalOpenStates: { ...state.modalOpenStates, - [payload]: isOpen, + [action.payload]: true, + }, }; + case 'CLOSE_MODAL': return { ...state, - modalOpenStates, - }; - } - if (type == "OPEN_MODAL") { - return updateModalInState(payload, true); - } - if (type == "CLOSE_MODAL") { - return updateModalInState(payload, false); - } - - if (Object.keys(reducerMapping).includes(type)){ + modalOpenStates: { + ...state.modalOpenStates, + [action.payload]: false, + }, + }; + case 'SET_SNACKBAR_STATE': return { ...state, - [reducerMapping[type]]: payload + snackbarState: action.payload, }; - } else { - console.error(`global action type: ${type} does not exist`); + default: return state; } }; -const GlobalContext = createContext({}); +/** + * Context to provide global state and dispatch function. + */ +interface GlobalContextProps { + state: GlobalState; + dispatch: Dispatch; +} -export const GlobalContextProvider = ({ children }) => { - const [state, dispatch] = useReducer(globalReducer, globalContextParams); - const value = { state, dispatch }; - return {children}; +const GlobalContext = createContext(undefined); + +/** + * GlobalContextProvider to wrap around the application. + */ +export const GlobalContextProvider = ({ children }: { children: ReactNode }) => { + const [state, dispatch] = useReducer(globalReducer, initalGlobalState); + return ( + + {children} + + ); }; export const useGlobalContext = () => { From 4deddeab8130481c6228af4d68f928883c2412fb Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Mon, 1 Jul 2024 16:50:45 -0400 Subject: [PATCH 20/22] simplify useContext --- client/context/GlobalContext.tsx | 43 ++++++++++++++------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index 2d9887c1d..aab87a378 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -17,7 +17,7 @@ interface GlobalState { * Pass a shared entity to react components, * specifically a way to make api requests. */ -const globalContextParams: GlobalState = { +const initalGlobalState: GlobalState = { // Modal control modalOpenStates: {}, // Snackbar @@ -42,25 +42,25 @@ const globalReducer = (state: GlobalState, action: Action): GlobalState => { return { ...state, modalOpenStates: { - ...state.modalOpenStates, + ...state.modalOpenStates, [action.payload]: true, }, - }; + }; case 'CLOSE_MODAL': - return { - ...state, + return { + ...state, modalOpenStates: { ...state.modalOpenStates, [action.payload]: false, }, }; case 'SET_SNACKBAR_STATE': - return { - ...state, + return { + ...state, snackbarState: action.payload, - }; + }; default: - return state; + return state; } }; @@ -87,19 +87,17 @@ export const GlobalContextProvider = ({ children }: { children: ReactNode }) => }; export const useGlobalContext = () => { - const { - state: { - // Modal control - modalOpenStates, - // Snackbar - snackbarState, - }, - dispatch, - } = useContext(GlobalContext); + const context = useContext(GlobalContext); + + if (!context) { + throw new Error("useGlobalContext must be used within a GlobalContextProvider"); + } + + const { state, dispatch } = context; // Modal control - const isModalOpen = (modal: string) => modalOpenStates[modal] ?? false; + const isModalOpen = (modal: string) => state.modalOpenStates[modal] ?? false; const openModal = (modal: string) => { dispatch({ type: "OPEN_MODAL", @@ -113,7 +111,7 @@ export const useGlobalContext = () => { }); }; const toggleModal = (modal: string) => { - if (modalOpenStates[modal] ?? false) { + if (isModalOpen(modal)) { closeModal(modal); } else { openModal(modal); @@ -130,10 +128,7 @@ export const useGlobalContext = () => { }; return { - state: { - // Snackbar - snackbarState, - }, + state, action: { // Modal control isModalOpen, From 9cacdeac5fa15af18fd97228cd0f555c07a8a7e8 Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 3 Jul 2024 11:13:57 -0400 Subject: [PATCH 21/22] fix: return limited state. --- client/context/GlobalContext.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index aab87a378..3c47df455 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -128,7 +128,11 @@ export const useGlobalContext = () => { }; return { - state, + state: { + // only return limited state + // to create "private" attributes + snackbarState: state.snackbarState, + }, action: { // Modal control isModalOpen, From f48b273b4389299e6da8d7c39c3e2b29a1b77cec Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Fri, 5 Jul 2024 11:34:16 -0400 Subject: [PATCH 22/22] Use open/close instead of toggle. --- client/components/edit/ObjectStatus.tsx | 4 ++-- client/hooks/useDatastreamOperation.test.ts | 6 +++--- client/hooks/useDatastreamOperation.ts | 13 ++++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/client/components/edit/ObjectStatus.tsx b/client/components/edit/ObjectStatus.tsx index dbebf8ae7..3cbfe67ca 100644 --- a/client/components/edit/ObjectStatus.tsx +++ b/client/components/edit/ObjectStatus.tsx @@ -10,7 +10,7 @@ export interface ObjectStatusProps { export const ObjectStatus = ({ pid }: ObjectStatusProps): React.ReactElement => { const { - action: { toggleModal }, + action: { openModal }, } = useGlobalContext(); const { state: { objectDetailsStorage }, @@ -22,7 +22,7 @@ export const ObjectStatus = ({ pid }: ObjectStatusProps): React.ReactElement => const stateTxt = details.state ?? "Unknown"; const clickAction = () => { setStateModalActivePid(pid); - toggleModal("state"); + openModal("state"); }; const stateMsg = loaded ? (