diff --git a/src/renderer/components/layout/top-bar/close-window.injectable.ts b/src/renderer/components/layout/top-bar/close-window.injectable.ts
new file mode 100644
index 0000000000..906cf88427
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/close-window.injectable.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { WindowAction } from "../../../../common/ipc/window";
+import { requestWindowAction } from "../../../ipc";
+
+const closeWindowInjectable = getInjectable({
+ id: "close-window",
+ instantiate: () => () => requestWindowAction(WindowAction.CLOSE),
+ causesSideEffects: true,
+});
+
+export default closeWindowInjectable;
diff --git a/src/renderer/components/layout/top-bar/go-back.injectable.ts b/src/renderer/components/layout/top-bar/go-back.injectable.ts
new file mode 100644
index 0000000000..88a804bd9f
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/go-back.injectable.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { WindowAction } from "../../../../common/ipc/window";
+import { requestWindowAction } from "../../../ipc";
+
+const goBackInjectable = getInjectable({
+ id: "go-back",
+ instantiate: () => () => requestWindowAction(WindowAction.GO_BACK),
+ causesSideEffects: true,
+});
+
+export default goBackInjectable;
diff --git a/src/renderer/components/layout/top-bar/go-forward.injectable.ts b/src/renderer/components/layout/top-bar/go-forward.injectable.ts
new file mode 100644
index 0000000000..a4a69b66f5
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/go-forward.injectable.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { WindowAction } from "../../../../common/ipc/window";
+import { requestWindowAction } from "../../../ipc";
+
+const goForwardInjectable = getInjectable({
+ id: "go-forward",
+ instantiate: () => () => requestWindowAction(WindowAction.GO_FORWARD),
+ causesSideEffects: true,
+});
+
+export default goForwardInjectable;
diff --git a/src/renderer/components/layout/top-bar/maximize-window.injectable.ts b/src/renderer/components/layout/top-bar/maximize-window.injectable.ts
new file mode 100644
index 0000000000..bfcb2916c4
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/maximize-window.injectable.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { WindowAction } from "../../../../common/ipc/window";
+import { requestWindowAction } from "../../../ipc";
+
+const maximizeWindowInjectable = getInjectable({
+ id: "maximize-window",
+ instantiate: () => () => requestWindowAction(WindowAction.MINIMIZE),
+ causesSideEffects: true,
+});
+
+export default maximizeWindowInjectable;
diff --git a/src/renderer/components/layout/top-bar/next-enabled.injectable.ts b/src/renderer/components/layout/top-bar/next-enabled.injectable.ts
new file mode 100644
index 0000000000..1347327ba6
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/next-enabled.injectable.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import topBarStateInjectable from "./state.injectable";
+
+const topBarNextEnabledInjectable = getInjectable({
+ id: "top-bar-next-enabled",
+ instantiate: (di) => {
+ const state = di.inject(topBarStateInjectable);
+
+ return computed(() => state.nextEnabled);
+ },
+});
+
+export default topBarNextEnabledInjectable;
diff --git a/src/renderer/components/layout/top-bar/open-app-context-menu.injectable.ts b/src/renderer/components/layout/top-bar/open-app-context-menu.injectable.ts
new file mode 100644
index 0000000000..4d2bf911d3
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/open-app-context-menu.injectable.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { emitOpenAppMenuAsContextMenu } from "../../../ipc";
+
+const openAppContextMenuInjectable = getInjectable({
+ id: "open-app-context-menu",
+ instantiate: () => emitOpenAppMenuAsContextMenu,
+ causesSideEffects: true,
+});
+
+export default openAppContextMenuInjectable;
diff --git a/src/renderer/components/layout/top-bar/prev-enabled.injectable.ts b/src/renderer/components/layout/top-bar/prev-enabled.injectable.ts
new file mode 100644
index 0000000000..fcdf318e34
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/prev-enabled.injectable.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import topBarStateInjectable from "./state.injectable";
+
+const topBarPrevEnabledInjectable = getInjectable({
+ id: "top-bar-prev-enabled",
+ instantiate: (di) => {
+ const state = di.inject(topBarStateInjectable);
+
+ return computed(() => state.prevEnabled);
+ },
+});
+
+export default topBarPrevEnabledInjectable;
diff --git a/src/renderer/components/layout/top-bar/start-state-sync.injectable.ts b/src/renderer/components/layout/top-bar/start-state-sync.injectable.ts
new file mode 100644
index 0000000000..7ca881d527
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/start-state-sync.injectable.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { action } from "mobx";
+import { beforeFrameStartsInjectionToken } from "../../../before-frame-starts/before-frame-starts-injection-token";
+import ipcRendererInjectable from "../../../utils/channel/ipc-renderer.injectable";
+import topBarStateInjectable from "./state.injectable";
+
+// TODO: replace with a SyncBox
+const startTopbarStateSyncInjectable = getInjectable({
+ id: "start-topbar-state-sync",
+ instantiate: (di) => {
+ const state = di.inject(topBarStateInjectable);
+ const ipcRenderer = di.inject(ipcRendererInjectable);
+
+ return {
+ run: () => {
+ ipcRenderer.on("history:can-go-back", action((event, canGoBack: boolean) => {
+ state.prevEnabled = canGoBack;
+ }));
+
+ ipcRenderer.on("history:can-go-forward", action((event, canGoForward: boolean) => {
+ state.nextEnabled = canGoForward;
+ }));
+ },
+ };
+ },
+ injectionToken: beforeFrameStartsInjectionToken,
+});
+
+export default startTopbarStateSyncInjectable;
diff --git a/src/renderer/components/layout/top-bar/state.injectable.ts b/src/renderer/components/layout/top-bar/state.injectable.ts
new file mode 100644
index 0000000000..afc826a607
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/state.injectable.ts
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { observable } from "mobx";
+
+const topBarStateInjectable = getInjectable({
+ id: "top-bar-state",
+ instantiate: () => observable.object({
+ prevEnabled: false,
+ nextEnabled: false,
+ }),
+});
+
+export default topBarStateInjectable;
diff --git a/src/renderer/components/layout/top-bar/toggle-maximize-window.injectable.ts b/src/renderer/components/layout/top-bar/toggle-maximize-window.injectable.ts
new file mode 100644
index 0000000000..3ebecc3b39
--- /dev/null
+++ b/src/renderer/components/layout/top-bar/toggle-maximize-window.injectable.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { WindowAction } from "../../../../common/ipc/window";
+import { requestWindowAction } from "../../../ipc";
+
+const toggleMaximizeWindowInjectable = getInjectable({
+ id: "toggle-maximize-window",
+ instantiate: () => () => requestWindowAction(WindowAction.TOGGLE_MAXIMIZE),
+ causesSideEffects: true,
+});
+
+export default toggleMaximizeWindowInjectable;
diff --git a/src/renderer/components/layout/top-bar/top-bar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx
index 9355da74ae..2a8ce964a1 100644
--- a/src/renderer/components/layout/top-bar/top-bar.test.tsx
+++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx
@@ -12,163 +12,125 @@ import type { DiContainer } from "@ogre-tools/injectable";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
-import { computed } from "mobx";
-import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
-import mockFs from "mock-fs";
+import { computed, observable } from "mobx";
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
-
-jest.mock("../../../../common/vars", () => {
- const { SemVer } = require("semver");
-
- return {
- ...jest.requireActual<{}>("../../../../common/vars"),
- appSemVer: new SemVer("1.0.0"),
- };
-});
-
-const goBack = jest.fn();
-const goForward = jest.fn();
-
-jest.mock(
- "electron",
- () => ({
- ipcRenderer: {
- on: jest.fn(
- (channel: string, listener: (event: any, ...args: any[]) => void) => {
- if (channel === "history:can-go-back") {
- listener({}, true);
- }
-
- if (channel === "history:can-go-forward") {
- listener({}, true);
- }
- },
- ),
- invoke: jest.fn(
- (channel: string, action: string) => {
- console.log("channel", channel, action);
-
- if (channel !== "window:window-action") return;
-
- switch(action) {
- case "back": {
- goBack();
- break;
- }
-
- case "forward": {
- goForward();
- break;
- }
- }
- },
- ),
- },
- }),
-);
-
-jest.mock("../../+catalog", () => ({
- previousActiveTab: jest.fn(),
-}));
+import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
+import closeWindowInjectable from "./close-window.injectable";
+import goBackInjectable from "./go-back.injectable";
+import goForwardInjectable from "./go-forward.injectable";
+import maximizeWindowInjectable from "./maximize-window.injectable";
+import openAppContextMenuInjectable from "./open-app-context-menu.injectable";
+import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
+import topBarStateInjectable from "./state.injectable";
describe("", () => {
let di: DiContainer;
let render: DiRender;
+ let goBack: jest.MockedFunction<() => void>;
+ let goForward: jest.MockedFunction<() => void>;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
- mockFs();
-
- di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
+ di.override(rendererExtensionsInjectable, () => computed(() => []));
+ di.override(openAppContextMenuInjectable, () => jest.fn());
+ di.override(goBackInjectable, () => goBack = jest.fn());
+ di.override(goForwardInjectable, () => goForward = jest.fn());
+ di.override(closeWindowInjectable, () => jest.fn());
+ di.override(maximizeWindowInjectable, () => jest.fn());
+ di.override(toggleMaximizeWindowInjectable, () => jest.fn());
render = renderFor(di);
});
- afterEach(() => {
- mockFs.restore();
- });
+ describe("with both previous and next history enabled", () => {
+ beforeEach(() => {
+ di.override(topBarStateInjectable, () => observable.object({
+ prevEnabled: true,
+ nextEnabled: true,
+ }));
+ });
+ it("renders w/o errors", () => {
+ const { container } = render();
- it("renders w/o errors", () => {
- const { container } = render();
+ expect(container).toBeInstanceOf(HTMLElement);
+ });
- expect(container).toBeInstanceOf(HTMLElement);
- });
+ it("renders home button", async () => {
+ const { findByTestId } = render();
- it("renders home button", async () => {
- const { findByTestId } = render();
+ expect(await findByTestId("home-button")).toBeInTheDocument();
+ });
- expect(await findByTestId("home-button")).toBeInTheDocument();
- });
+ it("renders history arrows", async () => {
+ const { findByTestId } = render();
- it("renders history arrows", async () => {
- const { findByTestId } = render();
+ expect(await findByTestId("history-back")).toBeInTheDocument();
+ expect(await findByTestId("history-forward")).toBeInTheDocument();
+ });
- expect(await findByTestId("history-back")).toBeInTheDocument();
- expect(await findByTestId("history-forward")).toBeInTheDocument();
- });
+ it("enables arrow by ipc event", async () => {
+ const { findByTestId } = render();
- it("enables arrow by ipc event", async () => {
- const { findByTestId } = render();
+ expect(await findByTestId("history-back")).not.toHaveClass("disabled");
+ expect(await findByTestId("history-forward")).not.toHaveClass("disabled");
+ });
- expect(await findByTestId("history-back")).not.toHaveClass("disabled");
- expect(await findByTestId("history-forward")).not.toHaveClass("disabled");
- });
+ it("triggers browser history back and forward", async () => {
+ const { findByTestId } = render();
- it("triggers browser history back and forward", async () => {
- const { findByTestId } = render();
+ const prevButton = await findByTestId("history-back");
+ const nextButton = await findByTestId("history-forward");
- const prevButton = await findByTestId("history-back");
- const nextButton = await findByTestId("history-forward");
+ fireEvent.click(prevButton);
- fireEvent.click(prevButton);
+ expect(goBack).toBeCalled();
- expect(goBack).toBeCalled();
+ fireEvent.click(nextButton);
- fireEvent.click(nextButton);
+ expect(goForward).toBeCalled();
+ });
- expect(goForward).toBeCalled();
- });
+ it("renders items", async () => {
+ const testId = "testId";
+ const text = "an item";
- it("renders items", async () => {
- const testId = "testId";
- const text = "an item";
-
- di.override(topBarItemsInjectable, () => computed(() => [
- {
- components: {
- Item: () => {text},
+ di.override(topBarItemsInjectable, () => computed(() => [
+ {
+ components: {
+ Item: () => {text},
+ },
},
- },
- ]));
+ ]));
- const { findByTestId } = render();
+ const { findByTestId } = render();
- expect(await findByTestId(testId)).toHaveTextContent(text);
- });
+ expect(await findByTestId(testId)).toHaveTextContent(text);
+ });
- it("doesn't show windows title buttons on macos", () => {
- di.override(isLinuxInjectable, () => false);
- di.override(isWindowsInjectable, () => false);
+ it("doesn't show windows title buttons on macos", () => {
+ di.override(isLinuxInjectable, () => false);
+ di.override(isWindowsInjectable, () => false);
- const { queryByTestId } = render();
+ const { queryByTestId } = render();
- expect(queryByTestId("window-menu")).not.toBeInTheDocument();
- expect(queryByTestId("window-minimize")).not.toBeInTheDocument();
- expect(queryByTestId("window-maximize")).not.toBeInTheDocument();
- expect(queryByTestId("window-close")).not.toBeInTheDocument();
- });
+ expect(queryByTestId("window-menu")).not.toBeInTheDocument();
+ expect(queryByTestId("window-minimize")).not.toBeInTheDocument();
+ expect(queryByTestId("window-maximize")).not.toBeInTheDocument();
+ expect(queryByTestId("window-close")).not.toBeInTheDocument();
+ });
- it("does show windows title buttons on linux", () => {
- di.override(isLinuxInjectable, () => true);
- di.override(isWindowsInjectable, () => false);
+ it("does show windows title buttons on linux", () => {
+ di.override(isLinuxInjectable, () => true);
+ di.override(isWindowsInjectable, () => false);
- const { queryByTestId } = render();
+ const { queryByTestId } = render();
- expect(queryByTestId("window-menu")).toBeInTheDocument();
- expect(queryByTestId("window-minimize")).toBeInTheDocument();
- expect(queryByTestId("window-maximize")).toBeInTheDocument();
- expect(queryByTestId("window-close")).toBeInTheDocument();
+ expect(queryByTestId("window-menu")).toBeInTheDocument();
+ expect(queryByTestId("window-minimize")).toBeInTheDocument();
+ expect(queryByTestId("window-maximize")).toBeInTheDocument();
+ expect(queryByTestId("window-close")).toBeInTheDocument();
+ });
});
});
diff --git a/src/renderer/components/layout/top-bar/top-bar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx
index adc5457747..50b3c84f1c 100644
--- a/src/renderer/components/layout/top-bar/top-bar.tsx
+++ b/src/renderer/components/layout/top-bar/top-bar.tsx
@@ -8,15 +8,11 @@ import React, { useEffect, useRef } from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import { Icon } from "../../icon";
-import { observable } from "mobx";
-import { ipcRendererOn } from "../../../../common/ipc";
import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { cssNames, noop } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration";
-import { emitOpenAppMenuAsContextMenu, requestWindowAction } from "../../../ipc";
-import { WindowAction } from "../../../../common/ipc/window";
import isLinuxInjectable from "../../../../common/vars/is-linux.injectable";
import isWindowsInjectable from "../../../../common/vars/is-windows.injectable";
import type { NavigateToCatalog } from "../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
@@ -24,6 +20,14 @@ import navigateToCatalogInjectable from "../../../../common/front-end-routing/ro
import catalogRouteInjectable from "../../../../common/front-end-routing/routes/catalog/catalog-route.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { UpdateButton } from "../../update-button";
+import topBarPrevEnabledInjectable from "./prev-enabled.injectable";
+import topBarNextEnabledInjectable from "./next-enabled.injectable";
+import openAppContextMenuInjectable from "./open-app-context-menu.injectable";
+import goBackInjectable from "./go-back.injectable";
+import goForwardInjectable from "./go-forward.injectable";
+import closeWindowInjectable from "./close-window.injectable";
+import maximizeWindowInjectable from "./maximize-window.injectable";
+import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
interface Dependencies {
navigateToCatalog: NavigateToCatalog;
@@ -31,62 +35,43 @@ interface Dependencies {
items: IComputedValue;
isWindows: boolean;
isLinux: boolean;
+ prevEnabled: IComputedValue;
+ nextEnabled: IComputedValue;
+ openAppContextMenu: () => void;
+ goBack: () => void;
+ goForward: () => void;
+ minimizeWindow: () => void;
+ toggleMaximizeWindow: () => void;
+ closeWindow: () => void;
}
-const prevEnabled = observable.box(false);
-const nextEnabled = observable.box(false);
-
-ipcRendererOn("history:can-go-back", (event, state: boolean) => {
- prevEnabled.set(state);
-});
-
-ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
- nextEnabled.set(state);
-});
-
const NonInjectedTopBar = observer(({
items,
navigateToCatalog,
catalogRouteIsActive,
isWindows,
isLinux,
+ prevEnabled,
+ nextEnabled,
+ openAppContextMenu,
+ goBack,
+ goForward,
+ closeWindow,
+ minimizeWindow,
+ toggleMaximizeWindow,
}: Dependencies) => {
const elem = useRef(null);
- const openAppContextMenu = () => {
- emitOpenAppMenuAsContextMenu();
- };
-
const goHome = () => {
navigateToCatalog();
};
- const goBack = () => {
- requestWindowAction(WindowAction.GO_BACK);
- };
-
- const goForward = () => {
- requestWindowAction(WindowAction.GO_FORWARD);
- };
-
const windowSizeToggle = (evt: React.MouseEvent) => {
if (elem.current === evt.target) {
- toggleMaximize();
+ toggleMaximizeWindow();
}
};
- const minimizeWindow = () => {
- requestWindowAction(WindowAction.MINIMIZE);
- };
-
- const toggleMaximize = () => {
- requestWindowAction(WindowAction.TOGGLE_MAXIMIZE);
- };
-
- const closeWindow = () => {
- requestWindowAction(WindowAction.CLOSE);
- };
-
useEffect(() => watchHistoryState(), []);
return (
@@ -147,14 +132,14 @@ const NonInjectedTopBar = observer(({
width="10"
height="1"
x="1"
- y="9"
+ y="9"
/>
@@ -193,23 +178,23 @@ const renderRegisteredItems = (items: TopBarRegistration[]) => (
})
);
-export const TopBar = withInjectables(
- NonInjectedTopBar,
- {
- getProps: (di) => {
- const catalogRoute = di.inject(catalogRouteInjectable);
-
- return {
- navigateToCatalog: di.inject(navigateToCatalogInjectable),
- items: di.inject(topBarItemsInjectable),
- isLinux: di.inject(isLinuxInjectable),
- isWindows: di.inject(isWindowsInjectable),
-
- catalogRouteIsActive: di.inject(
- routeIsActiveInjectable,
- catalogRoute,
- ),
- };
- },
- },
-);
+export const TopBar = withInjectables(NonInjectedTopBar, {
+ getProps: (di) => ({
+ navigateToCatalog: di.inject(navigateToCatalogInjectable),
+ items: di.inject(topBarItemsInjectable),
+ isLinux: di.inject(isLinuxInjectable),
+ isWindows: di.inject(isWindowsInjectable),
+ prevEnabled: di.inject(topBarPrevEnabledInjectable),
+ nextEnabled: di.inject(topBarNextEnabledInjectable),
+ catalogRouteIsActive: di.inject(
+ routeIsActiveInjectable,
+ di.inject(catalogRouteInjectable),
+ ),
+ openAppContextMenu: di.inject(openAppContextMenuInjectable),
+ goBack: di.inject(goBackInjectable),
+ goForward: di.inject(goForwardInjectable),
+ closeWindow: di.inject(closeWindowInjectable),
+ minimizeWindow: di.inject(maximizeWindowInjectable),
+ toggleMaximizeWindow: di.inject(toggleMaximizeWindowInjectable),
+ }),
+});