mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Simplify hierarchy of application menu items using "single-root" composite
Also solve composed typing of application menu by using Discriminated Unions of TypeScript, see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com> Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
159f2bb9c1
commit
a8cc1cd17c
@ -29,7 +29,7 @@ describe("add-cluster - navigation using application menu", () => {
|
||||
|
||||
describe("when navigating to add cluster using application menu", () => {
|
||||
beforeEach(async () => {
|
||||
await applicationBuilder.applicationMenu.click("file.add-cluster");
|
||||
await applicationBuilder.applicationMenu.click("root.file.add-cluster");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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 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";
|
||||
|
||||
describe("application-menu-in-legacy-extension-api", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
|
||||
beforeEach(async () => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
builder.beforeApplicationStart(
|
||||
action((mainDi) => {
|
||||
mainDi.register(
|
||||
someTopMenuItemInjectable,
|
||||
someNonExtensionBasedMenuItemInjectable,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
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-clickable-item",
|
||||
parentId: "some-top-menu-item",
|
||||
click: onClickMock,
|
||||
},
|
||||
|
||||
{
|
||||
parentId: "some-top-menu-item",
|
||||
type: "separator",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
builder.extensions.enable(testExtensionOptions);
|
||||
});
|
||||
|
||||
it("menu related items exist", () => {
|
||||
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
|
||||
(x) =>
|
||||
x.startsWith("root.some-top-menu-item.some-extension-name"),
|
||||
);
|
||||
|
||||
expect(menuItemPathsForExtension).toEqual([
|
||||
"root.some-top-menu-item.some-extension-name/application-menu-item/clickable-menu-item(some-clickable-item)",
|
||||
"root.some-top-menu-item.some-extension-name/application-menu-item/separator(1)",
|
||||
]);
|
||||
});
|
||||
|
||||
it("when the extension-based clickable menu item is clicked, does so", () => {
|
||||
builder.applicationMenu.click(
|
||||
"root.some-top-menu-item.some-extension-name/application-menu-item/clickable-menu-item(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.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.startsWith("root.some-top-menu-item.some-extension-name"),
|
||||
);
|
||||
|
||||
expect(menuItemPathsForExtension).toEqual([
|
||||
"root.some-top-menu-item.some-extension-name/application-menu-item/clickable-menu-item(some-clickable-item)",
|
||||
"root.some-top-menu-item.some-extension-name/application-menu-item/separator(1)",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
@ -6,6 +6,7 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
|
||||
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
|
||||
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";
|
||||
import { getCompositePaths } from "./main/menu-items/get-composite/get-composite-paths/get-composite-paths";
|
||||
|
||||
describe("application-menu", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
@ -35,18 +36,21 @@ describe("application-menu", () => {
|
||||
});
|
||||
|
||||
describe("given enough time passes", () => {
|
||||
let applicationMenuPaths: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
advanceFakeTime(100);
|
||||
});
|
||||
|
||||
it("populates application menu", () => {
|
||||
expect(populateApplicationMenuMock).toHaveBeenCalledWith(
|
||||
expect.any(Array),
|
||||
applicationMenuPaths = getCompositePaths(
|
||||
populateApplicationMenuMock.mock.calls[0][0],
|
||||
);
|
||||
});
|
||||
|
||||
it("populates application menu lol", () => {
|
||||
expect(populateApplicationMenuMock.mock.calls).toMatchSnapshot();
|
||||
it("populates application menu with at least something", () => {
|
||||
expect(applicationMenuPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("populates application menu", () => {
|
||||
expect(applicationMenuPaths).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 "./menu-items/get-composite/get-composite";
|
||||
import getComposite from "./menu-items/get-composite/get-composite";
|
||||
import { computed } from "mobx";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { get } from "lodash/fp";
|
||||
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
|
||||
|
||||
export interface MenuItemRoot { id: "root"; kind: "root"; orderNumber: 0 }
|
||||
|
||||
const applicationMenuItemCompositeInjectable = getInjectable({
|
||||
id: "application-menu-item-composite",
|
||||
|
||||
instantiate: (di) => {
|
||||
const menuItems = di.inject(applicationMenuItemsInjectable);
|
||||
|
||||
return computed((): Composite<ApplicationMenuItemTypes | MenuItemRoot> => {
|
||||
const items = menuItems.get();
|
||||
|
||||
return pipeline(
|
||||
[{ id: "root" as const, kind: "root" as const, orderNumber: 0 as const }, ...items],
|
||||
|
||||
x => getComposite({
|
||||
source: x,
|
||||
rootId: "root",
|
||||
getId: get("id"),
|
||||
getParentId: get("parentId"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default applicationMenuItemCompositeInjectable;
|
||||
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { 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 { ClickableMenuItem, Separator } from "./menu-items/application-menu-item-injection-token";
|
||||
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
|
||||
|
||||
const applicationMenuItemRegistratorInjectable = getInjectable({
|
||||
id: "application-menu-item-registrator",
|
||||
|
||||
instantiate: () => (ext: LensExtension) => {
|
||||
const extension = ext as LensMainExtension;
|
||||
|
||||
return extension.appMenus.map((registration, index) => {
|
||||
const registrationId = registration.id || index;
|
||||
const applicationMenuId = `${extension.sanitizedExtensionId}/application-menu-item`;
|
||||
|
||||
return getInjectable({
|
||||
id: `${applicationMenuId}/${registrationId}`,
|
||||
|
||||
instantiate: () => {
|
||||
const orderNumber = 1000 + index * 10;
|
||||
|
||||
if (registration.type === "separator") {
|
||||
return {
|
||||
kind: "separator" as const,
|
||||
id: `${applicationMenuId}/separator(${registrationId})`,
|
||||
parentId: registration.parentId,
|
||||
orderNumber,
|
||||
} as Separator;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
id: `${applicationMenuId}/clickable-menu-item(${registrationId})`,
|
||||
parentId: registration.parentId,
|
||||
// Todo: hide electron evens from this abstraction.
|
||||
onClick: registration.click,
|
||||
label: registration.label,
|
||||
isShown: registration.visible ?? true,
|
||||
orderNumber,
|
||||
...(registration.accelerator ? { keyboardShortcut: registration.accelerator as string } : {}),
|
||||
} as ClickableMenuItem;
|
||||
},
|
||||
|
||||
injectionToken: applicationMenuItemInjectionToken,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: extensionRegistratorInjectionToken,
|
||||
});
|
||||
|
||||
export default applicationMenuItemRegistratorInjectable;
|
||||
@ -5,12 +5,8 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { MenuItemConstructorOptions } from "electron";
|
||||
import { computed } from "mobx";
|
||||
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
|
||||
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
|
||||
import { filter, map, sortBy } from "lodash/fp";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import type { Composite } from "./menu-items/get-composite/get-composite";
|
||||
import getComposite from "./menu-items/get-composite/get-composite";
|
||||
import applicationMenuItemInjectionToken, { isShown } from "./menu-items/application-menu-item-injection-token";
|
||||
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
|
||||
export interface MenuItemOpts extends MenuItemConstructorOptions {
|
||||
submenu?: MenuItemConstructorOptions[];
|
||||
@ -20,76 +16,33 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
id: "application-menu-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const allShown = pipeline(
|
||||
di.injectMany(applicationMenuItemInjectionToken),
|
||||
filter((x) => x.isShown !== false),
|
||||
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||
|
||||
return computed(() =>
|
||||
computedInjectMany(applicationMenuItemInjectionToken)
|
||||
.get()
|
||||
.filter(isShown),
|
||||
);
|
||||
|
||||
const roots = allShown.filter((x) => x.parentId === null);
|
||||
// Prepare menu items order
|
||||
|
||||
const toMenuItemOpt = (
|
||||
x: Composite<ApplicationMenuItemTypes>,
|
||||
): MenuItemOpts => ({
|
||||
// @ts-ignore
|
||||
label: x.value.label,
|
||||
id: x.id,
|
||||
|
||||
submenu: pipeline(
|
||||
x.children,
|
||||
sortBy(x => x.value.orderNumber),
|
||||
map(toMenuItemOpt),
|
||||
),
|
||||
|
||||
// @ts-ignore
|
||||
type: x.value.type,
|
||||
// @ts-ignore
|
||||
role: x.value.role,
|
||||
// @ts-ignore
|
||||
click: x.value.click,
|
||||
// @ts-ignore
|
||||
accelerator: x.value.accelerator,
|
||||
});
|
||||
|
||||
const menuItems = pipeline(
|
||||
roots,
|
||||
|
||||
map((root) =>
|
||||
getComposite({
|
||||
source: allShown,
|
||||
// @ts-ignore
|
||||
rootId: root.id,
|
||||
// @ts-ignore
|
||||
getId: (x) => x.id,
|
||||
// @ts-ignore
|
||||
getParentId: (x) => x.parentId,
|
||||
}),
|
||||
),
|
||||
|
||||
map(toMenuItemOpt),
|
||||
);
|
||||
|
||||
return computed((): MenuItemOpts[] => {
|
||||
// Prepare menu items order
|
||||
|
||||
// // Modify menu from extensions-api
|
||||
// for (const menuItem of electronMenuItems.get()) {
|
||||
// const parentMenu = appMenu.get(menuItem.parentId);
|
||||
//
|
||||
// if (!parentMenu) {
|
||||
// logger.error(
|
||||
// `[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||
// { menuItem },
|
||||
// );
|
||||
//
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // (parentMenu.submenu ??= []).push(menuItem);
|
||||
// }
|
||||
|
||||
return menuItems;
|
||||
});
|
||||
// // Modify menu from extensions-api
|
||||
// for (const menuItem of electronMenuItems.get()) {
|
||||
// const parentMenu = appMenu.get(menuItem.parentId);
|
||||
//
|
||||
// if (!parentMenu) {
|
||||
// logger.error(
|
||||
// `[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||
// { menuItem },
|
||||
// );
|
||||
//
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // (parentMenu.submenu ??= []).push(menuItem);
|
||||
// }
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default applicationMenuItemsInjectable;
|
||||
|
||||
@ -4,19 +4,19 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { autorun } from "mobx";
|
||||
import applicationMenuItemsInjectable from "./application-menu-items.injectable";
|
||||
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 applicationMenuItems = di.inject(applicationMenuItemsInjectable);
|
||||
const applicationMenuItemComposite = di.inject(applicationMenuItemCompositeInjectable);
|
||||
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
|
||||
|
||||
return getStartableStoppable("application-menu-reactivity", () =>
|
||||
autorun(() => populateApplicationMenu(applicationMenuItems.get()), {
|
||||
autorun(() => populateApplicationMenu(applicationMenuItemComposite.get()), {
|
||||
delay: 100,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -3,63 +3,104 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { BrowserWindow, MenuItem, KeyboardEvent } from "electron";
|
||||
import type { BrowserWindow, KeyboardEvent, MenuItemConstructorOptions, MenuItem as ElectronMenuItem } from "electron";
|
||||
import type { SetOptional } from "type-fest";
|
||||
|
||||
interface Shared {
|
||||
parentId: string | null;
|
||||
orderNumber: number;
|
||||
export interface MayHaveKeyboardShortcut {
|
||||
keyboardShortcut?: string;
|
||||
}
|
||||
|
||||
export interface Showable {
|
||||
isShown?: boolean;
|
||||
}
|
||||
export const isShown = (showable: Showable) => showable.isShown !== false;
|
||||
|
||||
export interface ApplicationMenuItem extends Shared {
|
||||
label: string;
|
||||
accelerator?: string;
|
||||
id: string;
|
||||
|
||||
export interface Clickable {
|
||||
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
|
||||
click?: (
|
||||
menuItem: MenuItem,
|
||||
browserWindow: BrowserWindow | undefined,
|
||||
event: KeyboardEvent
|
||||
) => void;
|
||||
onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
export interface Separator extends Shared {
|
||||
type: "separator";
|
||||
export interface Labeled {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface OperationSystemAction extends Shared {
|
||||
label?: string;
|
||||
accelerator?: string;
|
||||
export interface MaybeLabeled extends SetOptional<Labeled, "label"> {}
|
||||
|
||||
role:
|
||||
| "services"
|
||||
| "hide"
|
||||
| "hideOthers"
|
||||
| "unhide"
|
||||
| "close"
|
||||
| "undo"
|
||||
| "redo"
|
||||
| "cut"
|
||||
| "copy"
|
||||
| "paste"
|
||||
| "delete"
|
||||
| "selectAll"
|
||||
| "toggleDevTools"
|
||||
| "resetZoom"
|
||||
| "zoomIn"
|
||||
| "zoomOut"
|
||||
| "togglefullscreen";
|
||||
export interface CanBeChildOfParent {
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
export interface Orderable {
|
||||
orderNumber: number;
|
||||
}
|
||||
|
||||
export interface Identifiable {
|
||||
id: string;
|
||||
}
|
||||
|
||||
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
|
||||
& Kind<T>
|
||||
& Identifiable
|
||||
& CanBeChildOfParent
|
||||
& Showable
|
||||
& Orderable;
|
||||
|
||||
interface Kind<T extends string> { kind: T }
|
||||
|
||||
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
|
||||
& CanBeChildOfParent;
|
||||
|
||||
export type ClickableMenuItem =
|
||||
& ApplicationMenuItemType<"clickable-menu-item">
|
||||
& MenuItem
|
||||
& Labeled
|
||||
& Clickable;
|
||||
|
||||
export type OsActionMenuItem =
|
||||
& ApplicationMenuItemType<"os-action-menu-item">
|
||||
& MenuItem
|
||||
& MaybeLabeled
|
||||
& TriggersElectronAction;
|
||||
|
||||
type MenuItem =
|
||||
& CanBeChildOfParent
|
||||
& MayHaveKeyboardShortcut;
|
||||
|
||||
interface TriggersElectronAction {
|
||||
actionName: ElectronRoles;
|
||||
}
|
||||
|
||||
// Todo: SeparatorMenuItem
|
||||
export type Separator =
|
||||
& ApplicationMenuItemType<"separator">
|
||||
& CanBeChildOfParent;
|
||||
|
||||
export type ApplicationMenuItemTypes =
|
||||
| ApplicationMenuItem
|
||||
| TopLevelMenu
|
||||
| SubMenu
|
||||
| OsActionMenuItem
|
||||
| ClickableMenuItem
|
||||
| Separator
|
||||
| OperationSystemAction;
|
||||
;
|
||||
|
||||
const applicationMenuItemInjectionToken =
|
||||
getInjectionToken<ApplicationMenuItemTypes>({
|
||||
id: "application-menu-item-injection-token",
|
||||
});
|
||||
const applicationMenuItemInjectionToken = getInjectionToken<ApplicationMenuItemTypes>({
|
||||
id: "application-menu-item-injection-token",
|
||||
});
|
||||
|
||||
export default applicationMenuItemInjectionToken;
|
||||
|
||||
@ -9,8 +9,9 @@ const editMenuItemInjectable = getInjectable({
|
||||
id: "edit-application-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
parentId: null,
|
||||
kind: "top-level-menu" as const,
|
||||
id: "edit",
|
||||
parentId: "root" as const,
|
||||
orderNumber: 30,
|
||||
label: "Edit",
|
||||
}),
|
||||
|
||||
@ -3,25 +3,21 @@
|
||||
* 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";
|
||||
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,
|
||||
role: "undo",
|
||||
actionName: "undo",
|
||||
});
|
||||
|
||||
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "redo",
|
||||
parentId: "edit",
|
||||
orderNumber: 20,
|
||||
role: "redo",
|
||||
actionName: "redo",
|
||||
});
|
||||
|
||||
export const separator1 = getApplicationMenuSeparatorInjectable({
|
||||
@ -34,28 +30,28 @@ export const actionForCut = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "cut",
|
||||
parentId: "edit",
|
||||
orderNumber: 40,
|
||||
role: "cut",
|
||||
actionName: "cut",
|
||||
});
|
||||
|
||||
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "copy",
|
||||
parentId: "edit",
|
||||
orderNumber: 50,
|
||||
role: "copy",
|
||||
actionName: "copy",
|
||||
});
|
||||
|
||||
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "paste",
|
||||
parentId: "edit",
|
||||
orderNumber: 60,
|
||||
role: "paste",
|
||||
actionName: "paste",
|
||||
});
|
||||
|
||||
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "delete",
|
||||
parentId: "edit",
|
||||
orderNumber: 70,
|
||||
role: "delete",
|
||||
actionName: "delete",
|
||||
});
|
||||
|
||||
export const separator2 = getApplicationMenuSeparatorInjectable({
|
||||
@ -68,6 +64,6 @@ export const actionForSelectAll = getApplicationMenuOperationSystemActionInjecta
|
||||
id: "selectAll",
|
||||
parentId: "edit",
|
||||
orderNumber: 90,
|
||||
role: "selectAll",
|
||||
actionName: "selectAll",
|
||||
});
|
||||
|
||||
|
||||
@ -13,13 +13,14 @@ const addClusterMenuItemInjectable = getInjectable({
|
||||
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "file",
|
||||
id: "add-cluster",
|
||||
orderNumber: 10,
|
||||
label: "Add Cluster",
|
||||
accelerator: "CmdOrCtrl+Shift+A",
|
||||
keyboardShortcut: "CmdOrCtrl+Shift+A",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
navigateToAddCluster();
|
||||
},
|
||||
};
|
||||
|
||||
@ -13,11 +13,13 @@ const closeWindowMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
id: "close-window",
|
||||
kind: "os-action-menu-item" as const,
|
||||
parentId: "file",
|
||||
orderNumber: 60,
|
||||
role: "close" as const,
|
||||
actionName: "close" as const,
|
||||
label: "Close Window",
|
||||
accelerator: "Shift+Cmd+W",
|
||||
keyboardShortcut: "Shift+Cmd+W",
|
||||
isShown: isMac,
|
||||
};
|
||||
},
|
||||
|
||||
@ -9,8 +9,9 @@ const fileMenuItemInjectable = getInjectable({
|
||||
id: "file-application-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
parentId: null,
|
||||
kind: "top-level-menu" as const,
|
||||
id: "file",
|
||||
parentId: "root" as const,
|
||||
orderNumber: 20,
|
||||
label: "File",
|
||||
}),
|
||||
|
||||
@ -13,9 +13,3 @@ export const separator1 = getApplicationMenuSeparatorInjectable({
|
||||
orderNumber: 20,
|
||||
isShownOnlyOnMac: true,
|
||||
});
|
||||
|
||||
export const separator2 = getApplicationMenuSeparatorInjectable({
|
||||
id: "separator-2-for-file",
|
||||
parentId: "file",
|
||||
orderNumber: 50,
|
||||
});
|
||||
|
||||
@ -3,20 +3,20 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { OperationSystemAction } from "./application-menu-item-injection-token";
|
||||
import type { OsActionMenuItem } from "./application-menu-item-injection-token";
|
||||
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
|
||||
|
||||
const getApplicationMenuOperationSystemActionInjectable = ({
|
||||
id,
|
||||
role,
|
||||
...rest
|
||||
}: { id: string } & OperationSystemAction) =>
|
||||
}: Omit<OsActionMenuItem, "kind" >) =>
|
||||
getInjectable({
|
||||
id: `application-menu-operation-system-action/${id}`,
|
||||
|
||||
instantiate: () => ({
|
||||
...rest,
|
||||
role,
|
||||
id,
|
||||
kind: "os-action-menu-item" as const,
|
||||
}),
|
||||
|
||||
injectionToken: applicationMenuItemInjectionToken,
|
||||
|
||||
@ -11,7 +11,10 @@ const getApplicationMenuSeparatorInjectable = ({
|
||||
id,
|
||||
isShownOnlyOnMac = false,
|
||||
...rest
|
||||
}: { id: string; isShownOnlyOnMac?: boolean } & Omit<Separator, "type">) =>
|
||||
}: { isShownOnlyOnMac?: boolean } & Omit<
|
||||
Separator,
|
||||
"kind" | "isShown"
|
||||
>) =>
|
||||
getInjectable({
|
||||
id: `application-menu-separator/${id}`,
|
||||
|
||||
@ -21,8 +24,9 @@ const getApplicationMenuSeparatorInjectable = ({
|
||||
|
||||
return {
|
||||
...rest,
|
||||
id,
|
||||
kind: "separator" as const,
|
||||
isShown,
|
||||
type: "separator" as const,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -9,10 +9,12 @@ const helpMenuItemInjectable = getInjectable({
|
||||
id: "help-application-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
parentId: null,
|
||||
kind: "top-level-menu" as const,
|
||||
id: "help",
|
||||
parentId: "root" as const,
|
||||
orderNumber: 50,
|
||||
label: "Help",
|
||||
role: "help" as const,
|
||||
}),
|
||||
|
||||
injectionToken: applicationMenuItemInjectionToken,
|
||||
|
||||
@ -13,12 +13,13 @@ const navigateToWelcomeMenuItem = getInjectable({
|
||||
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "help",
|
||||
id: "navigate-to-welcome",
|
||||
orderNumber: 10,
|
||||
label: "Welcome",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
navigateToWelcome();
|
||||
},
|
||||
};
|
||||
|
||||
@ -16,13 +16,14 @@ const openDocumentationMenuItemInjectable = getInjectable({
|
||||
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
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
openLinkInBrowser(docsUrl).catch((error) => {
|
||||
logger.error("[MENU]: failed to open browser", { error });
|
||||
});
|
||||
|
||||
@ -16,13 +16,14 @@ const openSupportItemInjectable = getInjectable({
|
||||
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
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
openLinkInBrowser(supportUrl).catch((error) => {
|
||||
logger.error("[MENU]: failed to open browser", { error });
|
||||
});
|
||||
|
||||
@ -24,13 +24,14 @@ const checkForUpdatesMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
id: "check-for-updates",
|
||||
parentId: isMac ? "primary-for-mac" : "help",
|
||||
orderNumber: isMac ? 20 : 50,
|
||||
label: "Check for updates",
|
||||
isShown: updatingIsEnabled,
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
// Todo: implement using async/await
|
||||
processCheckingForUpdates("application-menu").then(() =>
|
||||
showApplicationWindow(),
|
||||
|
||||
@ -15,13 +15,14 @@ const navigateToExtensionsMenuItem = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: isMac ? "primary-for-mac" : "file",
|
||||
id: "navigate-to-extensions",
|
||||
orderNumber: isMac ? 50 : 40,
|
||||
label: "Extensions",
|
||||
accelerator: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
|
||||
keyboardShortcut: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
navigateToExtensions();
|
||||
},
|
||||
};
|
||||
|
||||
@ -15,13 +15,14 @@ const navigateToPreferencesMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: isMac ? "primary-for-mac" : "file",
|
||||
id: "navigate-to-preferences",
|
||||
orderNumber: isMac ? 40 : 30,
|
||||
label: "Preferences",
|
||||
accelerator: isMac ? "CmdOrCtrl+," : "Ctrl+,",
|
||||
keyboardShortcut: isMac ? "CmdOrCtrl+," : "Ctrl+,",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
navigateToPreferences();
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,26 +11,26 @@ export const actionForServices = getApplicationMenuOperationSystemActionInjectab
|
||||
id: "services",
|
||||
parentId: "primary-for-mac",
|
||||
orderNumber: 80,
|
||||
role: "services",
|
||||
actionName: "services",
|
||||
});
|
||||
|
||||
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "hide",
|
||||
parentId: "primary-for-mac",
|
||||
orderNumber: 100,
|
||||
role: "hide",
|
||||
actionName: "hide",
|
||||
});
|
||||
|
||||
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "hide-others",
|
||||
parentId: "primary-for-mac",
|
||||
orderNumber: 110,
|
||||
role: "hideOthers",
|
||||
actionName: "hideOthers",
|
||||
});
|
||||
|
||||
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "unhide",
|
||||
parentId: "primary-for-mac",
|
||||
orderNumber: 120,
|
||||
role: "unhide",
|
||||
actionName: "unhide",
|
||||
});
|
||||
|
||||
@ -15,7 +15,8 @@ const primaryMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
parentId: null,
|
||||
kind: "top-level-menu" as const,
|
||||
parentId: "root" as const,
|
||||
id: "primary-for-mac",
|
||||
orderNumber: 10,
|
||||
label: appName,
|
||||
|
||||
@ -15,14 +15,15 @@ const quitApplicationMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
id: "quit",
|
||||
label: "Quit",
|
||||
|
||||
parentId: isMac ? "primary-for-mac" : "file",
|
||||
orderNumber: isMac ? 140 : 70,
|
||||
accelerator: isMac ? "Cmd+Q" : "Alt+F4",
|
||||
keyboardShortcut: isMac ? "Cmd+Q" : "Alt+F4",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
stopServicesAndExitApp();
|
||||
},
|
||||
};
|
||||
|
||||
@ -17,12 +17,13 @@ const aboutMenuItemInjectable = getInjectable({
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
id: "about",
|
||||
parentId: isMac ? "primary-for-mac" : "help",
|
||||
orderNumber: isMac ? 10 : 40,
|
||||
label: `About ${productName}`,
|
||||
|
||||
click() {
|
||||
onClick() {
|
||||
showAbout();
|
||||
},
|
||||
};
|
||||
|
||||
@ -10,13 +10,14 @@ const goBackMenuItemInjectable = getInjectable({
|
||||
id: "go-back-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "view",
|
||||
id: "go-back",
|
||||
orderNumber: 40,
|
||||
label: "Back",
|
||||
accelerator: "CmdOrCtrl+[",
|
||||
keyboardShortcut: "CmdOrCtrl+[",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
webContents
|
||||
.getAllWebContents()
|
||||
.filter((wc) => wc.getType() === "window")
|
||||
|
||||
@ -10,13 +10,14 @@ const goForwardMenuItemInjectable = getInjectable({
|
||||
id: "go-forward-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "view",
|
||||
id: "go-forward",
|
||||
orderNumber: 50,
|
||||
label: "Forward",
|
||||
accelerator: "CmdOrCtrl+]",
|
||||
keyboardShortcut: "CmdOrCtrl+]",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
webContents
|
||||
.getAllWebContents()
|
||||
.filter((wc) => wc.getType() === "window")
|
||||
|
||||
@ -13,13 +13,14 @@ const navigateToCatalogMenuItemInjectable = getInjectable({
|
||||
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "view",
|
||||
id: "navigate-to-catalog",
|
||||
orderNumber: 10,
|
||||
label: "Catalog",
|
||||
accelerator: "Shift+CmdOrCtrl+C",
|
||||
keyboardShortcut: "Shift+CmdOrCtrl+C",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
navigateToCatalog();
|
||||
},
|
||||
};
|
||||
|
||||
@ -13,13 +13,14 @@ const openCommandPaletteMenuItemInjectable = getInjectable({
|
||||
const broadcastMessage = di.inject(broadcastMessageInjectable);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "view",
|
||||
id: "open-command-palette",
|
||||
orderNumber: 20,
|
||||
label: "Command Palette...",
|
||||
accelerator: "Shift+CmdOrCtrl+P",
|
||||
keyboardShortcut: "Shift+CmdOrCtrl+P",
|
||||
|
||||
click(_m, _b, event) {
|
||||
onClick(_m, _b, event) {
|
||||
/**
|
||||
* Don't broadcast unless it was triggered by menu iteration so that
|
||||
* there aren't double events in renderer
|
||||
|
||||
@ -10,33 +10,33 @@ export const actionForToggleDevTools = getApplicationMenuOperationSystemActionIn
|
||||
id: "toggle-dev-tools",
|
||||
parentId: "view",
|
||||
orderNumber: 70,
|
||||
role: "toggleDevTools",
|
||||
actionName: "toggleDevTools",
|
||||
});
|
||||
|
||||
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "reset-zoom",
|
||||
parentId: "view",
|
||||
orderNumber: 90,
|
||||
role: "resetZoom",
|
||||
actionName: "resetZoom",
|
||||
});
|
||||
|
||||
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "zoom-in",
|
||||
parentId: "view",
|
||||
orderNumber: 100,
|
||||
role: "zoomIn",
|
||||
actionName: "zoomIn",
|
||||
});
|
||||
|
||||
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "zoom-out",
|
||||
parentId: "view",
|
||||
orderNumber: 110,
|
||||
role: "zoomOut",
|
||||
actionName: "zoomOut",
|
||||
});
|
||||
|
||||
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
|
||||
id: "toggle-full-screen",
|
||||
parentId: "view",
|
||||
orderNumber: 130,
|
||||
role: "togglefullscreen",
|
||||
actionName: "togglefullscreen",
|
||||
});
|
||||
|
||||
@ -15,13 +15,14 @@ const reloadMenuItemInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
kind: "clickable-menu-item" as const,
|
||||
parentId: "view",
|
||||
id: "reload",
|
||||
orderNumber: 60,
|
||||
label: "Reload",
|
||||
accelerator: "CmdOrCtrl+R",
|
||||
keyboardShortcut: "CmdOrCtrl+R",
|
||||
|
||||
click: () => {
|
||||
onClick: () => {
|
||||
reloadApplicationWindow();
|
||||
},
|
||||
};
|
||||
|
||||
@ -9,7 +9,8 @@ const viewMenuItemInjectable = getInjectable({
|
||||
id: "view-application-menu-item",
|
||||
|
||||
instantiate: () => ({
|
||||
parentId: null,
|
||||
kind: "top-level-menu" as const,
|
||||
parentId: "root" as const,
|
||||
id: "view",
|
||||
orderNumber: 40,
|
||||
label: "View",
|
||||
|
||||
@ -5,15 +5,112 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { Menu } from "electron";
|
||||
import type { MenuItemOpts } from "./application-menu-items.injectable";
|
||||
import type { Composite } from "./menu-items/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";
|
||||
|
||||
const populateApplicationMenuInjectable = getInjectable({
|
||||
id: "populate-application-menu",
|
||||
|
||||
instantiate: () => (applicationMenuItems: MenuItemOpts[]) => {
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(applicationMenuItems));
|
||||
instantiate: () => (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
|
||||
const topLevelMenus = composite.children.filter(
|
||||
(x): x is Composite<ApplicationMenuItemTypes> => x.value.kind !== "root",
|
||||
);
|
||||
|
||||
const electronTemplate = topLevelMenus.map(toHierarchicalElectronMenuItem);
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(electronTemplate));
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default populateApplicationMenuInjectable;
|
||||
|
||||
const toHierarchicalElectronMenuItem = (
|
||||
composite: Composite<ApplicationMenuItemTypes>,
|
||||
): MenuItemOpts => {
|
||||
switch (composite.value.kind) {
|
||||
case "top-level-menu": {
|
||||
const {
|
||||
id,
|
||||
value: { label, role },
|
||||
} = composite;
|
||||
|
||||
return {
|
||||
...(id ? { id } : {}),
|
||||
...(role ? { role } : {}),
|
||||
label,
|
||||
|
||||
submenu: pipeline(
|
||||
composite.children,
|
||||
sortBy((childComposite) => childComposite.value.orderNumber),
|
||||
map(toHierarchicalElectronMenuItem),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case "sub-menu": {
|
||||
const {
|
||||
id,
|
||||
value: { label },
|
||||
} = composite;
|
||||
|
||||
return {
|
||||
...(id ? { id } : {}),
|
||||
label,
|
||||
|
||||
submenu: pipeline(
|
||||
composite.children,
|
||||
sortBy((childComposite) => childComposite.value.orderNumber),
|
||||
map(toHierarchicalElectronMenuItem),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case "clickable-menu-item": {
|
||||
const {
|
||||
id,
|
||||
value: { label, onClick, keyboardShortcut },
|
||||
} = composite;
|
||||
|
||||
return {
|
||||
...(id ? { id } : {}),
|
||||
...(label ? { label } : {}),
|
||||
...(keyboardShortcut ? { accelerator: keyboardShortcut }: {}),
|
||||
click: onClick,
|
||||
};
|
||||
}
|
||||
|
||||
case "os-action-menu-item": {
|
||||
const {
|
||||
value: { label, keyboardShortcut, actionName },
|
||||
} = composite;
|
||||
|
||||
return {
|
||||
...(label ? { label } : {}),
|
||||
...(keyboardShortcut ? { accelerator: keyboardShortcut } : {}),
|
||||
role: actionName,
|
||||
};
|
||||
}
|
||||
|
||||
case "separator": {
|
||||
return {
|
||||
type: "separator",
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
// Note: this will fail at transpilation time, if all ApplicationMenuItemTypes
|
||||
// are not handled in switch/case.
|
||||
const _exhaustiveCheck: never = composite.value;
|
||||
|
||||
// Note: this code is unreachable, it is here to make ts not complain about
|
||||
// _exhaustiveCheck not being used.
|
||||
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
|
||||
throw new Error(`Tried to create application menu, but foreign menu item was encountered: ${_exhaustiveCheck} ${composite.value}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -142,7 +142,7 @@ describe("analytics for installing update", () => {
|
||||
it("when checking for updates using application menu, sends event to analytics for being checked from application menu", async () => {
|
||||
analyticsListenerMock.mockClear();
|
||||
|
||||
builder.applicationMenu.click("root.check-for-updates");
|
||||
builder.applicationMenu.click("root.primary-for-mac.check-for-updates");
|
||||
|
||||
expect(analyticsListenerMock.mock.calls).toEqual([
|
||||
[
|
||||
|
||||
@ -48,7 +48,7 @@ describe("extensions - navigation using application menu", () => {
|
||||
|
||||
describe("when navigating to extensions using application menu", () => {
|
||||
beforeEach(() => {
|
||||
builder.applicationMenu.click("root.extensions");
|
||||
builder.applicationMenu.click("root.primary-for-mac.navigate-to-extensions");
|
||||
});
|
||||
|
||||
it("focuses the window", () => {
|
||||
|
||||
@ -29,7 +29,7 @@ describe("preferences - navigation using application menu", () => {
|
||||
|
||||
describe("when navigating to preferences using application menu", () => {
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("root.preferences");
|
||||
applicationBuilder.applicationMenu.click("root.primary-for-mac.navigate-to-preferences");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -45,7 +45,7 @@ describe("quitting the app using application menu", () => {
|
||||
|
||||
describe("when application is quit", () => {
|
||||
beforeEach(() => {
|
||||
builder.applicationMenu.click("root.quit");
|
||||
builder.applicationMenu.click("root.primary-for-mac.quit");
|
||||
});
|
||||
|
||||
it("closes all windows", () => {
|
||||
|
||||
@ -29,7 +29,7 @@ describe("welcome - navigation using application menu", () => {
|
||||
|
||||
describe("when navigated somewhere else", () => {
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("root.preferences");
|
||||
applicationBuilder.applicationMenu.click("root.primary-for-mac.navigate-to-preferences");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
@ -44,7 +44,7 @@ describe("welcome - navigation using application menu", () => {
|
||||
|
||||
describe("when navigated to welcome using application menu", () => {
|
||||
beforeEach(() => {
|
||||
applicationBuilder.applicationMenu.click("help.welcome");
|
||||
applicationBuilder.applicationMenu.click("root.help.navigate-to-welcome");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -17,12 +17,9 @@ import type { DiContainer, Injectable } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { compact, filter, first, flatMap, get, join, last, map, matches } from "lodash/fp";
|
||||
import { filter, first, get, join, last, map, matches } from "lodash/fp";
|
||||
import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
|
||||
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import type { MenuItemOpts } from "../../../features/application-menu/main/application-menu-items.injectable";
|
||||
import applicationMenuItemsInjectable from "../../../features/application-menu/main/application-menu-items.injectable";
|
||||
import type { MenuItem, MenuItemConstructorOptions } from "electron";
|
||||
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
|
||||
@ -68,6 +65,11 @@ import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-exte
|
||||
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
|
||||
import { Namespace } from "../../../common/k8s-api/endpoints";
|
||||
import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
|
||||
import applicationMenuItemCompositeInjectable from "../../../features/application-menu/main/application-menu-item-composite.injectable";
|
||||
import { getCompositePaths } from "../../../features/application-menu/main/menu-items/get-composite/get-composite-paths/get-composite-paths";
|
||||
import { normalizeComposite } from "../../../features/application-menu/main/menu-items/get-composite/normalize-composite/normalize-composite";
|
||||
import type { ClickableMenuItem } from "../../../features/application-menu/main/menu-items/application-menu-item-injection-token";
|
||||
import type { Composite } from "../../../features/application-menu/main/menu-items/get-composite/get-composite";
|
||||
|
||||
type Callback = (di: DiContainer) => void | Promise<void>;
|
||||
|
||||
@ -127,6 +129,7 @@ export interface ApplicationBuilder {
|
||||
|
||||
applicationMenu: {
|
||||
click: (path: string) => void;
|
||||
items: string[];
|
||||
};
|
||||
preferences: {
|
||||
close: () => void;
|
||||
@ -343,37 +346,35 @@ export const getApplicationBuilder = () => {
|
||||
select: action((namespace) => selectedNamespaces.add(namespace)),
|
||||
},
|
||||
applicationMenu: {
|
||||
get items() {
|
||||
const composite = mainDi.inject(
|
||||
applicationMenuItemCompositeInjectable,
|
||||
).get();
|
||||
|
||||
return getCompositePaths(composite);
|
||||
},
|
||||
|
||||
click: (path: string) => {
|
||||
const applicationMenuItems = mainDi.inject(
|
||||
applicationMenuItemsInjectable,
|
||||
);
|
||||
const composite = mainDi.inject(
|
||||
applicationMenuItemCompositeInjectable,
|
||||
).get();
|
||||
|
||||
const menuItems = pipeline(
|
||||
applicationMenuItems.get(),
|
||||
flatMap(toFlatChildren(null)),
|
||||
filter((menuItem) => !!menuItem.click),
|
||||
);
|
||||
const clickableMenuItems = normalizeComposite(composite).filter(isClickableMenuItem);
|
||||
const clickableMenuItemMap = new Map(clickableMenuItems);
|
||||
// TODO: find out why this any!? The typing of above map is strict, so why map.get() isn't?
|
||||
const clickableMenuItem = clickableMenuItemMap.get(path);
|
||||
|
||||
const menuItem = menuItems.find((menuItem) => menuItem.path === path);
|
||||
|
||||
if (!menuItem) {
|
||||
const availableIds = menuItems.map(get("path")).join('", "');
|
||||
if (clickableMenuItem === undefined) {
|
||||
const clickableIds = [...clickableMenuItemMap.keys()];
|
||||
|
||||
throw new Error(
|
||||
`Tried to click application menu item with ID "${path}" which does not exist. Available IDs are: "${availableIds}"`,
|
||||
`Tried to click application menu item with unknown path "${path}". Available clickable paths are: \n"${clickableIds.join('",\n"')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
menuItem.click?.(
|
||||
{
|
||||
menu: null as never,
|
||||
commandId: 0,
|
||||
userAccelerator: null,
|
||||
...menuItem,
|
||||
} as MenuItem,
|
||||
undefined,
|
||||
{},
|
||||
);
|
||||
// Todo: prevent leaking of Electron.
|
||||
// @ts-ignore
|
||||
clickableMenuItem.value.onClick();
|
||||
},
|
||||
},
|
||||
|
||||
@ -747,25 +748,6 @@ export const getApplicationBuilder = () => {
|
||||
return builder;
|
||||
};
|
||||
|
||||
export type ToFlatChildren = (opts: MenuItemConstructorOptions) => (MenuItemOpts & { path: string })[];
|
||||
|
||||
function toFlatChildren(parentId: string | null | undefined): ToFlatChildren {
|
||||
return ({ submenu = [], ...menuItem }) => [
|
||||
{
|
||||
...menuItem,
|
||||
path: pipeline([parentId, menuItem.id], compact, join(".")),
|
||||
},
|
||||
...(
|
||||
Array.isArray(submenu)
|
||||
? submenu.flatMap(toFlatChildren(menuItem.id))
|
||||
: [{
|
||||
...submenu,
|
||||
path: pipeline([parentId, menuItem.id], compact, join(".")),
|
||||
}]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export const rendererExtensionsStateInjectable = getInjectable({
|
||||
id: "renderer-extensions-state",
|
||||
instantiate: () => observable.map<string, LensRendererExtension>(),
|
||||
@ -897,3 +879,9 @@ const disableExtensionFor =
|
||||
});
|
||||
};
|
||||
|
||||
const isClickableMenuItem = (
|
||||
pathAndComposite: readonly [path: string, composite: any],
|
||||
): pathAndComposite is [string, Composite<ClickableMenuItem>] =>
|
||||
pathAndComposite[1].value.kind === "clickable-menu-item";
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user