Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application-menu, given platform is 'darwin' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> mac",
"root -> mac -> about",
"root -> mac -> separator-1",
"root -> mac -> navigate-to-preferences",
"root -> mac -> navigate-to-extensions",
"root -> mac -> separator-2",
"root -> mac -> services",
"root -> mac -> separator-3",
"root -> mac -> hide",
"root -> mac -> hide-others",
"root -> mac -> unhide",
"root -> mac -> separator-4",
"root -> mac -> quit",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> separator-1-for-file",
"root -> file -> close-window",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
]
`;
exports[`application-menu, given platform is 'linux' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
exports[`application-menu, given platform is 'win32' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
@@ -0,0 +1,226 @@
/**
* 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 { noop } from "lodash/fp";
import { runInAction } from "mobx";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import logErrorInjectable from "../../common/log-error.injectable";
describe("application-menu-in-legacy-extension-api", () => {
let builder: ApplicationBuilder;
let logErrorMock: jest.Mock;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.beforeApplicationStart(
(mainDi) => {
runInAction(() => {
mainDi.register(
someTopMenuItemInjectable,
someNonExtensionBasedMenuItemInjectable,
);
});
logErrorMock = jest.fn();
mainDi.override(logErrorInjectable, () => logErrorMock);
},
);
await builder.startHidden();
});
describe("when extension with application menu items is enabled", () => {
let onClickMock: jest.Mock;
let testExtensionOptions: FakeExtensionOptions;
beforeEach(() => {
onClickMock = jest.fn();
testExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-non-shown-item",
parentId: "some-top-menu-item",
click: noop,
label: "Irrelevant",
visible: false,
},
{
id: "some-clickable-item",
parentId: "some-top-menu-item",
click: onClickMock,
},
{
parentId: "some-top-menu-item",
type: "separator",
},
{
id: "some-os-action-menu-item-id",
parentId: "some-top-menu-item",
role: "help",
},
{
id: "some-submenu-with-explicit-children",
parentId: "some-top-menu-item",
submenu: [
{ id: "some-explicit-child", label: "Some explicit child", click: noop },
],
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("related menu items exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
// Note: anonymous index "1" is used by the non-visible menu item.
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
it("when the extension-based clickable menu item is clicked, does so", () => {
builder.applicationMenu.click(
"root", "some-top-menu-item", "some-extension-name/some-clickable-item",
);
expect(onClickMock).toHaveBeenCalled();
});
describe("when the extension is disabled", () => {
beforeEach(() => {
builder.extensions.disable(testExtensionOptions);
});
it("when related menu items no longer exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([]);
});
it("when the extension is enabled again, also related menu items exist again", () => {
builder.extensions.enable(testExtensionOptions);
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
});
});
describe("when extension with unrecognizable application menu items is enabled", () => {
beforeEach(() => {
const testExtensionOptions: FakeExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-recognizable-item",
parentId: "some-top-menu-item",
click: noop,
},
{
id: "some-unrecognizable-item",
parentId: "some-top-menu-item",
// Note: there is no way to recognize this
// click: noop,
// role: "help"
// submenu: []
// type: "separator"
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("only recognizable menu items from extension exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-recognizable-item"],
]);
});
it("logs about the unrecognizable item", () => {
expect(logErrorMock).toHaveBeenCalledWith(
'[MENU]: Tried to register menu item "some-extension-name/some-unrecognizable-item" but it is not recognizable as any of ApplicationMenuItemTypes',
);
});
});
});
const someTopMenuItemInjectable = getInjectable({
id: "some-top-menu-item",
instantiate: () => ({
id: "some-top-menu-item",
parentId: "root" as const,
kind: "top-level-menu" as const,
label: "Some existing root menu item",
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
const someNonExtensionBasedMenuItemInjectable = getInjectable({
id: "some-non-extension-based-menu-item",
instantiate: () => ({
id: "some-non-extension-based-menu-item",
parentId: "some-top-menu-item",
kind: "clickable-menu-item" as const,
label: "Some menu item",
onClick: () => {},
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import platformInjectable, { allPlatforms } from "../../common/vars/platform.injectable";
describe.each(allPlatforms)("application-menu, given platform is '%s'", (platform) => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
beforeEach(async () => {
testUsingFakeTime();
populateApplicationMenuMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(platformInjectable, () => platform);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
it("when insufficient time passes, does not populate menu items yet", () => {
advanceFakeTime(99);
expect(populateApplicationMenuMock).not.toHaveBeenCalled();
});
describe("given enough time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("populates application menu with at least something", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("populates application menu", () => {
expect(applicationMenuPaths.map(x => x.join(" -> "))).toMatchSnapshot();
});
});
});
@@ -0,0 +1,81 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import { runInAction } from "mobx";
import logErrorInjectable from "../../common/log-error.injectable";
describe("handling-of-orphan-application-menu-items, given orphan menu item", () => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
let logErrorMock: jest.Mock;
beforeEach(async () => {
testUsingFakeTime();
populateApplicationMenuMock = jest.fn();
logErrorMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
const someOrphanMenuItemInjectable = getInjectable({
id: "some-orphan-menu-item",
instantiate: () => ({
kind: "sub-menu" as const,
id: "some-item-id",
// Note: unknown id makes this item an orphan.
parentId: "some-unknown-parent-id",
orderNumber: 0,
label: "irrelevant",
}),
injectionToken: applicationMenuItemInjectionToken,
});
runInAction(() => {
mainDi.register(someOrphanMenuItemInjectable);
});
mainDi.override(logErrorInjectable, () => logErrorMock);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
describe("given some time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("keeps showing the other application menu items without throwing", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("does not show orphan application menu item", () => {
expect(applicationMenuPaths.find(x => x.join(".").endsWith("some-item-id")));
});
it("logs about bad menu item", () => {
expect(logErrorMock).toHaveBeenCalledWith('[MENU]: cannot render menu item for missing parentIds: "some-unknown-parent-id"');
});
});
});
@@ -0,0 +1,64 @@
/**
* 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 applicationMenuItemsInjectable from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import { getCompositeFor } from "../../../common/utils/composite/get-composite/get-composite";
import { computed } from "mobx";
import { pipeline } from "@ogre-tools/fp";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import type { RootComposite } from "../../../common/utils/composite/interfaces";
import type { Discriminable } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable";
import logErrorInjectable from "../../../common/log-error.injectable";
import { isShown } from "../../../common/utils/composable-responsibilities/showable/showable";
export type MenuItemRoot = Discriminable<"root"> & RootComposite<"root">;
const applicationMenuItemCompositeInjectable = getInjectable({
id: "application-menu-item-composite",
instantiate: (di) => {
const menuItems = di.inject(applicationMenuItemsInjectable);
const logError = di.inject(logErrorInjectable);
return computed((): Composite<ApplicationMenuItemTypes | MenuItemRoot> => {
const items = menuItems.get();
return pipeline(
[
{
parentId: undefined,
id: "root",
kind: "root",
} as const,
...items,
],
getCompositeFor({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: (children) =>
pipeline(
children,
orderByOrderNumber,
(children) => children.filter(isShown),
),
handleMissingParentIds: ({ missingParentIds }) => {
logError(
`[MENU]: cannot render menu item for missing parentIds: "${missingParentIds.join(
'", "',
)}"`,
);
},
}),
);
});
},
});
export default applicationMenuItemCompositeInjectable;
@@ -0,0 +1,163 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed } from "mobx";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensExtension } from "../../../extensions/lens-extension";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type {
ApplicationMenuItemTypes,
ClickableMenuItem,
OsActionMenuItem,
Separator,
} from "./menu-items/application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import type { MenuRegistration } from "./menu-registration";
import logErrorInjectable from "../../../common/log-error.injectable";
const applicationMenuItemRegistratorInjectable = getInjectable({
id: "application-menu-item-registrator",
instantiate: (di) => {
const logError = di.inject(logErrorInjectable);
const toRecursedInjectables = toRecursedInjectablesFor(logError);
return (ext: LensExtension) => {
const mainExtension = ext as LensMainExtension;
return computed(() => {
const appMenus = Array.isArray(mainExtension.appMenus) ? mainExtension.appMenus : mainExtension.appMenus.get();
return appMenus.flatMap(
toRecursedInjectables([mainExtension.sanitizedExtensionId]),
);
});
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default applicationMenuItemRegistratorInjectable;
const toRecursedInjectablesFor = (logError: (errorMessage: string) => void) => {
const toRecursedInjectables = (previousIdPath: string[]) =>
(
registration: MenuRegistration,
index: number,
// Todo: new version of injectable would require less type parameters with defaults.
): Injectable<
ApplicationMenuItemTypes,
ApplicationMenuItemTypes,
void
>[] => {
const previousIdPathString = previousIdPath.join("/");
const registrationId = registration.id || index.toString();
const currentIdPath = [...previousIdPath, registrationId];
const currentIdPathString = currentIdPath.join("/");
const parentId = registration.parentId || previousIdPathString;
const menuItem = getApplicationMenuItem({
registration,
parentId,
currentIdPathString,
index,
});
if (!menuItem) {
logError(`[MENU]: Tried to register menu item "${currentIdPathString}" but it is not recognizable as any of ApplicationMenuItemTypes`);
return [];
}
return [
getInjectable({
id: `${currentIdPathString}/application-menu-item`,
instantiate: () => menuItem,
injectionToken: applicationMenuItemInjectionToken,
}),
...((registration.submenu as MenuRegistration[])
? (registration.submenu as MenuRegistration[]).flatMap(
toRecursedInjectables(currentIdPath),
)
: []),
];
};
return toRecursedInjectables;
};
const getApplicationMenuItem = ({
registration,
index,
currentIdPathString,
parentId,
}: {
registration: MenuRegistration;
index: number;
currentIdPathString: string;
parentId: string;
}): ApplicationMenuItemTypes | undefined => {
const orderNumber = 1000 + index * 10;
if (registration.type === "separator") {
return {
kind: "separator" as const,
id: `${currentIdPathString}-separator`,
parentId,
orderNumber,
} as Separator;
}
if (registration.submenu) {
return {
kind: "sub-menu" as const,
id: currentIdPathString,
parentId,
isShown: registration.visible ?? true,
orderNumber,
label: registration.label || "",
};
}
if (registration.click) {
return {
kind: "clickable-menu-item" as const,
id: currentIdPathString,
parentId,
// Todo: hide electron events from this abstraction.
onClick: registration.click,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as ClickableMenuItem;
}
if (registration.role) {
return {
kind: "os-action-menu-item" as const,
id: currentIdPathString,
parentId,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
actionName: registration.role,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as OsActionMenuItem;
}
return undefined;
};
@@ -0,0 +1,28 @@
/**
* 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 type { MenuItemConstructorOptions } from "electron";
import { computed } from "mobx";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
export interface MenuItemOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
return computed(() =>
computedInjectMany(applicationMenuItemInjectionToken).get(),
);
},
});
export default applicationMenuItemsInjectable;
@@ -0,0 +1,27 @@
/**
* 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 { autorun } from "mobx";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable";
const applicationMenuReactivityInjectable = getInjectable({
id: "application-menu-reactivity",
instantiate: (di) => {
const applicationMenuItemComposite = di.inject(applicationMenuItemCompositeInjectable);
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
return getStartableStoppable(
"application-menu-reactivity",
() => autorun(() => populateApplicationMenu(applicationMenuItemComposite.get()), {
delay: 100,
}),
);
},
});
export default applicationMenuReactivityInjectable;
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { BrowserWindow, KeyboardEvent, MenuItemConstructorOptions, MenuItem as ElectronMenuItem } from "electron";
import type { SetOptional } from "type-fest";
import type { ChildOfParentComposite, ParentOfChildComposite } from "../../../../common/utils/composite/interfaces";
import type { MaybeShowable } from "../../../../common/utils/composable-responsibilities/showable/showable";
import type { Discriminable } from "../../../../common/utils/composable-responsibilities/discriminable/discriminable";
import type { Orderable } from "../../../../common/utils/composable-responsibilities/orderable/orderable";
export interface MayHaveKeyboardShortcut {
keyboardShortcut?: string;
}
export interface ElectronClickable {
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void;
}
export interface Labeled {
label: string;
}
export interface MaybeLabeled extends SetOptional<Labeled, "label"> {}
type ApplicationMenuItemType<T extends string> =
// Note: "kind" is being used for Discriminated unions of TypeScript to achieve type narrowing.
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
& Discriminable<T>
& ParentOfChildComposite
& ChildOfParentComposite
& MaybeShowable
& Orderable;
export type TopLevelMenu =
& ApplicationMenuItemType<"top-level-menu">
& { parentId: "root" }
& Labeled
& MayHaveElectronRole;
interface MayHaveElectronRole {
role?: ElectronRoles;
}
type ElectronRoles = Exclude<MenuItemConstructorOptions["role"], undefined>;
export type SubMenu =
& ApplicationMenuItemType<"sub-menu">
& Labeled
& ChildOfParentComposite;
export type ClickableMenuItem =
& ApplicationMenuItemType<"clickable-menu-item">
& MenuItem
& Labeled
& ElectronClickable;
export type OsActionMenuItem =
& ApplicationMenuItemType<"os-action-menu-item">
& MenuItem
& MaybeLabeled
& TriggersElectronAction;
type MenuItem =
& ChildOfParentComposite
& MayHaveKeyboardShortcut;
interface TriggersElectronAction {
actionName: ElectronRoles;
}
// Todo: SeparatorMenuItem
export type Separator =
& ApplicationMenuItemType<"separator">
& ChildOfParentComposite;
export type ApplicationMenuItemTypes =
| TopLevelMenu
| SubMenu
| OsActionMenuItem
| ClickableMenuItem
| Separator
;
const applicationMenuItemInjectionToken = getInjectionToken<ApplicationMenuItemTypes>({
id: "application-menu-item-injection-token",
});
export default applicationMenuItemInjectionToken;
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const editMenuItemInjectable = getInjectable({
id: "edit-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "edit",
parentId: "root" as const,
orderNumber: 30,
label: "Edit",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default editMenuItemInjectable;
@@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getApplicationMenuOperationSystemActionInjectable } from "../../get-application-menu-operation-system-action-injectable";
import { getApplicationMenuSeparatorInjectable } from "../../get-application-menu-separator-injectable";
export const actionForUndo = getApplicationMenuOperationSystemActionInjectable({
id: "undo",
parentId: "edit",
orderNumber: 10,
actionName: "undo",
});
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
id: "redo",
parentId: "edit",
orderNumber: 20,
actionName: "redo",
});
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-in-edit",
parentId: "edit",
orderNumber: 30,
});
export const actionForCut = getApplicationMenuOperationSystemActionInjectable({
id: "cut",
parentId: "edit",
orderNumber: 40,
actionName: "cut",
});
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
id: "copy",
parentId: "edit",
orderNumber: 50,
actionName: "copy",
});
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
id: "paste",
parentId: "edit",
orderNumber: 60,
actionName: "paste",
});
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
id: "delete",
parentId: "edit",
orderNumber: 70,
actionName: "delete",
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-in-edit",
parentId: "edit",
orderNumber: 80,
});
export const actionForSelectAll = getApplicationMenuOperationSystemActionInjectable({
id: "selectAll",
parentId: "edit",
orderNumber: 90,
actionName: "selectAll",
});
@@ -0,0 +1,32 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToAddClusterInjectable from "../../../../../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
const addClusterMenuItemInjectable = getInjectable({
id: "add-cluster-application-menu-item",
instantiate: (di) => {
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "file",
id: "add-cluster",
orderNumber: 10,
label: "Add Cluster",
keyboardShortcut: "CmdOrCtrl+Shift+A",
onClick: () => {
navigateToAddCluster();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default addClusterMenuItemInjectable;
@@ -0,0 +1,30 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const closeWindowMenuItemInjectable = getInjectable({
id: "close-window-application-menu-item",
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
return {
id: "close-window",
kind: "os-action-menu-item" as const,
parentId: "file",
orderNumber: 60,
actionName: "close" as const,
label: "Close Window",
keyboardShortcut: "Shift+Cmd+W",
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default closeWindowMenuItemInjectable;
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const fileMenuItemInjectable = getInjectable({
id: "file-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "file",
parentId: "root" as const,
orderNumber: 20,
label: "File",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default fileMenuItemInjectable;
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-file",
parentId: "file",
orderNumber: 20,
isShownOnlyOnMac: true,
});
@@ -0,0 +1,25 @@
/**
* 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 type { OsActionMenuItem } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
const getApplicationMenuOperationSystemActionInjectable = ({
id,
...rest
}: Omit<OsActionMenuItem, "kind" >) =>
getInjectable({
id: `application-menu-operation-system-action/${id}`,
instantiate: () => ({
...rest,
id,
kind: "os-action-menu-item" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuOperationSystemActionInjectable };
@@ -0,0 +1,36 @@
/**
* 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 type { Separator } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
const getApplicationMenuSeparatorInjectable = ({
id,
isShownOnlyOnMac = false,
...rest
}: { isShownOnlyOnMac?: boolean } & Omit<
Separator,
"kind" | "isShown"
>) =>
getInjectable({
id: `application-menu-separator/${id}`,
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
const isShown = isShownOnlyOnMac ? isMac : true;
return {
...rest,
id,
kind: "separator" as const,
isShown,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuSeparatorInjectable };
@@ -0,0 +1,23 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const helpMenuItemInjectable = getInjectable({
id: "help-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "help",
parentId: "root" as const,
orderNumber: 50,
label: "Help",
role: "help" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default helpMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToWelcomeInjectable from "../../../../../../common/front-end-routing/routes/welcome/navigate-to-welcome.injectable";
const navigateToWelcomeMenuItem = getInjectable({
id: "navigate-to-welcome-menu-item",
instantiate: (di) => {
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "navigate-to-welcome",
orderNumber: 10,
label: "Welcome",
onClick: () => {
navigateToWelcome();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToWelcomeMenuItem;
@@ -0,0 +1,37 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { docsUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openDocumentationMenuItemInjectable = getInjectable({
id: "open-documentation-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-documentation",
orderNumber: 20,
label: "Documentation",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openDocumentationMenuItemInjectable;
@@ -0,0 +1,37 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { supportUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openSupportItemInjectable = getInjectable({
id: "open-support-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-support",
orderNumber: 30,
label: "Support",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openSupportItemInjectable;
@@ -0,0 +1,34 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToExtensionsInjectable from "../../../../../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const navigateToExtensionsMenuItem = getInjectable({
id: "navigate-to-extensions-menu-item",
instantiate: (di) => {
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: isMac ? "mac" : "file",
id: "navigate-to-extensions",
orderNumber: isMac ? 50 : 40,
label: "Extensions",
keyboardShortcut: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
onClick: () => {
navigateToExtensions();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToExtensionsMenuItem;
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForServices = getApplicationMenuOperationSystemActionInjectable({
id: "services",
parentId: "mac",
orderNumber: 80,
actionName: "services",
});
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
id: "hide",
parentId: "mac",
orderNumber: 100,
actionName: "hide",
});
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
id: "hide-others",
parentId: "mac",
orderNumber: 110,
actionName: "hideOthers",
});
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
id: "unhide",
parentId: "mac",
orderNumber: 120,
actionName: "unhide",
});
@@ -0,0 +1,30 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
import appNameInjectable from "../../../../../common/vars/app-name.injectable";
import isMacInjectable from "../../../../../common/vars/is-mac.injectable";
const primaryMenuItemInjectable = getInjectable({
id: "primary-application-menu-item",
instantiate: (di) => {
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "mac",
orderNumber: 10,
label: appName,
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default primaryMenuItemInjectable;
@@ -0,0 +1,35 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import stopServicesAndExitAppInjectable from "../../../../../../main/stop-services-and-exit-app.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const quitApplicationMenuItemInjectable = getInjectable({
id: "quit-application-menu-item",
instantiate: (di) => {
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "quit",
label: "Quit",
parentId: isMac ? "mac" : "file",
orderNumber: isMac ? 140 : 70,
keyboardShortcut: isMac ? "Cmd+Q" : "Alt+F4",
onClick: () => {
stopServicesAndExitApp();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default quitApplicationMenuItemInjectable;
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1",
parentId: "mac",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2",
parentId: "mac",
orderNumber: 70,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3",
parentId: "mac",
orderNumber: 90,
});
export const separator4 = getApplicationMenuSeparatorInjectable({
id: "separator-4",
parentId: "mac",
orderNumber: 130,
});
@@ -0,0 +1,35 @@
/**
* 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 productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import showAboutInjectable from "./show-about.injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const aboutMenuItemInjectable = getInjectable({
id: "about-menu-item",
instantiate: (di) => {
const productName = di.inject(productNameInjectable);
const showAbout = di.inject(showAboutInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "about",
parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 10 : 40,
label: `About ${productName}`,
onClick() {
showAbout();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default aboutMenuItemInjectable;
@@ -0,0 +1,45 @@
/**
* 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 showMessagePopupInjectable from "../../../../../../main/electron-app/features/show-message-popup.injectable";
import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable";
import appNameInjectable from "../../../../../../common/vars/app-name.injectable";
import productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import buildVersionInjectable from "../../../../../../main/vars/build-version/build-version.injectable";
import extensionApiVersionInjectable from "../../../../../../common/vars/extension-api-version.injectable";
import applicationCopyrightInjectable from "../../../../../../common/vars/application-copyright.injectable";
const showAboutInjectable = getInjectable({
id: "show-about",
instantiate: (di) => {
const buildVersion = di.inject(buildVersionInjectable);
const extensionApiVersion = di.inject(extensionApiVersionInjectable);
const showMessagePopup = di.inject(showMessagePopupInjectable);
const isWindows = di.inject(isWindowsInjectable);
const appName = di.inject(appNameInjectable);
const productName = di.inject(productNameInjectable);
const applicationCopyright = di.inject(applicationCopyrightInjectable);
return () => {
const appInfo = [
`${appName}: ${buildVersion.get()}`,
`Extension API: ${extensionApiVersion}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Node: ${process.versions.node}`,
applicationCopyright,
];
showMessagePopup(
`${isWindows ? " ".repeat(2) : ""}${appName}`,
productName,
appInfo.join("\r\n"),
);
};
},
});
export default showAboutInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goBackMenuItemInjectable = getInjectable({
id: "go-back-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-back",
orderNumber: 40,
label: "Back",
keyboardShortcut: "CmdOrCtrl+[",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goBack());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goBackMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goForwardMenuItemInjectable = getInjectable({
id: "go-forward-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-forward",
orderNumber: 50,
label: "Forward",
keyboardShortcut: "CmdOrCtrl+]",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goForward());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goForwardMenuItemInjectable;
@@ -0,0 +1,32 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToCatalogInjectable from "../../../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
const navigateToCatalogMenuItemInjectable = getInjectable({
id: "navigate-to-catalog-menu-item",
instantiate: (di) => {
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "navigate-to-catalog",
orderNumber: 10,
label: "Catalog",
keyboardShortcut: "Shift+CmdOrCtrl+C",
onClick: () => {
navigateToCatalog();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToCatalogMenuItemInjectable;
@@ -0,0 +1,40 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import broadcastMessageInjectable from "../../../../../../common/ipc/broadcast-message.injectable";
const openCommandPaletteMenuItemInjectable = getInjectable({
id: "open-command-palette-menu-item",
instantiate: (di) => {
const broadcastMessage = di.inject(broadcastMessageInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "open-command-palette",
orderNumber: 20,
label: "Command Palette...",
keyboardShortcut: "Shift+CmdOrCtrl+P",
onClick(_m, _b, event) {
/**
* Don't broadcast unless it was triggered by menu iteration so that
* there aren't double events in renderer
*
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
*/
if (!event?.triggeredByAccelerator) {
broadcastMessage("command-palette:open");
}
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openCommandPaletteMenuItemInjectable;
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForToggleDevTools = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-dev-tools",
parentId: "view",
orderNumber: 70,
actionName: "toggleDevTools",
});
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
id: "reset-zoom",
parentId: "view",
orderNumber: 90,
actionName: "resetZoom",
});
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-in",
parentId: "view",
orderNumber: 100,
actionName: "zoomIn",
});
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-out",
parentId: "view",
orderNumber: 110,
actionName: "zoomOut",
});
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-full-screen",
parentId: "view",
orderNumber: 130,
actionName: "togglefullscreen",
});
@@ -0,0 +1,34 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import reloadCurrentApplicationWindowInjectable from "../../../../../../main/start-main-application/lens-window/reload-current-application-window.injectable";
const reloadMenuItemInjectable = getInjectable({
id: "reload-menu-item",
instantiate: (di) => {
const reloadApplicationWindow = di.inject(
reloadCurrentApplicationWindowInjectable,
);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "reload",
orderNumber: 60,
label: "Reload",
keyboardShortcut: "CmdOrCtrl+R",
onClick: () => {
reloadApplicationWindow();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default reloadMenuItemInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-view",
parentId: "view",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-for-view",
parentId: "view",
orderNumber: 80,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3-for-view",
parentId: "view",
orderNumber: 120,
});
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const viewMenuItemInjectable = getInjectable({
id: "view-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "view",
orderNumber: 40,
label: "View",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default viewMenuItemInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MenuItemConstructorOptions } from "electron";
import type { IComputedValue } from "mobx";
export type MenuRegistration = {
parentId: string;
visible?: IComputedValue<boolean> | boolean;
} & Omit<MenuItemConstructorOptions, "visible">;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
export function buildMenu(applicationMenuItems: MenuItemOpts[]) {
Menu.setApplicationMenu(
Menu.buildFromTemplate(applicationMenuItems),
);
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
export default getGlobalOverride(populateApplicationMenuInjectable, () => () => {});
@@ -0,0 +1,110 @@
/**
* 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 { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { map, sortBy } from "lodash/fp";
import type { MenuItemRoot } from "./application-menu-item-composite.injectable";
import { checkThatAllDiscriminablesAreExhausted } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
const populateApplicationMenuInjectable = getInjectable({
id: "populate-application-menu",
instantiate:
() => (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const electronTemplate = getApplicationMenuTemplate(composite);
const menu = Menu.buildFromTemplate(electronTemplate);
Menu.setApplicationMenu(menu);
},
causesSideEffects: true,
});
export default populateApplicationMenuInjectable;
export const getApplicationMenuTemplate = (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const topLevelMenus = composite.children.filter(
(x): x is Composite<ApplicationMenuItemTypes> => x.value.kind !== "root",
);
return topLevelMenus.map(toHierarchicalElectronMenuItem);
};
const toHierarchicalElectronMenuItem = (
composite: Composite<ApplicationMenuItemTypes>,
): MenuItemOpts => {
const value = composite.value;
switch (value.kind) {
case "top-level-menu": {
const { id } = composite;
const { label, role } = value;
return {
...(id ? { id } : {}),
...(role ? { role } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "sub-menu": {
const { id } = composite;
const { label } = value;
return {
...(id ? { id } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "clickable-menu-item": {
const { id } = composite;
const { label, onClick, keyboardShortcut } = value;
return {
...(id ? { id } : {}),
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut }: {}),
click: onClick,
};
}
case "os-action-menu-item": {
const { label, keyboardShortcut, actionName } = value;
return {
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut } : {}),
role: actionName,
};
}
case "separator": {
return {
type: "separator",
};
}
default: {
throw checkThatAllDiscriminablesAreExhausted(value);
}
}
};
@@ -0,0 +1,28 @@
/**
* 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 applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../main/start-main-application/runnable-tokens/on-load-of-application-injection-token";
const startApplicationMenuInjectable = getInjectable({
id: "start-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuReactivityInjectable,
);
return {
id: "start-application-menu",
run: () => {
applicationMenu.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startApplicationMenuInjectable;
@@ -0,0 +1,24 @@
/**
* 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 applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopApplicationMenuInjectable = getInjectable({
id: "stop-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(applicationMenuReactivityInjectable);
return {
id: "stop-application-menu",
run: () => void applicationMenu.stop(),
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopApplicationMenuInjectable;