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", () => {
|
describe("when navigating to add cluster using application menu", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await applicationBuilder.applicationMenu.click("file.add-cluster");
|
await applicationBuilder.applicationMenu.click("root.file.add-cluster");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
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 { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||||
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
|
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
|
||||||
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";
|
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", () => {
|
describe("application-menu", () => {
|
||||||
let builder: ApplicationBuilder;
|
let builder: ApplicationBuilder;
|
||||||
@ -35,18 +36,21 @@ describe("application-menu", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("given enough time passes", () => {
|
describe("given enough time passes", () => {
|
||||||
|
let applicationMenuPaths: string[];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
advanceFakeTime(100);
|
advanceFakeTime(100);
|
||||||
});
|
applicationMenuPaths = getCompositePaths(
|
||||||
|
populateApplicationMenuMock.mock.calls[0][0],
|
||||||
it("populates application menu", () => {
|
|
||||||
expect(populateApplicationMenuMock).toHaveBeenCalledWith(
|
|
||||||
expect.any(Array),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("populates application menu lol", () => {
|
it("populates application menu with at least something", () => {
|
||||||
expect(populateApplicationMenuMock.mock.calls).toMatchSnapshot();
|
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 { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { MenuItemConstructorOptions } from "electron";
|
import type { MenuItemConstructorOptions } from "electron";
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
|
import applicationMenuItemInjectionToken, { isShown } from "./menu-items/application-menu-item-injection-token";
|
||||||
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
|
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||||
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";
|
|
||||||
|
|
||||||
export interface MenuItemOpts extends MenuItemConstructorOptions {
|
export interface MenuItemOpts extends MenuItemConstructorOptions {
|
||||||
submenu?: MenuItemConstructorOptions[];
|
submenu?: MenuItemConstructorOptions[];
|
||||||
@ -20,55 +16,14 @@ const applicationMenuItemsInjectable = getInjectable({
|
|||||||
id: "application-menu-items",
|
id: "application-menu-items",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const allShown = pipeline(
|
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||||
di.injectMany(applicationMenuItemInjectionToken),
|
|
||||||
filter((x) => x.isShown !== false),
|
return computed(() =>
|
||||||
|
computedInjectMany(applicationMenuItemInjectionToken)
|
||||||
|
.get()
|
||||||
|
.filter(isShown),
|
||||||
);
|
);
|
||||||
|
|
||||||
const roots = allShown.filter((x) => x.parentId === null);
|
|
||||||
|
|
||||||
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
|
// Prepare menu items order
|
||||||
|
|
||||||
// // Modify menu from extensions-api
|
// // Modify menu from extensions-api
|
||||||
@ -86,10 +41,8 @@ const applicationMenuItemsInjectable = getInjectable({
|
|||||||
//
|
//
|
||||||
// // (parentMenu.submenu ??= []).push(menuItem);
|
// // (parentMenu.submenu ??= []).push(menuItem);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return menuItems;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export default applicationMenuItemsInjectable;
|
export default applicationMenuItemsInjectable;
|
||||||
|
|||||||
@ -4,19 +4,19 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import applicationMenuItemsInjectable from "./application-menu-items.injectable";
|
|
||||||
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
|
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
|
||||||
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
|
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
|
||||||
|
import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable";
|
||||||
|
|
||||||
const applicationMenuReactivityInjectable = getInjectable({
|
const applicationMenuReactivityInjectable = getInjectable({
|
||||||
id: "application-menu-reactivity",
|
id: "application-menu-reactivity",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
|
const applicationMenuItemComposite = di.inject(applicationMenuItemCompositeInjectable);
|
||||||
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
|
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
|
||||||
|
|
||||||
return getStartableStoppable("application-menu-reactivity", () =>
|
return getStartableStoppable("application-menu-reactivity", () =>
|
||||||
autorun(() => populateApplicationMenu(applicationMenuItems.get()), {
|
autorun(() => populateApplicationMenu(applicationMenuItemComposite.get()), {
|
||||||
delay: 100,
|
delay: 100,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,62 +3,103 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
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 {
|
export interface MayHaveKeyboardShortcut {
|
||||||
parentId: string | null;
|
keyboardShortcut?: string;
|
||||||
orderNumber: number;
|
}
|
||||||
|
|
||||||
|
export interface Showable {
|
||||||
isShown?: boolean;
|
isShown?: boolean;
|
||||||
}
|
}
|
||||||
|
export const isShown = (showable: Showable) => showable.isShown !== false;
|
||||||
|
|
||||||
export interface ApplicationMenuItem extends Shared {
|
export interface Clickable {
|
||||||
label: string;
|
|
||||||
accelerator?: string;
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
|
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
|
||||||
click?: (
|
onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void;
|
||||||
menuItem: MenuItem,
|
|
||||||
browserWindow: BrowserWindow | undefined,
|
|
||||||
event: KeyboardEvent
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Separator extends Shared {
|
export interface Labeled {
|
||||||
type: "separator";
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OperationSystemAction extends Shared {
|
export interface MaybeLabeled extends SetOptional<Labeled, "label"> {}
|
||||||
label?: string;
|
|
||||||
accelerator?: string;
|
|
||||||
|
|
||||||
role:
|
export interface CanBeChildOfParent {
|
||||||
| "services"
|
parentId: string;
|
||||||
| "hide"
|
|
||||||
| "hideOthers"
|
|
||||||
| "unhide"
|
|
||||||
| "close"
|
|
||||||
| "undo"
|
|
||||||
| "redo"
|
|
||||||
| "cut"
|
|
||||||
| "copy"
|
|
||||||
| "paste"
|
|
||||||
| "delete"
|
|
||||||
| "selectAll"
|
|
||||||
| "toggleDevTools"
|
|
||||||
| "resetZoom"
|
|
||||||
| "zoomIn"
|
|
||||||
| "zoomOut"
|
|
||||||
| "togglefullscreen";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 =
|
export type ApplicationMenuItemTypes =
|
||||||
| ApplicationMenuItem
|
| TopLevelMenu
|
||||||
|
| SubMenu
|
||||||
|
| OsActionMenuItem
|
||||||
|
| ClickableMenuItem
|
||||||
| Separator
|
| Separator
|
||||||
| OperationSystemAction;
|
;
|
||||||
|
|
||||||
const applicationMenuItemInjectionToken =
|
const applicationMenuItemInjectionToken = getInjectionToken<ApplicationMenuItemTypes>({
|
||||||
getInjectionToken<ApplicationMenuItemTypes>({
|
|
||||||
id: "application-menu-item-injection-token",
|
id: "application-menu-item-injection-token",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,9 @@ const editMenuItemInjectable = getInjectable({
|
|||||||
id: "edit-application-menu-item",
|
id: "edit-application-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
parentId: null,
|
kind: "top-level-menu" as const,
|
||||||
id: "edit",
|
id: "edit",
|
||||||
|
parentId: "root" as const,
|
||||||
orderNumber: 30,
|
orderNumber: 30,
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -3,25 +3,21 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { getApplicationMenuOperationSystemActionInjectable } from "../../get-application-menu-operation-system-action-injectable";
|
||||||
getApplicationMenuOperationSystemActionInjectable,
|
import { getApplicationMenuSeparatorInjectable } from "../../get-application-menu-separator-injectable";
|
||||||
} from "../../get-application-menu-operation-system-action-injectable";
|
|
||||||
import {
|
|
||||||
getApplicationMenuSeparatorInjectable,
|
|
||||||
} from "../../get-application-menu-separator-injectable";
|
|
||||||
|
|
||||||
export const actionForUndo = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForUndo = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "undo",
|
id: "undo",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
role: "undo",
|
actionName: "undo",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "redo",
|
id: "redo",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 20,
|
orderNumber: 20,
|
||||||
role: "redo",
|
actionName: "redo",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const separator1 = getApplicationMenuSeparatorInjectable({
|
export const separator1 = getApplicationMenuSeparatorInjectable({
|
||||||
@ -34,28 +30,28 @@ export const actionForCut = getApplicationMenuOperationSystemActionInjectable({
|
|||||||
id: "cut",
|
id: "cut",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 40,
|
orderNumber: 40,
|
||||||
role: "cut",
|
actionName: "cut",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "copy",
|
id: "copy",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 50,
|
orderNumber: 50,
|
||||||
role: "copy",
|
actionName: "copy",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "paste",
|
id: "paste",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 60,
|
orderNumber: 60,
|
||||||
role: "paste",
|
actionName: "paste",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "delete",
|
id: "delete",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 70,
|
orderNumber: 70,
|
||||||
role: "delete",
|
actionName: "delete",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const separator2 = getApplicationMenuSeparatorInjectable({
|
export const separator2 = getApplicationMenuSeparatorInjectable({
|
||||||
@ -68,6 +64,6 @@ export const actionForSelectAll = getApplicationMenuOperationSystemActionInjecta
|
|||||||
id: "selectAll",
|
id: "selectAll",
|
||||||
parentId: "edit",
|
parentId: "edit",
|
||||||
orderNumber: 90,
|
orderNumber: 90,
|
||||||
role: "selectAll",
|
actionName: "selectAll",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,14 @@ const addClusterMenuItemInjectable = getInjectable({
|
|||||||
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "file",
|
parentId: "file",
|
||||||
id: "add-cluster",
|
id: "add-cluster",
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
label: "Add Cluster",
|
label: "Add Cluster",
|
||||||
accelerator: "CmdOrCtrl+Shift+A",
|
keyboardShortcut: "CmdOrCtrl+Shift+A",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
navigateToAddCluster();
|
navigateToAddCluster();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,11 +13,13 @@ const closeWindowMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: "close-window",
|
||||||
|
kind: "os-action-menu-item" as const,
|
||||||
parentId: "file",
|
parentId: "file",
|
||||||
orderNumber: 60,
|
orderNumber: 60,
|
||||||
role: "close" as const,
|
actionName: "close" as const,
|
||||||
label: "Close Window",
|
label: "Close Window",
|
||||||
accelerator: "Shift+Cmd+W",
|
keyboardShortcut: "Shift+Cmd+W",
|
||||||
isShown: isMac,
|
isShown: isMac,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,8 +9,9 @@ const fileMenuItemInjectable = getInjectable({
|
|||||||
id: "file-application-menu-item",
|
id: "file-application-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
parentId: null,
|
kind: "top-level-menu" as const,
|
||||||
id: "file",
|
id: "file",
|
||||||
|
parentId: "root" as const,
|
||||||
orderNumber: 20,
|
orderNumber: 20,
|
||||||
label: "File",
|
label: "File",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -13,9 +13,3 @@ export const separator1 = getApplicationMenuSeparatorInjectable({
|
|||||||
orderNumber: 20,
|
orderNumber: 20,
|
||||||
isShownOnlyOnMac: true,
|
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.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
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";
|
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
|
||||||
|
|
||||||
const getApplicationMenuOperationSystemActionInjectable = ({
|
const getApplicationMenuOperationSystemActionInjectable = ({
|
||||||
id,
|
id,
|
||||||
role,
|
|
||||||
...rest
|
...rest
|
||||||
}: { id: string } & OperationSystemAction) =>
|
}: Omit<OsActionMenuItem, "kind" >) =>
|
||||||
getInjectable({
|
getInjectable({
|
||||||
id: `application-menu-operation-system-action/${id}`,
|
id: `application-menu-operation-system-action/${id}`,
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
...rest,
|
...rest,
|
||||||
role,
|
id,
|
||||||
|
kind: "os-action-menu-item" as const,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
injectionToken: applicationMenuItemInjectionToken,
|
injectionToken: applicationMenuItemInjectionToken,
|
||||||
|
|||||||
@ -11,7 +11,10 @@ const getApplicationMenuSeparatorInjectable = ({
|
|||||||
id,
|
id,
|
||||||
isShownOnlyOnMac = false,
|
isShownOnlyOnMac = false,
|
||||||
...rest
|
...rest
|
||||||
}: { id: string; isShownOnlyOnMac?: boolean } & Omit<Separator, "type">) =>
|
}: { isShownOnlyOnMac?: boolean } & Omit<
|
||||||
|
Separator,
|
||||||
|
"kind" | "isShown"
|
||||||
|
>) =>
|
||||||
getInjectable({
|
getInjectable({
|
||||||
id: `application-menu-separator/${id}`,
|
id: `application-menu-separator/${id}`,
|
||||||
|
|
||||||
@ -21,8 +24,9 @@ const getApplicationMenuSeparatorInjectable = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
|
id,
|
||||||
|
kind: "separator" as const,
|
||||||
isShown,
|
isShown,
|
||||||
type: "separator" as const,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,12 @@ const helpMenuItemInjectable = getInjectable({
|
|||||||
id: "help-application-menu-item",
|
id: "help-application-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
parentId: null,
|
kind: "top-level-menu" as const,
|
||||||
id: "help",
|
id: "help",
|
||||||
|
parentId: "root" as const,
|
||||||
orderNumber: 50,
|
orderNumber: 50,
|
||||||
label: "Help",
|
label: "Help",
|
||||||
|
role: "help" as const,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
injectionToken: applicationMenuItemInjectionToken,
|
injectionToken: applicationMenuItemInjectionToken,
|
||||||
|
|||||||
@ -13,12 +13,13 @@ const navigateToWelcomeMenuItem = getInjectable({
|
|||||||
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
|
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
id: "navigate-to-welcome",
|
id: "navigate-to-welcome",
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
label: "Welcome",
|
label: "Welcome",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
navigateToWelcome();
|
navigateToWelcome();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,13 +16,14 @@ const openDocumentationMenuItemInjectable = getInjectable({
|
|||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
id: "open-documentation",
|
id: "open-documentation",
|
||||||
orderNumber: 20,
|
orderNumber: 20,
|
||||||
label: "Documentation",
|
label: "Documentation",
|
||||||
|
|
||||||
// TODO: Convert to async/await
|
// TODO: Convert to async/await
|
||||||
click: () => {
|
onClick: () => {
|
||||||
openLinkInBrowser(docsUrl).catch((error) => {
|
openLinkInBrowser(docsUrl).catch((error) => {
|
||||||
logger.error("[MENU]: failed to open browser", { error });
|
logger.error("[MENU]: failed to open browser", { error });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,13 +16,14 @@ const openSupportItemInjectable = getInjectable({
|
|||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "help",
|
parentId: "help",
|
||||||
id: "open-support",
|
id: "open-support",
|
||||||
orderNumber: 30,
|
orderNumber: 30,
|
||||||
label: "Support",
|
label: "Support",
|
||||||
|
|
||||||
// TODO: Convert to async/await
|
// TODO: Convert to async/await
|
||||||
click: () => {
|
onClick: () => {
|
||||||
openLinkInBrowser(supportUrl).catch((error) => {
|
openLinkInBrowser(supportUrl).catch((error) => {
|
||||||
logger.error("[MENU]: failed to open browser", { error });
|
logger.error("[MENU]: failed to open browser", { error });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,13 +24,14 @@ const checkForUpdatesMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
id: "check-for-updates",
|
id: "check-for-updates",
|
||||||
parentId: isMac ? "primary-for-mac" : "help",
|
parentId: isMac ? "primary-for-mac" : "help",
|
||||||
orderNumber: isMac ? 20 : 50,
|
orderNumber: isMac ? 20 : 50,
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
isShown: updatingIsEnabled,
|
isShown: updatingIsEnabled,
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
// Todo: implement using async/await
|
// Todo: implement using async/await
|
||||||
processCheckingForUpdates("application-menu").then(() =>
|
processCheckingForUpdates("application-menu").then(() =>
|
||||||
showApplicationWindow(),
|
showApplicationWindow(),
|
||||||
|
|||||||
@ -15,13 +15,14 @@ const navigateToExtensionsMenuItem = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: isMac ? "primary-for-mac" : "file",
|
parentId: isMac ? "primary-for-mac" : "file",
|
||||||
id: "navigate-to-extensions",
|
id: "navigate-to-extensions",
|
||||||
orderNumber: isMac ? 50 : 40,
|
orderNumber: isMac ? 50 : 40,
|
||||||
label: "Extensions",
|
label: "Extensions",
|
||||||
accelerator: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
|
keyboardShortcut: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
navigateToExtensions();
|
navigateToExtensions();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,13 +15,14 @@ const navigateToPreferencesMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: isMac ? "primary-for-mac" : "file",
|
parentId: isMac ? "primary-for-mac" : "file",
|
||||||
id: "navigate-to-preferences",
|
id: "navigate-to-preferences",
|
||||||
orderNumber: isMac ? 40 : 30,
|
orderNumber: isMac ? 40 : 30,
|
||||||
label: "Preferences",
|
label: "Preferences",
|
||||||
accelerator: isMac ? "CmdOrCtrl+," : "Ctrl+,",
|
keyboardShortcut: isMac ? "CmdOrCtrl+," : "Ctrl+,",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
navigateToPreferences();
|
navigateToPreferences();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,26 +11,26 @@ export const actionForServices = getApplicationMenuOperationSystemActionInjectab
|
|||||||
id: "services",
|
id: "services",
|
||||||
parentId: "primary-for-mac",
|
parentId: "primary-for-mac",
|
||||||
orderNumber: 80,
|
orderNumber: 80,
|
||||||
role: "services",
|
actionName: "services",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "hide",
|
id: "hide",
|
||||||
parentId: "primary-for-mac",
|
parentId: "primary-for-mac",
|
||||||
orderNumber: 100,
|
orderNumber: 100,
|
||||||
role: "hide",
|
actionName: "hide",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "hide-others",
|
id: "hide-others",
|
||||||
parentId: "primary-for-mac",
|
parentId: "primary-for-mac",
|
||||||
orderNumber: 110,
|
orderNumber: 110,
|
||||||
role: "hideOthers",
|
actionName: "hideOthers",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "unhide",
|
id: "unhide",
|
||||||
parentId: "primary-for-mac",
|
parentId: "primary-for-mac",
|
||||||
orderNumber: 120,
|
orderNumber: 120,
|
||||||
role: "unhide",
|
actionName: "unhide",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,8 @@ const primaryMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
parentId: null,
|
kind: "top-level-menu" as const,
|
||||||
|
parentId: "root" as const,
|
||||||
id: "primary-for-mac",
|
id: "primary-for-mac",
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
label: appName,
|
label: appName,
|
||||||
|
|||||||
@ -15,14 +15,15 @@ const quitApplicationMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
id: "quit",
|
id: "quit",
|
||||||
label: "Quit",
|
label: "Quit",
|
||||||
|
|
||||||
parentId: isMac ? "primary-for-mac" : "file",
|
parentId: isMac ? "primary-for-mac" : "file",
|
||||||
orderNumber: isMac ? 140 : 70,
|
orderNumber: isMac ? 140 : 70,
|
||||||
accelerator: isMac ? "Cmd+Q" : "Alt+F4",
|
keyboardShortcut: isMac ? "Cmd+Q" : "Alt+F4",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
stopServicesAndExitApp();
|
stopServicesAndExitApp();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,12 +17,13 @@ const aboutMenuItemInjectable = getInjectable({
|
|||||||
const isMac = di.inject(isMacInjectable);
|
const isMac = di.inject(isMacInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
id: "about",
|
id: "about",
|
||||||
parentId: isMac ? "primary-for-mac" : "help",
|
parentId: isMac ? "primary-for-mac" : "help",
|
||||||
orderNumber: isMac ? 10 : 40,
|
orderNumber: isMac ? 10 : 40,
|
||||||
label: `About ${productName}`,
|
label: `About ${productName}`,
|
||||||
|
|
||||||
click() {
|
onClick() {
|
||||||
showAbout();
|
showAbout();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,13 +10,14 @@ const goBackMenuItemInjectable = getInjectable({
|
|||||||
id: "go-back-menu-item",
|
id: "go-back-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
id: "go-back",
|
id: "go-back",
|
||||||
orderNumber: 40,
|
orderNumber: 40,
|
||||||
label: "Back",
|
label: "Back",
|
||||||
accelerator: "CmdOrCtrl+[",
|
keyboardShortcut: "CmdOrCtrl+[",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
webContents
|
webContents
|
||||||
.getAllWebContents()
|
.getAllWebContents()
|
||||||
.filter((wc) => wc.getType() === "window")
|
.filter((wc) => wc.getType() === "window")
|
||||||
|
|||||||
@ -10,13 +10,14 @@ const goForwardMenuItemInjectable = getInjectable({
|
|||||||
id: "go-forward-menu-item",
|
id: "go-forward-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
id: "go-forward",
|
id: "go-forward",
|
||||||
orderNumber: 50,
|
orderNumber: 50,
|
||||||
label: "Forward",
|
label: "Forward",
|
||||||
accelerator: "CmdOrCtrl+]",
|
keyboardShortcut: "CmdOrCtrl+]",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
webContents
|
webContents
|
||||||
.getAllWebContents()
|
.getAllWebContents()
|
||||||
.filter((wc) => wc.getType() === "window")
|
.filter((wc) => wc.getType() === "window")
|
||||||
|
|||||||
@ -13,13 +13,14 @@ const navigateToCatalogMenuItemInjectable = getInjectable({
|
|||||||
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
id: "navigate-to-catalog",
|
id: "navigate-to-catalog",
|
||||||
orderNumber: 10,
|
orderNumber: 10,
|
||||||
label: "Catalog",
|
label: "Catalog",
|
||||||
accelerator: "Shift+CmdOrCtrl+C",
|
keyboardShortcut: "Shift+CmdOrCtrl+C",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
navigateToCatalog();
|
navigateToCatalog();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,13 +13,14 @@ const openCommandPaletteMenuItemInjectable = getInjectable({
|
|||||||
const broadcastMessage = di.inject(broadcastMessageInjectable);
|
const broadcastMessage = di.inject(broadcastMessageInjectable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
id: "open-command-palette",
|
id: "open-command-palette",
|
||||||
orderNumber: 20,
|
orderNumber: 20,
|
||||||
label: "Command Palette...",
|
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
|
* Don't broadcast unless it was triggered by menu iteration so that
|
||||||
* there aren't double events in renderer
|
* there aren't double events in renderer
|
||||||
|
|||||||
@ -10,33 +10,33 @@ export const actionForToggleDevTools = getApplicationMenuOperationSystemActionIn
|
|||||||
id: "toggle-dev-tools",
|
id: "toggle-dev-tools",
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
orderNumber: 70,
|
orderNumber: 70,
|
||||||
role: "toggleDevTools",
|
actionName: "toggleDevTools",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "reset-zoom",
|
id: "reset-zoom",
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
orderNumber: 90,
|
orderNumber: 90,
|
||||||
role: "resetZoom",
|
actionName: "resetZoom",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "zoom-in",
|
id: "zoom-in",
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
orderNumber: 100,
|
orderNumber: 100,
|
||||||
role: "zoomIn",
|
actionName: "zoomIn",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "zoom-out",
|
id: "zoom-out",
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
orderNumber: 110,
|
orderNumber: 110,
|
||||||
role: "zoomOut",
|
actionName: "zoomOut",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
|
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
|
||||||
id: "toggle-full-screen",
|
id: "toggle-full-screen",
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
orderNumber: 130,
|
orderNumber: 130,
|
||||||
role: "togglefullscreen",
|
actionName: "togglefullscreen",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,13 +15,14 @@ const reloadMenuItemInjectable = getInjectable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
kind: "clickable-menu-item" as const,
|
||||||
parentId: "view",
|
parentId: "view",
|
||||||
id: "reload",
|
id: "reload",
|
||||||
orderNumber: 60,
|
orderNumber: 60,
|
||||||
label: "Reload",
|
label: "Reload",
|
||||||
accelerator: "CmdOrCtrl+R",
|
keyboardShortcut: "CmdOrCtrl+R",
|
||||||
|
|
||||||
click: () => {
|
onClick: () => {
|
||||||
reloadApplicationWindow();
|
reloadApplicationWindow();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,8 @@ const viewMenuItemInjectable = getInjectable({
|
|||||||
id: "view-application-menu-item",
|
id: "view-application-menu-item",
|
||||||
|
|
||||||
instantiate: () => ({
|
instantiate: () => ({
|
||||||
parentId: null,
|
kind: "top-level-menu" as const,
|
||||||
|
parentId: "root" as const,
|
||||||
id: "view",
|
id: "view",
|
||||||
orderNumber: 40,
|
orderNumber: 40,
|
||||||
label: "View",
|
label: "View",
|
||||||
|
|||||||
@ -5,15 +5,112 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { Menu } from "electron";
|
import { Menu } from "electron";
|
||||||
import type { MenuItemOpts } from "./application-menu-items.injectable";
|
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({
|
const populateApplicationMenuInjectable = getInjectable({
|
||||||
id: "populate-application-menu",
|
id: "populate-application-menu",
|
||||||
|
|
||||||
instantiate: () => (applicationMenuItems: MenuItemOpts[]) => {
|
instantiate: () => (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
|
||||||
Menu.setApplicationMenu(Menu.buildFromTemplate(applicationMenuItems));
|
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,
|
causesSideEffects: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default populateApplicationMenuInjectable;
|
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 () => {
|
it("when checking for updates using application menu, sends event to analytics for being checked from application menu", async () => {
|
||||||
analyticsListenerMock.mockClear();
|
analyticsListenerMock.mockClear();
|
||||||
|
|
||||||
builder.applicationMenu.click("root.check-for-updates");
|
builder.applicationMenu.click("root.primary-for-mac.check-for-updates");
|
||||||
|
|
||||||
expect(analyticsListenerMock.mock.calls).toEqual([
|
expect(analyticsListenerMock.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
|||||||
@ -48,7 +48,7 @@ describe("extensions - navigation using application menu", () => {
|
|||||||
|
|
||||||
describe("when navigating to extensions using application menu", () => {
|
describe("when navigating to extensions using application menu", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
builder.applicationMenu.click("root.extensions");
|
builder.applicationMenu.click("root.primary-for-mac.navigate-to-extensions");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("focuses the window", () => {
|
it("focuses the window", () => {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ describe("preferences - navigation using application menu", () => {
|
|||||||
|
|
||||||
describe("when navigating to preferences using application menu", () => {
|
describe("when navigating to preferences using application menu", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
applicationBuilder.applicationMenu.click("root.preferences");
|
applicationBuilder.applicationMenu.click("root.primary-for-mac.navigate-to-preferences");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@ -45,7 +45,7 @@ describe("quitting the app using application menu", () => {
|
|||||||
|
|
||||||
describe("when application is quit", () => {
|
describe("when application is quit", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
builder.applicationMenu.click("root.quit");
|
builder.applicationMenu.click("root.primary-for-mac.quit");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("closes all windows", () => {
|
it("closes all windows", () => {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ describe("welcome - navigation using application menu", () => {
|
|||||||
|
|
||||||
describe("when navigated somewhere else", () => {
|
describe("when navigated somewhere else", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
applicationBuilder.applicationMenu.click("root.preferences");
|
applicationBuilder.applicationMenu.click("root.primary-for-mac.navigate-to-preferences");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
@ -44,7 +44,7 @@ describe("welcome - navigation using application menu", () => {
|
|||||||
|
|
||||||
describe("when navigated to welcome using application menu", () => {
|
describe("when navigated to welcome using application menu", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
applicationBuilder.applicationMenu.click("help.welcome");
|
applicationBuilder.applicationMenu.click("root.help.navigate-to-welcome");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@ -17,12 +17,9 @@ import type { DiContainer, Injectable } from "@ogre-tools/injectable";
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
|
||||||
import { pipeline } from "@ogre-tools/fp";
|
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 preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable";
|
||||||
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.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 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 navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||||
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.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 namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
|
||||||
import { Namespace } from "../../../common/k8s-api/endpoints";
|
import { Namespace } from "../../../common/k8s-api/endpoints";
|
||||||
import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
|
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>;
|
type Callback = (di: DiContainer) => void | Promise<void>;
|
||||||
|
|
||||||
@ -127,6 +129,7 @@ export interface ApplicationBuilder {
|
|||||||
|
|
||||||
applicationMenu: {
|
applicationMenu: {
|
||||||
click: (path: string) => void;
|
click: (path: string) => void;
|
||||||
|
items: string[];
|
||||||
};
|
};
|
||||||
preferences: {
|
preferences: {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
@ -343,37 +346,35 @@ export const getApplicationBuilder = () => {
|
|||||||
select: action((namespace) => selectedNamespaces.add(namespace)),
|
select: action((namespace) => selectedNamespaces.add(namespace)),
|
||||||
},
|
},
|
||||||
applicationMenu: {
|
applicationMenu: {
|
||||||
|
get items() {
|
||||||
|
const composite = mainDi.inject(
|
||||||
|
applicationMenuItemCompositeInjectable,
|
||||||
|
).get();
|
||||||
|
|
||||||
|
return getCompositePaths(composite);
|
||||||
|
},
|
||||||
|
|
||||||
click: (path: string) => {
|
click: (path: string) => {
|
||||||
const applicationMenuItems = mainDi.inject(
|
const composite = mainDi.inject(
|
||||||
applicationMenuItemsInjectable,
|
applicationMenuItemCompositeInjectable,
|
||||||
);
|
).get();
|
||||||
|
|
||||||
const menuItems = pipeline(
|
const clickableMenuItems = normalizeComposite(composite).filter(isClickableMenuItem);
|
||||||
applicationMenuItems.get(),
|
const clickableMenuItemMap = new Map(clickableMenuItems);
|
||||||
flatMap(toFlatChildren(null)),
|
// TODO: find out why this any!? The typing of above map is strict, so why map.get() isn't?
|
||||||
filter((menuItem) => !!menuItem.click),
|
const clickableMenuItem = clickableMenuItemMap.get(path);
|
||||||
);
|
|
||||||
|
|
||||||
const menuItem = menuItems.find((menuItem) => menuItem.path === path);
|
if (clickableMenuItem === undefined) {
|
||||||
|
const clickableIds = [...clickableMenuItemMap.keys()];
|
||||||
if (!menuItem) {
|
|
||||||
const availableIds = menuItems.map(get("path")).join('", "');
|
|
||||||
|
|
||||||
throw new Error(
|
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?.(
|
// Todo: prevent leaking of Electron.
|
||||||
{
|
// @ts-ignore
|
||||||
menu: null as never,
|
clickableMenuItem.value.onClick();
|
||||||
commandId: 0,
|
|
||||||
userAccelerator: null,
|
|
||||||
...menuItem,
|
|
||||||
} as MenuItem,
|
|
||||||
undefined,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -747,25 +748,6 @@ export const getApplicationBuilder = () => {
|
|||||||
return builder;
|
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({
|
export const rendererExtensionsStateInjectable = getInjectable({
|
||||||
id: "renderer-extensions-state",
|
id: "renderer-extensions-state",
|
||||||
instantiate: () => observable.map<string, LensRendererExtension>(),
|
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