From 1861fe20496d768ea4756f8a21d9d20808f253d5 Mon Sep 17 00:00:00 2001 From: Panu Horsmalahti Date: Thu, 17 Nov 2022 18:03:25 +0200 Subject: [PATCH] Allow computed tray menu for extensions (#6598) * Add computed tray menu for extensions. Signed-off-by: Panu Horsmalahti * Use disposer. Fix style. Signed-off-by: Panu Horsmalahti * Register/reregister injectables by id. Signed-off-by: Panu Horsmalahti Signed-off-by: Panu Horsmalahti --- .../extension-registrator-injection-token.ts | 4 +- .../extension/extension.injectable.ts | 48 ++++++++-- src/extensions/lens-main-extension.ts | 4 +- .../tray/extension-adding-tray-items.test.tsx | 89 ++++++++++++++++++- ...art-reactive-tray-menu-items.injectable.ts | 4 +- .../tray-menu-item-registrator.injectable.ts | 12 +-- 6 files changed, 143 insertions(+), 18 deletions(-) diff --git a/src/extensions/extension-loader/extension-registrator-injection-token.ts b/src/extensions/extension-loader/extension-registrator-injection-token.ts index 6dd725c1b5..295d3b67a4 100644 --- a/src/extensions/extension-loader/extension-registrator-injection-token.ts +++ b/src/extensions/extension-loader/extension-registrator-injection-token.ts @@ -4,9 +4,11 @@ */ import type { Injectable } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable"; +import type { IComputedValue } from "mobx"; import type { LensExtension } from "../lens-extension"; -export type ExtensionRegistrator = (extension: LensExtension) => Injectable[]; +export type ExtensionRegistrator = (extension: LensExtension) => + Injectable[] | IComputedValue[]>; export const extensionRegistratorInjectionToken = getInjectionToken({ id: "extension-registrator-token", diff --git a/src/extensions/extension-loader/extension/extension.injectable.ts b/src/extensions/extension-loader/extension/extension.injectable.ts index 7ed23a3dfa..6b9424cea4 100644 --- a/src/extensions/extension-loader/extension/extension.injectable.ts +++ b/src/extensions/extension-loader/extension/extension.injectable.ts @@ -2,8 +2,11 @@ * 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, lifecycleEnum } from "@ogre-tools/injectable"; -import { runInAction } from "mobx"; +import { difference, find, map } from "lodash"; +import { reaction, runInAction } from "mobx"; +import { disposer } from "../../../common/utils/disposer"; import type { LensExtension } from "../../lens-extension"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; @@ -12,6 +15,16 @@ export interface Extension { deregister: () => void; } +const idsToInjectables = (ids: string[], injectables: Injectable[]) => ids.map(id => { + const injectable = find(injectables, { id }); + + if (!injectable) { + throw new Error(`Injectable ${id} not found`); + } + + return injectable; +}); + const extensionInjectable = getInjectable({ id: "extension", @@ -21,19 +34,42 @@ const extensionInjectable = getInjectable({ instantiate: (childDi) => { const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); + const reactionDisposer = disposer(); return { register: () => { - const injectables = extensionRegistrators.flatMap((getInjectablesOfExtension) => - getInjectablesOfExtension(instance), - ); + extensionRegistrators.forEach((getInjectablesOfExtension) => { + const injectables = getInjectablesOfExtension(instance); - runInAction(() => { - childDi.register(...injectables); + reactionDisposer.push( + // injectables is either an array or a computed array, in which case + // we need to update the registered injectables with a reaction every time they change + reaction( + () => Array.isArray(injectables) ? injectables : injectables.get(), + (currentInjectables, previousInjectables = []) => { + // Register new injectables and deregister removed injectables by id + const currentIds = map(currentInjectables, "id"); + const previousIds = map(previousInjectables, "id"); + const idsToAdd = difference(currentIds, previousIds); + const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId)); + + if (idsToRemove.length > 0) { + childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables)); + } + + if (idsToAdd.length > 0) { + childDi.register(...idsToInjectables(idsToAdd, currentInjectables)); + } + }, { + fireImmediately: true, + }, + )); }); }, deregister: () => { + reactionDisposer(); + runInAction(() => { parentDi.deregister(extensionInjectable); }); diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 6c901db4ed..b97d8da834 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -5,7 +5,7 @@ import { LensExtension, lensExtensionDependencies } from "./lens-extension"; import type { CatalogEntity } from "../common/catalog"; -import type { IObservableArray } from "mobx"; +import type { IComputedValue, IObservableArray } from "mobx"; import type { MenuRegistration } from "../features/application-menu/main/menu-registration"; import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration"; import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration"; @@ -13,7 +13,7 @@ import type { LensMainExtensionDependencies } from "./lens-extension-set-depende export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; - trayMenus: TrayMenuRegistration[] = []; + trayMenus: TrayMenuRegistration[] | IComputedValue = []; /** * implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature diff --git a/src/features/tray/extension-adding-tray-items.test.tsx b/src/features/tray/extension-adding-tray-items.test.tsx index 5d2efbe37b..2b640be07c 100644 --- a/src/features/tray/extension-adding-tray-items.test.tsx +++ b/src/features/tray/extension-adding-tray-items.test.tsx @@ -2,13 +2,14 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { IObservableValue } from "mobx"; +import type { IObservableArray, IObservableValue } from "mobx"; import { computed, runInAction, observable } from "mobx"; +import type { TrayMenuRegistration } from "../../main/tray/tray-menu-registration"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; describe("preferences: extension adding tray items", () => { - describe("when extension with tray items is enabled", () => { + describe("when extension with tray items are statically defined", () => { let builder: ApplicationBuilder; let someObservableForVisibility: IObservableValue; let someObservableForEnabled: IObservableValue; @@ -190,4 +191,88 @@ describe("preferences: extension adding tray items", () => { expect(item?.enabled).toBe(false); }); }); + + describe("when extension with tray items are dynamically defined", () => { + let builder: ApplicationBuilder; + let menuItems: IObservableArray; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + await builder.render(); + + builder.preferences.navigate(); + + menuItems = observable.array([ + { + id: "some-visible", + label: "some-visible", + click: () => {}, + visible: computed(() => true), + }, + ]); + + const computedTrayMenu = computed(() => menuItems); + + const testExtension = { + id: "some-extension-id", + name: "some-extension", + + mainOptions: { + trayMenus: computedTrayMenu, + }, + }; + + builder.extensions.enable(testExtension); + }); + + it("given item exists, it's shown", () => { + expect( + builder.tray.get( + "some-visible-tray-menu-item-for-extension-some-extension", + ), + ).not.toBeNull(); + }); + + it("given item is added, it's shown", async () => { + menuItems.push({ + id: "some-added", + label: "some-added", + click: () => {}, + visible: computed(() => true), + }); + + expect( + builder.tray.get( + "some-added-tray-menu-item-for-extension-some-extension", + ), + ).not.toBeNull(); + }); + + it("given item is removed, it's not shown", async () => { + menuItems.replace([]); + + expect( + builder.tray.get( + "some-visible-tray-menu-item-for-extension-some-extension", + ), + ).toBeNull(); + }); + + it("given items are removed and one added, it's shown", async () => { + menuItems.replace([]); + menuItems.push({ + id: "some-added", + label: "some-added", + click: () => {}, + visible: computed(() => true), + }); + + expect( + builder.tray.get( + "some-added-tray-menu-item-for-extension-some-extension", + ), + ).not.toBeNull(); + }); + }); }); diff --git a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts index 7abd07e91b..634c6cd875 100644 --- a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts @@ -15,8 +15,8 @@ const startReactiveTrayMenuItemsInjectable = getInjectable({ return { id: "start-reactive-tray-menu-items", - run: async () => { - await reactiveTrayMenuItems.start(); + run: () => { + reactiveTrayMenuItems.start(); }, runAfter: di.inject(startTrayInjectable), diff --git a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts index f1ebe7885d..573af2f545 100644 --- a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts +++ b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts @@ -25,9 +25,13 @@ const trayMenuItemRegistratorInjectable = getInjectable({ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable); const getRandomId = di.inject(getRandomIdInjectable); - return mainExtension.trayMenus.flatMap( - toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId), - ); + return computed(() => { + const trayMenus = Array.isArray(mainExtension.trayMenus) ? mainExtension.trayMenus : mainExtension.trayMenus.get(); + + return trayMenus.flatMap( + toItemInjectablesFor(mainExtension, withErrorLoggingFor, getRandomId), + ); + }); }, injectionToken: extensionRegistratorInjectionToken, @@ -117,5 +121,3 @@ const toItemInjectablesFor = (extension: LensMainExtension, withErrorLoggingFor: return _toItemInjectables(null); }; - -