From f8128b8d008d95db563e782418e1fa536b9e32eb Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Tue, 7 Feb 2023 16:56:42 -0500 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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 9ecd09dbc777ad79885da53034d7c11fa1b67adb Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 8 Feb 2023 15:21:20 -0500 Subject: [PATCH 06/23] refactor: move colors to CSS variables in globals.css --- client/pages/_app.tsx | 1 + client/styles/application.css | 62 +++++++++++++++++++++++++--------- client/styles/client.css | 39 +++++++++++----------- client/styles/globals.css | 63 ++++++++++++++++++++++++++++------- 4 files changed, 117 insertions(+), 48 deletions(-) diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 09dce2e0c..1b178cbfa 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -5,6 +5,7 @@ import { PaginatorContextProvider } from "../context/PaginatorContext"; import { FetchContextProvider } from "../context/FetchContext"; import "../styles/normalize.css"; +import "../styles/globals.css"; import "../styles/application.css"; import "../styles/client.css"; import "../styles/justgrid.css"; diff --git a/client/styles/application.css b/client/styles/application.css index 5c6286eda..f7e689a9d 100644 --- a/client/styles/application.css +++ b/client/styles/application.css @@ -1,5 +1,5 @@ body { - background: #fafbfc; + background: var(--body-bg); font-size: 14px; font-family: "Roboto", Arial; font-weight: 500; @@ -18,7 +18,7 @@ h2 { margin-top: 2rem; margin-bottom: 0; font-size: 1.6rem; - color: #4d4d4d; + color: var(--heading-color); } ul { @@ -36,10 +36,10 @@ button, margin: 0 2px; padding: 4px 10px; border: 0; - color: #fff; + color: var(--btn-text); line-height: 1.5; text-decoration: none; - background-color: #485a6c; + background-color: var(--btn-bg); cursor: pointer; border-radius: 5px; } @@ -50,33 +50,33 @@ button.active, .button:hover, .button:focus, .button.active { - background-color: #3498db; - border-color: #275575; + background-color: var(--btn-focus-bg); + border-color: var(--btn-border); } button.primary, .button.primary { - background-color: #27ae60; - border-color: #13562f; - color: #fff; + background-color: var(--btn-primary-bg); + border-color: var(--btn-primary-border); + color: var(--btn-primary-text); } button.primary:hover, .button.primary:hover, button.primary:focus, .button.primary:focus { - background-color: #155724; + background-color: var(--btn-primary-focus-bg); } button.danger, .button.danger { - background-color: red; + background-color: var(--btn-danger-bg); margin-left: 20px; } button.danger:hover, .button.danger:hover, button.danger:focus, .button.danger:focus { - background-color: #a10705; + background-color: var(--btn-danger-focus-bg); } button.btn-link { @@ -84,18 +84,48 @@ button.btn-link { padding: 0 0.25rem; border-color: transparent; font-weight: bold; - color: #008aed; + color: var(--link-color); text-decoration: underline; background-color: transparent; box-shadow: none; } -.logout { +.nav--right { position: absolute; top: 12px; right: 8px; + display: flex; + gap: 0.25rem; } + +.user-theme__menu { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; +} +#user-theme { + min-height: auto; + padding-block: 5px; + border-width: 2px; + font-size: 0.875rem; + line-height: 1.25rem; +} +#user-theme__label { + position: relative; + width: fit-content; + font-size: 0.875rem; + transform: none; + transition: none; +} + .logout .button { - background: #1abc9c; - border-color: #16a085; + background-color: var(--btn-logout-bg); + border-color: var(--btn-logout-border); +} +.logout .button:hover, +.logout .button:focus { + background-color: var(--btn-logout-focus-bg); } diff --git a/client/styles/client.css b/client/styles/client.css index 4456f7797..d519f19c9 100644 --- a/client/styles/client.css +++ b/client/styles/client.css @@ -23,13 +23,13 @@ p { } input { padding: 2px; - border: 1px solid #6a737d; + border: 1px solid var(--input-border); border-radius: 4px; } input:focus { - border-color: #2188ff; - outline-color: #2188ff; - box-shadow: 0 0 0 4px #c8e1ff; + border-color: var(--input-focus-border); + outline-color: var(--input-focus-border); + box-shadow: 0 0 0 4px var(--shadow-gray); } .status { padding: 0; @@ -39,7 +39,7 @@ input:focus { height: auto; margin-right: 3px; padding: 6px; - border: 1px solid #53626f; + border: 1px solid var(--group-border); border-radius: 2px; } @@ -54,7 +54,7 @@ label { cursor: pointer; } label:hover { - background-color: #c8e1ff; + background-color: var(--label-hover); } label input[type="text"], label input[type="number"], @@ -79,8 +79,7 @@ label input[type="radio"] { } .group button { margin: 0; - border-right: 1px solid #ececec; - color: #fff; + border-right: 1px solid var(--group-border); border-radius: 0; } .group input[type="text"] + button, @@ -101,14 +100,14 @@ label input[type="radio"] { .toggles button, .toggles + button { padding: 5px 40px; - background-color: #7f8c8d; - border-color: #34495e; + background-color: var(--btn-toggle-bg); + border-color: var(--btn-toggle-border); } .toggles button:hover, .toggles + button:hover, .toggles button:focus, .toggles + button:focus { - background-color: #53626f; + background-color: var(--btn-toggle-focus-bg); } .group#prefixes button { @@ -128,8 +127,8 @@ label input[type="radio"] { margin-left: 8px; padding: 4px; padding-bottom: 400px; - border: 1px solid #c1c1c1; - background-color: #fff; + border: 1px solid var(--page-list-border); + background-color: var(--page-list-bg); border-radius: 6px 0 0 6px; } .thumbnail { @@ -141,7 +140,7 @@ label input[type="radio"] { height: 202px; margin: 2px; padding: 4px; - background: #e1e1e1; + background-color: var(--thumbnail-bg); font-weight: 400; text-align: center; text-shadow: 0 1px 0 #fff; @@ -149,7 +148,7 @@ label input[type="radio"] { word-break: break-all; } .thumbnail.selected { - background: #51aded; + background-color: var(--thumbnail-focus-bg); } .thumbnail img { display: block; @@ -175,8 +174,8 @@ label input[type="radio"] { left: 0; padding: 2px; font-size: 10px; - background: #fff; - color: #000; + background-color: var(--thumbail-number-bg); + color: var(--thumbail-number-text); } .preview, #zoomy { @@ -191,11 +190,11 @@ label input[type="radio"] { } .preview img { - border: 1px solid black; + border: 1px solid var(--thumbnail-preview-border); } .MuiTreeItem-root.Mui-selected, .MuiTreeItem-root.Mui-selected:hover { - color: #fff; - background-color: #000; + color: var(--tree-item-focus-text); + background-color: var(--tree-item-focus-bg); } diff --git a/client/styles/globals.css b/client/styles/globals.css index e5e2dcc23..5796901b7 100644 --- a/client/styles/globals.css +++ b/client/styles/globals.css @@ -1,16 +1,55 @@ -html, -body { - padding: 0; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} +:root, +[color-scheme="light"] { + --body-bg: #fafbfc; + --heading-color: #4d4d4d; + --link-color: #008aed; + + --btn-bg: #485a6c; + --btn-text: #ffffff; + --btn-border: #275575; + --btn-focus-bg: #3498db; + + --btn-primary-bg: #27ae60; + --btn-primary-text: #ffffff; + --btn-primary-border: #275575; + --btn-primary-focus-bg: #155724; + + --btn-danger-bg: red; + --btn-danger-text: #ffffff; + --btn-danger-border: #a10705; + --btn-danger-focus-bg: #a10705; + + --btn-logout-bg: #1abc9c; + --btn-logout-text: #ffffff; + --btn-logout-border: #16a085; + --btn-logout-focus-bg: #1abc9c; + + --btn-toggle-bg: #7f8c8d; + --btn-toggle-text: #ffffff; + --btn-toggle-border: #34495e; + --btn-toggle-focus-bg: #53626f; + + --group-border: #53626f; + + --input-border: #6a737d; + --input-focus-border: #2188ff; + + --label-hover: #c8e1ff; + + --page-list-bg: #ffffff; + --page-list-border: #c1c1c1; + + --thumbnail-bg: #e1e1e1; + --thumbnail-focus-bg: #51aded; + --thumbnail-number-bg: #ffffff; + --thumbnail-number-text: #000000; + --thumbnail-preview-border: #000000; + + --tree-item-focus-bg: #000000; + --tree-item-focus-text: #ffffff; -a { - color: inherit; - text-decoration: none; + --shadow-gray: #c8e1ff; } -* { - box-sizing: border-box; +[color-scheme="dark"] { } From edbf0cd968688827acf3d50c4b727f88ac6d220c Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 8 Feb 2023 15:21:40 -0500 Subject: [PATCH 07/23] refactor: modernize justgrid.css --- client/styles/justgrid.css | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/styles/justgrid.css b/client/styles/justgrid.css index 760c7e67b..d3a7aeec7 100644 --- a/client/styles/justgrid.css +++ b/client/styles/justgrid.css @@ -1 +1,20 @@ -.row:after,.row:before{clear:both;content:"";display:table}.col,.row{box-sizing:border-box;width:100%}.col{float:left}.col.one{width:8.333%}.col.two{width:16.666%}.col.three{width:25%}.col.four{width:33.333%}.col.five{width:41.666%}.col.six{width:50%}.col.seven{width:58.333%}.col.eight{width:66.666%}.col.nine{width:75%}.col.ten{width:83.333%}.col.eleven{width:91.666%}@media(max-width:600px){.row:not(.static)>.col{width:100%}} \ No newline at end of file +/* +.row:after,.row:before{clear:both;content:"";display:table}.col,.row{box-sizing:border-box;width:100%}.col{float:left}.col.one{width:8.333%}.col.two{width:16.666%}.col.three{width:25%}.col.four{width:33.333%}.col.five{width:41.666%}.col.six{width:50%}.col.seven{width:58.333%}.col.eight{width:66.666%}.col.nine{width:75%}.col.ten{width:83.333%}.col.eleven{width:91.666%}@media(max-width:600px){.row:not(.static)>.col{width:100%}} +*/ + +.row { + display: flex; + width: 100%; +} +.col.one { flex: 1 1 0%; } +.col.two { flex: 2 2 0%; } +.col.three { flex: 3 3 0%; } +.col.four { flex: 4 4 0%; } +.col.five { flex: 5 5 0%; } +.col.six { flex: 6 6 0%; } +.col.seven { flex: 7 7 0%; } +.col.eight { flex: 8 8 0%; } +.col.nine { flex: 9 9 0%; } +.col.ten { flex: 10 10 0%; } +.col.eleven { flex: 11 11 0%; } +.col.twelve { flex: 12 12 0%; } From c3f164e3779d2c64a22206e76e5a72d1732ddc21 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Thu, 9 Feb 2023 15:25:38 -0500 Subject: [PATCH 08/23] style: Touch up styles, focused on the editor. --- client/components/edit/ObjectButtonBar.tsx | 2 +- .../components/edit/ObjectStatus.module.css | 26 ++++++--- client/components/edit/ObjectStatus.tsx | 2 +- client/components/edit/children/Child.tsx | 10 ++-- .../edit/children/ChildList.module.css | 15 +++++- client/components/edit/children/ChildList.tsx | 1 + client/components/paginate/JobSelector.tsx | 2 +- client/styles/application.css | 27 ++++++---- client/styles/client.css | 33 ++++++++---- client/styles/globals.css | 53 ++++++++++++------- client/styles/normalize.css | 5 +- 11 files changed, 121 insertions(+), 55 deletions(-) diff --git a/client/components/edit/ObjectButtonBar.tsx b/client/components/edit/ObjectButtonBar.tsx index e683f2012..d6d5a87ed 100644 --- a/client/components/edit/ObjectButtonBar.tsx +++ b/client/components/edit/ObjectButtonBar.tsx @@ -19,7 +19,7 @@ const ObjectButtonBar = ({ pid }: ObjectButtonBarProps): React.ReactElement => { diff --git a/client/components/edit/ObjectStatus.module.css b/client/components/edit/ObjectStatus.module.css index ad8cbe4ed..bec37dfe3 100644 --- a/client/components/edit/ObjectStatus.module.css +++ b/client/components/edit/ObjectStatus.module.css @@ -1,19 +1,29 @@ +.active, +.inactive, +.deleted, +.unknown { + display: inline-flex; + width: 100px; + text-align: left; +} + +.indicator__label { + flex: 1 1 0%; + text-align: center; +} + .active .indicator { - color: green; + color: var(--object-status-active); } .inactive .indicator { - color: yellow; + color: var(--object-status-inactive); } .deleted .indicator { - color: red; + color: var(--object-status-deleted); } .unknown .indicator { - color: gray; -} - -.indicator { - text-shadow: -1px 1px 0 #000, 1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000; + color: var(--object-status-unknown); } diff --git a/client/components/edit/ObjectStatus.tsx b/client/components/edit/ObjectStatus.tsx index 6e230ea7d..18b7d88ba 100644 --- a/client/components/edit/ObjectStatus.tsx +++ b/client/components/edit/ObjectStatus.tsx @@ -23,7 +23,7 @@ export const ObjectStatus = ({ pid }: ObjectStatusProps): React.ReactElement => const stateMsg = loaded ? ( ) : ( "" diff --git a/client/components/edit/children/Child.tsx b/client/components/edit/children/Child.tsx index 6778a8ac5..1c89b40dd 100644 --- a/client/components/edit/children/Child.tsx +++ b/client/components/edit/children/Child.tsx @@ -29,7 +29,11 @@ export const Child = ({ pid, parentPid = "", initialTitle, thumbnail = false }: const title = !loaded ? initialTitle : extractFirstMetadataValue(details?.metadata ?? {}, "dc:title", "-"); const expandControl = ( setExpanded(!expanded)}> - {expanded ? : } + { + expanded + ? + : + } ); const childList = expanded ? : ""; @@ -40,8 +44,8 @@ export const Child = ({ pid, parentPid = "", initialTitle, thumbnail = false }: ) : null; return ( <> - - + + {expandControl} {loaded && parentPid ? : ""} {(title.length > 0 ? title : "-") + " [" + pid + "]"} diff --git a/client/components/edit/children/ChildList.module.css b/client/components/edit/children/ChildList.module.css index 7b13a2d4c..d4f78c32c 100644 --- a/client/components/edit/children/ChildList.module.css +++ b/client/components/edit/children/ChildList.module.css @@ -1,7 +1,20 @@ .childlist { + padding-inline: 0; list-style-type: none; } +.childlist .childlist { + margin-block: 0; + padding-inline: 1.5rem 0.75rem; +} + +.childlist p { + margin-inline-start: 1.75rem; + margin-block-end: 0rem; + padding-inline: 0; +} .childlist li { - border-top: 1px solid gray; + margin-block: 0; + padding-block: 0.25rem; + border-top: 1px solid var(--page-list-border, #878d96); } diff --git a/client/components/edit/children/ChildList.tsx b/client/components/edit/children/ChildList.tsx index 281aaae53..772b6cb20 100644 --- a/client/components/edit/children/ChildList.tsx +++ b/client/components/edit/children/ChildList.tsx @@ -82,6 +82,7 @@ export const ChildList = ({ const paginator = pageCount > 1 ? ( { return ( <> -
{[...categoryComponents[0], ...categoryComponents[1]]}
; +
{[...categoryComponents[0], ...categoryComponents[1]]}
); }; diff --git a/client/styles/application.css b/client/styles/application.css index f7e689a9d..35a5f1420 100644 --- a/client/styles/application.css +++ b/client/styles/application.css @@ -1,11 +1,23 @@ +html { + font-size: 16px; +} + body { - background: var(--body-bg); - font-size: 14px; - font-family: "Roboto", Arial; + padding-inline: 1rem; + font-size: 1rem; + font-family: var(--family-sans); font-weight: 500; + line-height: 1.5; + color: var(--body-color); + background-color: var(--body-bg); overflow-x: hidden; } +a, +button { + font-weight: 500; +} + .hidden { display: none; } @@ -22,7 +34,7 @@ h2 { } ul { - margin-top: 0.25rem; + margin-block: 1rem; padding-left: 1rem; list-style-position: inside; } @@ -104,19 +116,16 @@ button.btn-link { flex-direction: row; align-items: center; gap: 0.25rem; - font-size: 0.75rem; } #user-theme { min-height: auto; - padding-block: 5px; - border-width: 2px; - font-size: 0.875rem; + padding-block: 6px; line-height: 1.25rem; + background-color: var(--input-bg); } #user-theme__label { position: relative; width: fit-content; - font-size: 0.875rem; transform: none; transition: none; } diff --git a/client/styles/client.css b/client/styles/client.css index d519f19c9..663100619 100644 --- a/client/styles/client.css +++ b/client/styles/client.css @@ -1,16 +1,15 @@ p { - padding: 0 12px; + padding: 0 1rem; width: 100%; - margin-bottom: 9px; + margin-block: 1rem; } .controls { margin-top: 0px; - padding: 0 8px; } .top { position: absolute; - top: 41px; - right: 8px; + top: 3.5rem; + right: 1rem; } .top button { margin: 0 2px; @@ -23,6 +22,7 @@ p { } input { padding: 2px; + background-color: var(--input-bg) !important; border: 1px solid var(--input-border); border-radius: 4px; } @@ -33,7 +33,7 @@ input:focus { } .status { padding: 0; - padding-top: 5px; +/* padding-top: 5px;*/ } .group input[type="text"] { height: auto; @@ -140,10 +140,11 @@ label input[type="radio"] { height: 202px; margin: 2px; padding: 4px; - background-color: var(--thumbnail-bg); font-weight: 400; text-align: center; + background-color: var(--thumbnail-bg); text-shadow: 0 1px 0 #fff; + border-radius: 0.25rem; cursor: pointer; word-break: break-all; } @@ -172,10 +173,11 @@ label input[type="radio"] { display: table; top: 0; left: 0; - padding: 2px; + padding-inline: 4px; font-size: 10px; - background-color: var(--thumbail-number-bg); - color: var(--thumbail-number-text); + font-weight: 700; + background-color: var(--thumbnail-number-bg); + color: var(--thumbnail-number-text); } .preview, #zoomy { @@ -198,3 +200,14 @@ label input[type="radio"] { color: var(--tree-item-focus-text); background-color: var(--tree-item-focus-bg); } + +.child__container { + align-items: center; +} +.child__label { + display: flex; + align-items: center; +} +.child__expand-icon { + vertical-align: bottom; +} diff --git a/client/styles/globals.css b/client/styles/globals.css index 5796901b7..b184cbe4b 100644 --- a/client/styles/globals.css +++ b/client/styles/globals.css @@ -1,50 +1,65 @@ +:root { + --family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --family-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, var(--family-emoji); +} :root, [color-scheme="light"] { - --body-bg: #fafbfc; - --heading-color: #4d4d4d; - --link-color: #008aed; + --body-bg: #f2f4f8; + --body-color: #343a3f; + --heading-color: #121619; + --link-color: #0043ce; - --btn-bg: #485a6c; + --btn-bg: #4d5358; --btn-text: #ffffff; - --btn-border: #275575; - --btn-focus-bg: #3498db; + --btn-border: #343a3f; + --btn-focus-bg: #21272a; - --btn-primary-bg: #27ae60; + --btn-primary-bg: #198038; --btn-primary-text: #ffffff; - --btn-primary-border: #275575; - --btn-primary-focus-bg: #155724; + --btn-primary-border: #044317; + --btn-primary-focus-bg: #022d0d; - --btn-danger-bg: red; + --btn-danger-bg: #da1e28; --btn-danger-text: #ffffff; - --btn-danger-border: #a10705; - --btn-danger-focus-bg: #a10705; + --btn-danger-border: #750e13; + --btn-danger-focus-bg: #520408; - --btn-logout-bg: #1abc9c; + --btn-logout-bg: #007d79; --btn-logout-text: #ffffff; - --btn-logout-border: #16a085; - --btn-logout-focus-bg: #1abc9c; + --btn-logout-border: #004144; + --btn-logout-focus-bg: #022b30; --btn-toggle-bg: #7f8c8d; --btn-toggle-text: #ffffff; --btn-toggle-border: #34495e; --btn-toggle-focus-bg: #53626f; - --group-border: #53626f; + --group-border: #121619; + --input-bg: #ffffff; --input-border: #6a737d; --input-focus-border: #2188ff; --label-hover: #c8e1ff; + /* Paginator */ + --page-list-bg: #ffffff; --page-list-border: #c1c1c1; - --thumbnail-bg: #e1e1e1; - --thumbnail-focus-bg: #51aded; + --thumbnail-bg: #dde1e6; + --thumbnail-focus-bg: #82cfff; --thumbnail-number-bg: #ffffff; - --thumbnail-number-text: #000000; + --thumbnail-number-text: #4d5358; --thumbnail-preview-border: #000000; + /* Editor */ + + --object-status-active: #bef264; + --object-status-inactive: #fde047; + --object-status-deleted: #fca5a5; + --object-status-unknown: #c1c7cd; + --tree-item-focus-bg: #000000; --tree-item-focus-text: #ffffff; diff --git a/client/styles/normalize.css b/client/styles/normalize.css index 2e2610e91..8a3397ee1 100644 --- a/client/styles/normalize.css +++ b/client/styles/normalize.css @@ -1,2 +1,3 @@ -/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ -*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item} \ No newline at end of file +/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ +*,:before,:after{box-sizing:border-box}html{line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4}body{margin:0;font-family:system-ui,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline} +::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item} \ No newline at end of file From d5a34332d47b2fb0f134cb5acbe70941f94a3bb3 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Thu, 9 Feb 2023 16:29:08 -0500 Subject: [PATCH 09/23] 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 ? ( , ",", ] @@ -30,7 +34,11 @@ exports[`ObjectStatus displays the state found in the response 1`] = ` ◉   - Inactive + + Inactive + , ",", ] diff --git a/client/components/edit/children/__snapshots__/Child.test.tsx.snap b/client/components/edit/children/__snapshots__/Child.test.tsx.snap index ca44788a2..be7c5f9bb 100644 --- a/client/components/edit/children/__snapshots__/Child.test.tsx.snap +++ b/client/components/edit/children/__snapshots__/Child.test.tsx.snap @@ -2,16 +2,16 @@ exports[`Child handles empty titles appropriately 1`] = `
, - ";", ] `; diff --git a/client/context/EditorContext.tsx b/client/context/EditorContext.tsx index 6dd6b1ec9..451decbdf 100644 --- a/client/context/EditorContext.tsx +++ b/client/context/EditorContext.tsx @@ -1,6 +1,5 @@ import React, { createContext, useContext, useReducer } from "react"; import { editObjectCatalogUrl, getObjectChildrenUrl, getObjectDetailsUrl, getObjectParentsUrl } from "../util/routes"; -import { useGlobalContext } from "./GlobalContext"; import { useFetchContext } from "./FetchContext"; import { extractFirstMetadataValue as utilExtractFirstMetadataValue } from "../util/metadata"; import { TreeNode } from "../util/Breadcrumbs"; @@ -207,11 +206,6 @@ export const EditorContextProvider = ({ children }) => { }; export const useEditorContext = () => { - const { - action: { - toggleModal - } - } = useGlobalContext(); const { action: { fetchJSON @@ -531,4 +525,4 @@ export default { EditorContextProvider, DatastreamModalStates, useEditorContext -} \ No newline at end of file +} diff --git a/client/context/GlobalContext.tsx b/client/context/GlobalContext.tsx index 9fd40cd7a..f20073c89 100644 --- a/client/context/GlobalContext.tsx +++ b/client/context/GlobalContext.tsx @@ -182,7 +182,7 @@ export default { function systemTheme(): ThemeOption { let defaultTheme = "light" as ThemeOption; - if (typeof window != "undefined") { + if (typeof window != "undefined" && typeof window.matchMedia != "undefined") { if (window.matchMedia("(prefers-color-scheme)").media == "not all") { return defaultTheme; } From ff5ecd57e96a4fab552d04e2e3df908c5bfda643 Mon Sep 17 00:00:00 2001 From: Demian Katz Date: Tue, 9 Jul 2024 08:55:44 -0400 Subject: [PATCH 22/23] Remove whitespace diff. From 4fccbf43803957cd1706ba49a66358923eac466c Mon Sep 17 00:00:00 2001 From: Chris Hallberg Date: Wed, 30 Oct 2024 17:07:27 -0400 Subject: [PATCH 23/23] style: improve ChildList and Child --- client/components/edit/children/Child.tsx | 28 ++++---- .../edit/children/ChildList.module.css | 38 +++++++--- client/components/edit/children/ChildList.tsx | 72 ++++++++++--------- client/styles/client.css | 11 --- 4 files changed, 81 insertions(+), 68 deletions(-) diff --git a/client/components/edit/children/Child.tsx b/client/components/edit/children/Child.tsx index d1da2b2af..bebed4853 100644 --- a/client/components/edit/children/Child.tsx +++ b/client/components/edit/children/Child.tsx @@ -1,3 +1,4 @@ +import styles from "./ChildList.module.css"; import React, { useState } from "react"; import { useEditorContext } from "../../../context/EditorContext"; import ChildList from "./ChildList"; @@ -43,8 +44,8 @@ export const Child = ({ setExpanded(!expanded)}> { expanded - ? - : + ? + : } ); @@ -57,33 +58,36 @@ export const Child = ({ forceThumbs={thumbnail} /> ) : ( - "" + null ); const hasExtraTools = thumbnail || models || showChildCounts; const extraTools = hasExtraTools ? ( - + {thumbnail ? : ""} {showChildCounts ? : ""} {models ? : ""} ) : null; return ( - <> - - - {expandControl} +
+ + + {expandControl}{" "} {loaded && parentPid ? : ""} - {(title.length > 0 ? title : "-") + ` [${pid}]`}{" "} - + {title || "(no title)"} - + + {pid} + + + {loaded ? : ""} {extraTools} {childList} - +
); }; diff --git a/client/components/edit/children/ChildList.module.css b/client/components/edit/children/ChildList.module.css index d4f78c32c..dd5fe81c7 100644 --- a/client/components/edit/children/ChildList.module.css +++ b/client/components/edit/children/ChildList.module.css @@ -1,20 +1,38 @@ .childlist { + margin-block: 0.75rem; +} + +.childlist__list { + margin-block: 0; padding-inline: 0; list-style-type: none; + background-color: white; } -.childlist .childlist { +.childlist__list > li { margin-block: 0; - padding-inline: 1.5rem 0.75rem; + border-block-start: 1px solid var(--page-list-border); } -.childlist p { - margin-inline-start: 1.75rem; - margin-block-end: 0rem; - padding-inline: 0; +.childlist__item { + margin-block: 0; + padding: 0.25rem; } -.childlist li { - margin-block: 0; - padding-block: 0.25rem; - border-top: 1px solid var(--page-list-border, #878d96); +.childlist__expandicon { + vertical-align: bottom; +} + +/* Nested */ + +.childlist__list .childlist { + margin-inline-start: 1.5rem; +} + +/* Pagination alignment */ + +.childlist .pagination { + margin-block-end: 1px; +} +.childlist .pagination li { + margin: 0; } diff --git a/client/components/edit/children/ChildList.tsx b/client/components/edit/children/ChildList.tsx index ca30d2b5e..24f5e7798 100644 --- a/client/components/edit/children/ChildList.tsx +++ b/client/components/edit/children/ChildList.tsx @@ -4,6 +4,7 @@ import { useEditorContext } from "../../../context/EditorContext"; import Child from "./Child"; import SelectableChild from "./SelectableChild"; import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid"; import Pagination from "@mui/material/Pagination"; export interface ChildListProps { @@ -88,34 +89,29 @@ export const ChildList = ({ {showThumbs ? "Hide Thumbnails" : "Show Thumbnails"} ) : null; - const contents = - childDocs.length > 0 ? ( - childDocs.map((child: Record) => { - return ( -
  • - {selectCallback === false ? ( - - ) : ( - - )} -
  • - ); - }) - ) : ( -

    Empty.

    - ); + const contents = childDocs.map((child: Record) => { + return ( +
  • + {selectCallback === false ? ( + + ) : ( + + )} +
  • + ); + }); const pageCount = Math.ceil(children.numFound / pageSize); const paginator = pageCount > 1 ? ( @@ -136,20 +132,26 @@ export const ChildList = ({ const endNumber = startNumber + pageSize - 1; const paginatorLabel = children.numFound > 1 ? ( -

    + <> Showing {startNumber} - {children.numFound < endNumber ? children.numFound : endNumber} of{" "} {children.numFound} -

    + ) : null; return ( - <> +
    {thumbsButton} {modelsButton} {childButton} - {paginatorLabel} - {paginator} -
      {contents}
    - + + + {paginatorLabel} + + + {paginator} + + +
      {contents.length ? contents : Empty.}
    +
    ); }; diff --git a/client/styles/client.css b/client/styles/client.css index 7dd5f6235..d5dd36d46 100644 --- a/client/styles/client.css +++ b/client/styles/client.css @@ -203,14 +203,3 @@ label input[type="radio"] { color: var(--tree-item-focus-text); background-color: var(--tree-item-focus-bg); } - -.child__container { - align-items: center; -} -.child__label { - display: flex; - align-items: center; -} -.child__expand-icon { - vertical-align: bottom; -}