diff --git a/src/features/application-menu/application-menu-in-legacy-extension-api.test.ts b/src/features/application-menu/application-menu-in-legacy-extension-api.test.ts index 277dd41641..06dbc384c7 100644 --- a/src/features/application-menu/application-menu-in-legacy-extension-api.test.ts +++ b/src/features/application-menu/application-menu-in-legacy-extension-api.test.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { noop } from "lodash/fp"; 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"; @@ -40,6 +41,14 @@ describe("application-menu-in-legacy-extension-api", () => { mainOptions: { appMenus: [ + { + id: "some-non-shown-item", + parentId: "some-top-menu-item", + click: noop, + label: "Irrelevant", + visible: false, + }, + { id: "some-clickable-item", parentId: "some-top-menu-item", @@ -50,6 +59,21 @@ describe("application-menu-in-legacy-extension-api", () => { parentId: "some-top-menu-item", type: "separator", }, + + { + id: "some-os-action-menu-item-id", + parentId: "some-top-menu-item", + role: "help", + }, + + { + id: "some-submenu-with-explicit-children", + parentId: "some-top-menu-item", + + submenu: [ + { id: "some-explicit-child", label: "Some explicit child", click: noop }, + ], + }, ], }, }; @@ -57,21 +81,25 @@ describe("application-menu-in-legacy-extension-api", () => { builder.extensions.enable(testExtensionOptions); }); - it("menu related items exist", () => { + it("related menu 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)", + "root.some-top-menu-item.some-extension-name/some-clickable-item", + // Note: anonymous index "1" is used by the non-visible menu item. + "root.some-top-menu-item.some-extension-name/2-separator", + "root.some-top-menu-item.some-extension-name/some-os-action-menu-item-id", + "root.some-top-menu-item.some-extension-name/some-submenu-with-explicit-children", + "root.some-top-menu-item.some-extension-name/some-submenu-with-explicit-children.some-extension-name/some-submenu-with-explicit-children/some-explicit-child", ]); }); it("when the extension-based clickable menu item is clicked, does so", () => { builder.applicationMenu.click( - "root.some-top-menu-item.some-extension-name/application-menu-item/clickable-menu-item(some-clickable-item)", + "root.some-top-menu-item.some-extension-name/some-clickable-item", ); expect(onClickMock).toHaveBeenCalled(); @@ -100,8 +128,11 @@ describe("application-menu-in-legacy-extension-api", () => { ); 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)", + "root.some-top-menu-item.some-extension-name/some-clickable-item", + "root.some-top-menu-item.some-extension-name/2-separator", + "root.some-top-menu-item.some-extension-name/some-os-action-menu-item-id", + "root.some-top-menu-item.some-extension-name/some-submenu-with-explicit-children", + "root.some-top-menu-item.some-extension-name/some-submenu-with-explicit-children.some-extension-name/some-submenu-with-explicit-children/some-explicit-child", ]); }); }); diff --git a/src/features/application-menu/main/application-menu-item-registrator.injectable.ts b/src/features/application-menu/main/application-menu-item-registrator.injectable.ts index e4743338fb..ba8d34953f 100644 --- a/src/features/application-menu/main/application-menu-item-registrator.injectable.ts +++ b/src/features/application-menu/main/application-menu-item-registrator.injectable.ts @@ -2,12 +2,19 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable"; import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; import type { LensExtension } from "../../../extensions/lens-extension"; import type { LensMainExtension } from "../../../extensions/lens-main-extension"; -import type { ClickableMenuItem, Separator } from "./menu-items/application-menu-item-injection-token"; +import type { + ApplicationMenuItemTypes, + ClickableMenuItem, + OsActionMenuItem, + Separator, +} from "./menu-items/application-menu-item-injection-token"; import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token"; +import type { MenuRegistration } from "./menu-registration"; const applicationMenuItemRegistratorInjectable = getInjectable({ id: "application-menu-item-registrator", @@ -15,44 +22,122 @@ const applicationMenuItemRegistratorInjectable = getInjectable({ 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, - }); - }); + return extension.appMenus.flatMap( + toRecursedInjectables([extension.sanitizedExtensionId]), + ); }, injectionToken: extensionRegistratorInjectionToken, }); export default applicationMenuItemRegistratorInjectable; + +const toRecursedInjectables = + (previousIdPath: string[]) => + ( + registration: MenuRegistration, + index: number, + // Todo: new version of injectable would require less type parameters with defaults. + ): Injectable[] => { + const previousIdPathString = previousIdPath.join("/"); + const registrationId = registration.id || index.toString(); + const currentIdPath = [...previousIdPath, registrationId]; + const currentIdPathString = currentIdPath.join("/"); + const parentId = registration.parentId || previousIdPathString; + + const menuItem = getApplicationMenuItem({ + registration, + parentId, + currentIdPathString, + index, + }); + + if(!menuItem) { + return []; + } + + return [ + getInjectable({ + id: `${currentIdPathString}/application-menu-item`, + + instantiate: () => menuItem, + + injectionToken: applicationMenuItemInjectionToken, + }), + + ...((registration.submenu as MenuRegistration[]) + ? (registration.submenu as MenuRegistration[]).flatMap( + toRecursedInjectables(currentIdPath), + ) + : []), + ]; + }; + +const getApplicationMenuItem = ({ + registration, + index, + currentIdPathString, + parentId, +}: { + registration: MenuRegistration; + index: number; + currentIdPathString: string; + parentId: string; +}): ApplicationMenuItemTypes | undefined => { + const orderNumber = 1000 + index * 10; + + if (registration.type === "separator") { + return { + kind: "separator" as const, + id: `${currentIdPathString}-separator`, + parentId, + orderNumber, + } as Separator; + } + + if (registration.submenu) { + return { + kind: "sub-menu" as const, + id: currentIdPathString, + parentId, + isShown: registration.visible ?? true, + orderNumber, + label: registration.label || "", + }; + } + + if (registration.click) { + return { + kind: "clickable-menu-item" as const, + id: currentIdPathString, + parentId, + // Todo: hide electron events from this abstraction. + onClick: registration.click, + label: registration.label, + isShown: registration.visible ?? true, + orderNumber, + + ...(registration.accelerator + ? { keyboardShortcut: registration.accelerator as string } + : {}), + } as ClickableMenuItem; + } + + if (registration.role) { + return { + kind: "os-action-menu-item" as const, + id: currentIdPathString, + parentId, + label: registration.label, + isShown: registration.visible ?? true, + orderNumber, + actionName: registration.role, + + ...(registration.accelerator + ? { keyboardShortcut: registration.accelerator as string } + : {}), + } as OsActionMenuItem; + } + + return undefined; +};