diff --git a/src/behaviours/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap b/src/behaviours/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap new file mode 100644 index 0000000000..04b5432f31 --- /dev/null +++ b/src/behaviours/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap @@ -0,0 +1,1965 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Command Pallet: keyboard shortcut tests when on linux renders 1`] = ` + +
+
+
+
+
+
+ + + + + +
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on linux when pressing ESC renders 1`] = ` + +
+
+
+
+
+
+ + + + + +
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHIFT+CTRL+P renders 1`] = ` + +
+
+
+
+
+
+ + + + + +
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on linux when pressing SHIFT+CTRL+P when pressing ESC renders 1`] = ` + +
+
+
+
+
+
+ + + + + +
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on macOS renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing ESC renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHIFT+CMD+P renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`Command Pallet: keyboard shortcut tests when on macOS when pressing SHIFT+CMD+P when pressing ESC renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+ +
+
+

+ Welcome to OpenLens 5! +

+

+ To get you started we have auto-detected your clusters in your + + kubeconfig file and added them to the catalog, your centralized + + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our + + Lens Community slack channel + + . +

+ +
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ +`; diff --git a/src/behaviours/command-pallet/keyboard-shortcuts.test.tsx b/src/behaviours/command-pallet/keyboard-shortcuts.test.tsx new file mode 100644 index 0000000000..fba09862e3 --- /dev/null +++ b/src/behaviours/command-pallet/keyboard-shortcuts.test.tsx @@ -0,0 +1,148 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import platformInjectable from "../../common/vars/platform.injectable"; +import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; + +describe("Command Pallet: keyboard shortcut tests", () => { + let applicationBuilder: ApplicationBuilder; + let rendered: RenderResult; + + beforeEach(async () => { + applicationBuilder = getApplicationBuilder(); + }); + + describe("when on macOS", () => { + beforeEach(async () => { + applicationBuilder.dis.rendererDi.override(platformInjectable, () => "darwin"); + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show the command pallet yet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + + describe("when pressing ESC", () => { + beforeEach(() => { + userEvent.keyboard("{Escape}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show the command pallet yet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + }); + + describe("when pressing SHIFT+CMD+P", () => { + beforeEach(() => { + userEvent.keyboard("{Shift>}{Meta>}P{/Meta}{/Shift}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows the command pallet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeInTheDocument(); + }); + + describe("when pressing ESC", () => { + beforeEach(() => { + userEvent.keyboard("{Escape}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("no longer shows the command pallet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + }); + }); + }); + + describe("when on linux", () => { + beforeEach(async () => { + applicationBuilder.dis.rendererDi.override(platformInjectable, () => "linux"); + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show the command pallet yet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + + describe("when pressing ESC", () => { + beforeEach(() => { + userEvent.keyboard("{Escape}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not show the command pallet yet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + }); + + describe("when pressing SHIFT+CTRL+P", () => { + beforeEach(() => { + userEvent.keyboard("{Shift>}{Control>}P{/Control}{/Shift}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows the command pallet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeInTheDocument(); + }); + + describe("when pressing ESC", () => { + beforeEach(() => { + userEvent.keyboard("{Escape}"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("no longer shows the command pallet", () => { + const actual = rendered.queryByTestId("command-container"); + + expect(actual).toBeNull(); + }); + }); + }); + }); +}); diff --git a/src/extensions/registries/entity-setting-registry.ts b/src/extensions/registries/entity-setting-registry.ts index 60ac151133..0dc8eea23e 100644 --- a/src/extensions/registries/entity-setting-registry.ts +++ b/src/extensions/registries/entity-setting-registry.ts @@ -38,7 +38,7 @@ export class EntitySettingRegistry extends BaseRegistry { + getItemsForKind(kind: string, apiVersion: string, source?: string) { let items = this.getItems().filter((item) => { return item.kind === kind && item.apiVersions.includes(apiVersion); }); @@ -50,5 +50,5 @@ export class EntitySettingRegistry extends BaseRegistry (b.priority ?? 50) - (a.priority ?? 50)); - }; + } } diff --git a/src/renderer/components/command-palette/command-container-cluster-frame-child-component.injectable.ts b/src/renderer/components/command-palette/command-container-cluster-frame-child-component.injectable.ts index 09b1ce6c53..b517ab614a 100644 --- a/src/renderer/components/command-palette/command-container-cluster-frame-child-component.injectable.ts +++ b/src/renderer/components/command-palette/command-container-cluster-frame-child-component.injectable.ts @@ -9,15 +9,11 @@ import { clusterFrameChildComponentInjectionToken } from "../../frames/cluster-f const commandContainerClusterFrameChildComponentInjectable = getInjectable({ id: "command-container-cluster-frame-child-component", - instantiate: () => ({ id: "command-container", shouldRender: computed(() => true), Component: CommandContainer, }), - - causesSideEffects: true, - injectionToken: clusterFrameChildComponentInjectionToken, }); diff --git a/src/renderer/components/command-palette/command-container-root-frame-child-component.injectable.ts b/src/renderer/components/command-palette/command-container-root-frame-child-component.injectable.ts index f6c5105cc3..f1c7b72180 100644 --- a/src/renderer/components/command-palette/command-container-root-frame-child-component.injectable.ts +++ b/src/renderer/components/command-palette/command-container-root-frame-child-component.injectable.ts @@ -9,15 +9,11 @@ import { CommandContainer } from "./command-container"; const commandContainerRootFrameChildComponentInjectable = getInjectable({ id: "command-container-root-frame-child-component", - instantiate: () => ({ id: "command-container", shouldRender: computed(() => true), Component: CommandContainer, }), - - causesSideEffects: true, - injectionToken: rootFrameChildComponentInjectionToken, }); diff --git a/src/renderer/components/command-palette/command-container.scss b/src/renderer/components/command-palette/command-container.module.scss similarity index 82% rename from src/renderer/components/command-palette/command-container.scss rename to src/renderer/components/command-palette/command-container.module.scss index f5ec554842..89a98e137d 100644 --- a/src/renderer/components/command-palette/command-container.scss +++ b/src/renderer/components/command-palette/command-container.module.scss @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -#command-container { +.CommandContainer { position: absolute; top: 20px; left: 0; @@ -17,7 +17,7 @@ color: var(--settingsColor); transition: all 0.3s; - .Input { + :global(.Input) { label { caret-color: var(--blue); color: var(--settingsColor); @@ -33,20 +33,20 @@ } } - .hint { + :global(.hint) { padding: 8px; display: block; } - .errors { + :global(.errors) { padding: 8px; } - .Select__menu { + :global(.Select__menu) { position: relative; } - .Select__control { + :global(.Select__control) { padding: var(--padding); box-shadow: none; border-bottom: 1px solid var(--borderFaintColor); @@ -58,17 +58,17 @@ } } - .Select__menu { + :global(.Select__menu) { box-shadow: none; background: transparent; margin: 0; } - .Select__menu-list { + :global(.Select__menu-list) { padding: 0; } - .Select__option { + :global(.Select__option) { background-color: transparent; padding: 10px 18px; @@ -78,14 +78,14 @@ padding-left: 14px; } - &.Select__option--is-focused { + &:global(.Select__option--is-focused) { background-color: var(--menuSelectedOptionBgc); border-left: 4px solid var(--blue); padding-left: 14px; } } - .Select__menu-notice--no-options { + :global(.Select__menu-notice--no-options) { padding: 12px; } } diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index f435bdeec0..ca3d36bb00 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -4,7 +4,7 @@ */ -import "./command-container.scss"; +import styles from "./command-container.module.scss"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { Dialog } from "../dialog"; @@ -22,6 +22,7 @@ import matchedClusterIdInjectable from "../../navigation/matched-cluster-id.inje import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable"; import isMacInjectable from "../../../common/vars/is-mac.injectable"; import legacyOnChannelListenInjectable from "../../ipc/legacy-channel-listen.injectable"; +import { onKeyboardShortcut } from "../../utils/on-keyboard-shortcut"; interface Dependencies { addWindowEventListener: AddWindowEventListener; @@ -34,47 +35,38 @@ interface Dependencies { @observer class NonInjectedCommandContainer extends React.Component { - private escHandler = (event: KeyboardEvent) => { - if (event.key === "Escape") { - event.stopPropagation(); - this.props.commandOverlay.close(); - } - }; - - handleCommandPalette = () => { - const matchedClusterId = this.props.matchedClusterId.get(); - - if (matchedClusterId !== undefined) { - broadcastMessage(`command-palette:${matchedClusterId}:open`); - } else { - this.props.commandOverlay.open(); - } - }; - - onKeyboardShortcut(action: () => void) { - return ({ key, shiftKey, ctrlKey, altKey, metaKey }: KeyboardEvent) => { - const ctrlOrCmd = this.props.isMac ? metaKey && !ctrlKey : !metaKey && ctrlKey; - - if (key === "p" && shiftKey && ctrlOrCmd && !altKey) { - action(); - } - }; - } - componentDidMount() { - const { clusterId, addWindowEventListener, commandOverlay } = this.props; + const { clusterId, addWindowEventListener, commandOverlay, matchedClusterId, isMac } = this.props; const action = clusterId ? () => commandOverlay.open() - : this.handleCommandPalette; + : () => { + const matchedId = matchedClusterId.get(); + + if (matchedId) { + broadcastMessage(`command-palette:${matchedClusterId}:open`); + } else { + commandOverlay.open(); + } + }; const ipcChannel = clusterId ? `command-palette:${clusterId}:open` : "command-palette:open"; disposeOnUnmount(this, [ this.props.legacyOnChannelListen(ipcChannel, action), - addWindowEventListener("keydown", this.onKeyboardShortcut(action)), - addWindowEventListener("keyup", this.escHandler, true), + addWindowEventListener("keydown", onKeyboardShortcut( + isMac + ? "Shift+Cmd+P" + : "Shift+Ctrl+P", + action, + )), + addWindowEventListener("keydown", (event) => { + if (event.code === "Escape") { + event.stopPropagation(); + this.props.commandOverlay.close(); + } + }), ]); } @@ -84,11 +76,11 @@ class NonInjectedCommandContainer extends React.Component { return ( -
+
{commandOverlay.component}
diff --git a/src/renderer/components/command-palette/command-overlay.injectable.ts b/src/renderer/components/command-palette/command-overlay.injectable.ts index 542bab957d..988a5dcbdb 100644 --- a/src/renderer/components/command-palette/command-overlay.injectable.ts +++ b/src/renderer/components/command-palette/command-overlay.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; -import { observable } from "mobx"; +import { action, observable } from "mobx"; import React from "react"; export class CommandOverlay { @@ -14,17 +14,17 @@ export class CommandOverlay { return Boolean(this.#component.get()); } - open = (component: React.ReactElement) => { + open = action((component: React.ReactElement) => { if (!React.isValidElement(component)) { throw new TypeError("CommandOverlay.open must be passed a valid ReactElement"); } this.#component.set(component); - }; + }); - close = () => { + close = action(() => { this.#component.set(null); - }; + }); get component(): React.ReactElement | null { return this.#component.get(); diff --git a/src/renderer/components/command-palette/registered-commands/get-entity-setting-commands.injectable.ts b/src/renderer/components/command-palette/registered-commands/get-entity-setting-commands.injectable.ts new file mode 100644 index 0000000000..c2ea86d29c --- /dev/null +++ b/src/renderer/components/command-palette/registered-commands/get-entity-setting-commands.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { RegisteredEntitySetting } from "../../../../extensions/registries"; +import { EntitySettingRegistry } from "../../../../extensions/registries"; + +export type GetEntitySettingCommands = (kind: string, apiVersion: string, source?: string) => RegisteredEntitySetting[]; + +const getEntitySettingCommandsInjectable = getInjectable({ + id: "get-entity-setting-commands", + instantiate: (): GetEntitySettingCommands => { + const reg = EntitySettingRegistry.getInstance(); + + return (kind, apiVersion, source) => reg.getItemsForKind(kind, apiVersion, source); + }, + causesSideEffects: true, +}); + +export default getEntitySettingCommandsInjectable; diff --git a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx index 66f59df706..5bb37de137 100644 --- a/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx +++ b/src/renderer/components/command-palette/registered-commands/internal-commands.injectable.tsx @@ -5,7 +5,6 @@ import React from "react"; import type { RegisteredEntitySetting } from "../../../../extensions/registries"; -import { EntitySettingRegistry } from "../../../../extensions/registries"; import { HotbarAddCommand } from "../../hotbar/hotbar-add-command"; import { HotbarRemoveCommand } from "../../hotbar/hotbar-remove-command"; import { HotbarSwitchCommand } from "../../hotbar/hotbar-switch-command"; @@ -38,6 +37,7 @@ import navigateToJobsInjectable from "../../../../common/front-end-routing/route import navigateToCronJobsInjectable from "../../../../common/front-end-routing/routes/cluster/workloads/cron-jobs/navigate-to-cron-jobs.injectable"; import navigateToCustomResourcesInjectable from "../../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable"; import navigateToEntitySettingsInjectable from "../../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import getEntitySettingCommandsInjectable from "./get-entity-setting-commands.injectable"; export function isKubernetesClusterActive(context: CommandContext): boolean { return context.entity?.kind === "KubernetesCluster"; @@ -247,9 +247,7 @@ const internalCommandsInjectable = getInjectable({ instantiate: (di) => getInternalCommands({ openCommandDialog: di.inject(commandOverlayInjectable).open, - getEntitySettingItems: EntitySettingRegistry - .getInstance() - .getItemsForKind, + getEntitySettingItems: di.inject(getEntitySettingCommandsInjectable), createTerminalTab: di.inject(createTerminalTabInjectable), navigateToPreferences: di.inject(navigateToPreferencesInjectable), navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable), diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index d643dabfb0..1aad20d7f3 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -57,9 +57,7 @@ import goForwardInjectable from "./components/layout/top-bar/go-forward.injectab import closeWindowInjectable from "./components/layout/top-bar/close-window.injectable"; import maximizeWindowInjectable from "./components/layout/top-bar/maximize-window.injectable"; import toggleMaximizeWindowInjectable from "./components/layout/top-bar/toggle-maximize-window.injectable"; -import commandContainerRootFrameChildComponentInjectable from "./components/command-palette/command-container-root-frame-child-component.injectable"; import type { HotbarStore } from "../common/hotbars/store"; -import commandContainerClusterFrameChildComponentInjectable from "./components/command-palette/command-container-cluster-frame-child-component.injectable"; import cronJobTriggerDialogClusterFrameChildComponentInjectable from "./components/+workloads-cronjobs/cron-job-trigger-dialog-cluster-frame-child-component.injectable"; import deploymentScaleDialogClusterFrameChildComponentInjectable from "./components/+workloads-deployments/scale/deployment-scale-dialog-cluster-frame-child-component.injectable"; import replicasetScaleDialogClusterFrameChildComponentInjectable from "./components/+workloads-replicasets/scale-dialog/replicaset-scale-dialog-cluster-frame-child-component.injectable"; @@ -72,6 +70,8 @@ import setupSystemCaInjectable from "./frames/root-frame/setup-system-ca.injecta import extensionShouldBeEnabledForClusterFrameInjectable from "./extension-loader/extension-should-be-enabled-for-cluster-frame.injectable"; import { asyncComputed } from "@ogre-tools/injectable-react"; import forceUpdateModalRootFrameComponentInjectable from "./application-update/force-update-modal/force-update-modal-root-frame-component.injectable"; +import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable"; +import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable"; export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => { const { @@ -110,17 +110,12 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) di.override(appVersionInjectable, () => "1.0.0"); di.override(historyInjectable, () => createMemoryHistory()); - + di.override(legacyOnChannelListenInjectable, () => () => noop); di.override(requestAnimationFrameInjectable, () => (callback) => callback()); - di.override(lensResourcesDirInjectable, () => "/irrelevant"); - // TODO: Remove side-effects and shared global state - di.override(commandContainerRootFrameChildComponentInjectable, () => ({ - Component: () => null, - id: "command-container", - shouldRender: computed(() => false), - })); + // TODO: remove when entity settings registry is refactored + di.override(getEntitySettingCommandsInjectable, () => () => []); di.override(forceUpdateModalRootFrameComponentInjectable, () => ({ id: "force-update-modal", @@ -135,7 +130,6 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) // TODO: Remove side-effects and shared global state const clusterFrameChildComponentInjectables: Injectable[] = [ - commandContainerClusterFrameChildComponentInjectable, cronJobTriggerDialogClusterFrameChildComponentInjectable, deploymentScaleDialogClusterFrameChildComponentInjectable, replicasetScaleDialogClusterFrameChildComponentInjectable, diff --git a/src/renderer/utils/on-keyboard-shortcut.ts b/src/renderer/utils/on-keyboard-shortcut.ts new file mode 100644 index 0000000000..4504765ac7 --- /dev/null +++ b/src/renderer/utils/on-keyboard-shortcut.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { WindowEventListener } from "../window/event-listener.injectable"; + +function parseKeyDownDescriptor(descriptor: string): (event: KeyboardEvent) => boolean { + const parts = new Set(( + descriptor + .split("+") + .filter(Boolean) + .map(part => part.toLowerCase()) + )); + + if (parts.size === 0) { + return () => true; + } + + const hasShift = parts.delete("shift"); + const hasAlt = parts.delete("alt"); + + const rawHasCtrl = parts.delete("ctrl"); + const rawHasControl = parts.delete("control"); + const hasCtrl = rawHasCtrl || rawHasControl; + + const rawHasMeta = parts.delete("meta"); + const rawHasCmd = parts.delete("cmd"); + const hasMeta = rawHasCmd || rawHasMeta; // This means either matches + + const [key, ...rest] = [...parts]; + + if (rest.length !== 0) { + throw new Error("only single key combinations are currently supported"); + } + + return (event) => { + return event.altKey === hasAlt + && event.shiftKey === hasShift + && event.ctrlKey === hasCtrl + && event.metaKey === hasMeta + && event.key.toLowerCase() === key.toLowerCase(); + }; +} + +export function onKeyboardShortcut(descriptor: string, action: () => void): WindowEventListener<"keydown"> { + const isMatchingEvent = parseKeyDownDescriptor(descriptor); + + return (event) => { + if (isMatchingEvent(event)) { + action(); + } + }; +} diff --git a/src/renderer/window/event-listener.injectable.ts b/src/renderer/window/event-listener.injectable.ts index 84803cbd86..78a1827e19 100644 --- a/src/renderer/window/event-listener.injectable.ts +++ b/src/renderer/window/event-listener.injectable.ts @@ -7,8 +7,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Disposer } from "../utils"; export type AddWindowEventListener = typeof addWindowEventListener; +export type WindowEventListener = (this: Window, ev: WindowEventMap[K]) => any; -function addWindowEventListener(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer { +function addWindowEventListener(type: K, listener: WindowEventListener, options?: boolean | AddEventListenerOptions): Disposer { window.addEventListener(type, listener, options); return () => void window.removeEventListener(type, listener);