diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 9baa291acd..95a244f3c1 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -26,7 +26,7 @@ import logger from "../../main/logger"; import { AppPaths } from "../app-paths"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import { ClusterStore } from "../cluster-store"; -import { HotbarStore } from "../hotbar-store.injectable"; +import { HotbarStore } from "../hotbar-store"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { diff --git a/src/common/hotbar-store.injectable.ts b/src/common/hotbar-store.injectable.ts index daad150c2e..4d01f3bada 100644 --- a/src/common/hotbar-store.injectable.ts +++ b/src/common/hotbar-store.injectable.ts @@ -18,327 +18,8 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -import { action, comparer, observable, makeObservable, computed } from "mobx"; -import { BaseStore } from "./base-store"; -import migrations from "../migrations/hotbar-store"; -import { toJS } from "./utils"; -import { CatalogEntity } from "./catalog"; -import { catalogEntity } from "../main/catalog-sources/general"; -import logger from "../main/logger"; -import { broadcastMessage, HotbarTooManyItems } from "./ipc"; -import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; - -export interface HotbarStoreModel { - hotbars: Hotbar[]; - activeHotbarId: string; -} - -export class HotbarStore extends BaseStore { - readonly displayName = "HotbarStore"; - @observable hotbars: Hotbar[] = []; - @observable private _activeHotbarId: string; - - constructor() { - super({ - configName: "lens-hotbar-store", - accessPropertiesByDotNotation: false, // To make dots safe in cluster context names - syncOptions: { - equals: comparer.structural, - }, - migrations, - }); - makeObservable(this); - this.load(); - } - - @computed get activeHotbarId() { - return this._activeHotbarId; - } - - /** - * If `hotbar` points to a known hotbar, make it active. Otherwise, ignore - * @param hotbar The hotbar instance, or the index, or its ID - */ - setActiveHotbar(hotbar: Hotbar | number | string) { - if (typeof hotbar === "number") { - if (hotbar >= 0 && hotbar < this.hotbars.length) { - this._activeHotbarId = this.hotbars[hotbar].id; - } - } else if (typeof hotbar === "string") { - if (this.getById(hotbar)) { - this._activeHotbarId = hotbar; - } - } else { - if (this.hotbars.indexOf(hotbar) >= 0) { - this._activeHotbarId = hotbar.id; - } - } - } - - private hotbarIndexById(id: string) { - return this.hotbars.findIndex((hotbar) => hotbar.id === id); - } - - private hotbarIndex(hotbar: Hotbar) { - return this.hotbars.indexOf(hotbar); - } - - @computed get activeHotbarIndex() { - return this.hotbarIndexById(this.activeHotbarId); - } - - @action - protected fromStore(data: Partial = {}) { - if (!data.hotbars || !data.hotbars.length) { - const hotbar = getEmptyHotbar("Default"); - const { metadata: { uid, name, source }} = catalogEntity; - const initialItem = { entity: { uid, name, source }}; - - hotbar.items[0] = initialItem; - - this.hotbars = [hotbar]; - } else { - this.hotbars = data.hotbars; - } - - this.hotbars.forEach(ensureExactHotbarItemLength); - - if (data.activeHotbarId) { - this.setActiveHotbar(data.activeHotbarId); - } - - if (!this.activeHotbarId) { - this.setActiveHotbar(0); - } - } - - toJSON(): HotbarStoreModel { - const model: HotbarStoreModel = { - hotbars: this.hotbars, - activeHotbarId: this.activeHotbarId, - }; - - return toJS(model); - } - - getActive() { - return this.getById(this.activeHotbarId); - } - - getByName(name: string) { - return this.hotbars.find((hotbar) => hotbar.name === name); - } - - getById(id: string) { - return this.hotbars.find((hotbar) => hotbar.id === id); - } - - add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => { - const hotbar = getEmptyHotbar(data.name, data.id); - - this.hotbars.push(hotbar); - - if (setActive) { - this._activeHotbarId = hotbar.id; - } - }); - - setHotbarName = action((id: string, name: string) => { - const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); - - if (index < 0) { - return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }); - } - - this.hotbars[index].name = name; - }); - - remove = action((hotbar: Hotbar) => { - if (this.hotbars.length <= 1) { - throw new Error("Cannot remove the last hotbar"); - } - - this.hotbars = this.hotbars.filter((h) => h !== hotbar); - - if (this.activeHotbarId === hotbar.id) { - this.setActiveHotbar(0); - } - }); - - @action - addToHotbar(item: CatalogEntity, cellIndex?: number) { - const hotbar = this.getActive(); - const uid = item.metadata?.uid; - const name = item.metadata?.name; - - if (typeof uid !== "string") { - throw new TypeError("CatalogEntity.metadata.uid must be a string"); - } - - if (typeof name !== "string") { - throw new TypeError("CatalogEntity.metadata.name must be a string"); - } - - const newItem = { entity: { - uid, - name, - source: item.metadata.source, - }}; - - - if (this.isAddedToActive(item)) { - return; - } - - if (cellIndex === undefined) { - // Add item to empty cell - const emptyCellIndex = hotbar.items.indexOf(null); - - if (emptyCellIndex != -1) { - hotbar.items[emptyCellIndex] = newItem; - } else { - broadcastMessage(HotbarTooManyItems); - } - } else if (0 <= cellIndex && cellIndex < hotbar.items.length) { - hotbar.items[cellIndex] = newItem; - } else { - logger.error(`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, { entityId: uid, hotbarId: hotbar.id, cellIndex }); - } - } - - @action - removeFromHotbar(uid: string): void { - const hotbar = this.getActive(); - const index = hotbar.items.findIndex(item => item?.entity.uid === uid); - - if (index < 0) { - return; - } - - hotbar.items[index] = null; - } - - /** - * Remove all hotbar items that reference the `uid`. - * @param uid The `EntityId` that each hotbar item refers to - * @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed. - */ - @action - removeAllHotbarItems(uid: string) { - for (const hotbar of this.hotbars) { - const index = hotbar.items.findIndex((i) => i?.entity.uid === uid); - - if (index >= 0) { - hotbar.items[index] = null; - } - } - } - - findClosestEmptyIndex(from: number, direction = 1) { - let index = from; - - while(this.getActive().items[index] != null) { - index += direction; - } - - return index; - } - - @action - restackItems(from: number, to: number): void { - const { items } = this.getActive(); - const source = items[from]; - const moveDown = from < to; - - if (from < 0 || to < 0 || from >= items.length || to >= items.length || isNaN(from) || isNaN(to)) { - throw new Error("Invalid 'from' or 'to' arguments"); - } - - if (from == to) { - return; - } - - items.splice(from, 1, null); - - if (items[to] == null) { - items.splice(to, 1, source); - } else { - // Move cells up or down to closes empty cell - items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1); - items.splice(to, 0, source); - } - } - - switchToPrevious() { - const hotbarStore = HotbarStore.getInstance(); - let index = hotbarStore.activeHotbarIndex - 1; - - if (index < 0) { - index = hotbarStore.hotbars.length - 1; - } - - hotbarStore.setActiveHotbar(index); - } - - switchToNext() { - const hotbarStore = HotbarStore.getInstance(); - let index = hotbarStore.activeHotbarIndex + 1; - - if (index >= hotbarStore.hotbars.length) { - index = 0; - } - - hotbarStore.setActiveHotbar(index); - } - - /** - * Checks if entity already pinned to hotbar - * @returns boolean - */ - isAddedToActive(entity: CatalogEntity) { - return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); - } - - getDisplayLabel(hotbar: Hotbar): string { - return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`; - } - - getDisplayIndex(hotbar: Hotbar): string { - const index = this.hotbarIndex(hotbar); - - if (index < 0) { - return "??"; - } - - return `${index + 1}`; - } -} - -/** - * This function ensures that there are always exactly `defaultHotbarCells` - * worth of items in the hotbar. - * @param hotbar The hotbar to modify - */ -function ensureExactHotbarItemLength(hotbar: Hotbar) { - // if there are not enough items - while (hotbar.items.length < defaultHotbarCells) { - hotbar.items.push(null); - } - - // if for some reason the hotbar was overfilled before, remove as many entries - // as needed, but prefer empty slots and items at the end first. - while (hotbar.items.length > defaultHotbarCells) { - const lastNull = hotbar.items.lastIndexOf(null); - - if (lastNull >= 0) { - hotbar.items.splice(lastNull, 1); - } else { - hotbar.items.length = defaultHotbarCells; - } - } -} +import { HotbarStore } from "./hotbar-store"; const hotbarManagerInjectable = getInjectable({ instantiate: () => HotbarStore.getInstance(), diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts new file mode 100644 index 0000000000..504ba55cda --- /dev/null +++ b/src/common/hotbar-store.ts @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { action, comparer, observable, makeObservable, computed } from "mobx"; +import { BaseStore } from "./base-store"; +import migrations from "../migrations/hotbar-store"; +import { toJS } from "./utils"; +import { CatalogEntity } from "./catalog"; +import { catalogEntity } from "../main/catalog-sources/general"; +import logger from "../main/logger"; +import { broadcastMessage, HotbarTooManyItems } from "./ipc"; +import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; + +export interface HotbarStoreModel { + hotbars: Hotbar[]; + activeHotbarId: string; +} + +export class HotbarStore extends BaseStore { + readonly displayName = "HotbarStore"; + @observable hotbars: Hotbar[] = []; + @observable private _activeHotbarId: string; + + constructor() { + super({ + configName: "lens-hotbar-store", + accessPropertiesByDotNotation: false, // To make dots safe in cluster context names + syncOptions: { + equals: comparer.structural, + }, + migrations, + }); + makeObservable(this); + this.load(); + } + + @computed get activeHotbarId() { + return this._activeHotbarId; + } + + /** + * If `hotbar` points to a known hotbar, make it active. Otherwise, ignore + * @param hotbar The hotbar instance, or the index, or its ID + */ + setActiveHotbar(hotbar: Hotbar | number | string) { + if (typeof hotbar === "number") { + if (hotbar >= 0 && hotbar < this.hotbars.length) { + this._activeHotbarId = this.hotbars[hotbar].id; + } + } else if (typeof hotbar === "string") { + if (this.getById(hotbar)) { + this._activeHotbarId = hotbar; + } + } else { + if (this.hotbars.indexOf(hotbar) >= 0) { + this._activeHotbarId = hotbar.id; + } + } + } + + private hotbarIndexById(id: string) { + return this.hotbars.findIndex((hotbar) => hotbar.id === id); + } + + private hotbarIndex(hotbar: Hotbar) { + return this.hotbars.indexOf(hotbar); + } + + @computed get activeHotbarIndex() { + return this.hotbarIndexById(this.activeHotbarId); + } + + @action + protected fromStore(data: Partial = {}) { + if (!data.hotbars || !data.hotbars.length) { + const hotbar = getEmptyHotbar("Default"); + const { metadata: { uid, name, source }} = catalogEntity; + const initialItem = { entity: { uid, name, source }}; + + hotbar.items[0] = initialItem; + + this.hotbars = [hotbar]; + } else { + this.hotbars = data.hotbars; + } + + this.hotbars.forEach(ensureExactHotbarItemLength); + + if (data.activeHotbarId) { + this.setActiveHotbar(data.activeHotbarId); + } + + if (!this.activeHotbarId) { + this.setActiveHotbar(0); + } + } + + toJSON(): HotbarStoreModel { + const model: HotbarStoreModel = { + hotbars: this.hotbars, + activeHotbarId: this.activeHotbarId, + }; + + return toJS(model); + } + + getActive() { + return this.getById(this.activeHotbarId); + } + + getByName(name: string) { + return this.hotbars.find((hotbar) => hotbar.name === name); + } + + getById(id: string) { + return this.hotbars.find((hotbar) => hotbar.id === id); + } + + add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => { + const hotbar = getEmptyHotbar(data.name, data.id); + + this.hotbars.push(hotbar); + + if (setActive) { + this._activeHotbarId = hotbar.id; + } + }); + + setHotbarName = action((id: string, name: string) => { + const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); + + if (index < 0) { + return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }); + } + + this.hotbars[index].name = name; + }); + + remove = action((hotbar: Hotbar) => { + if (this.hotbars.length <= 1) { + throw new Error("Cannot remove the last hotbar"); + } + + this.hotbars = this.hotbars.filter((h) => h !== hotbar); + + if (this.activeHotbarId === hotbar.id) { + this.setActiveHotbar(0); + } + }); + + @action + addToHotbar(item: CatalogEntity, cellIndex?: number) { + const hotbar = this.getActive(); + const uid = item.metadata?.uid; + const name = item.metadata?.name; + + if (typeof uid !== "string") { + throw new TypeError("CatalogEntity.metadata.uid must be a string"); + } + + if (typeof name !== "string") { + throw new TypeError("CatalogEntity.metadata.name must be a string"); + } + + const newItem = { entity: { + uid, + name, + source: item.metadata.source, + }}; + + + if (this.isAddedToActive(item)) { + return; + } + + if (cellIndex === undefined) { + // Add item to empty cell + const emptyCellIndex = hotbar.items.indexOf(null); + + if (emptyCellIndex != -1) { + hotbar.items[emptyCellIndex] = newItem; + } else { + broadcastMessage(HotbarTooManyItems); + } + } else if (0 <= cellIndex && cellIndex < hotbar.items.length) { + hotbar.items[cellIndex] = newItem; + } else { + logger.error(`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, { entityId: uid, hotbarId: hotbar.id, cellIndex }); + } + } + + @action + removeFromHotbar(uid: string): void { + const hotbar = this.getActive(); + const index = hotbar.items.findIndex(item => item?.entity.uid === uid); + + if (index < 0) { + return; + } + + hotbar.items[index] = null; + } + + /** + * Remove all hotbar items that reference the `uid`. + * @param uid The `EntityId` that each hotbar item refers to + * @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed. + */ + @action + removeAllHotbarItems(uid: string) { + for (const hotbar of this.hotbars) { + const index = hotbar.items.findIndex((i) => i?.entity.uid === uid); + + if (index >= 0) { + hotbar.items[index] = null; + } + } + } + + findClosestEmptyIndex(from: number, direction = 1) { + let index = from; + + while(this.getActive().items[index] != null) { + index += direction; + } + + return index; + } + + @action + restackItems(from: number, to: number): void { + const { items } = this.getActive(); + const source = items[from]; + const moveDown = from < to; + + if (from < 0 || to < 0 || from >= items.length || to >= items.length || isNaN(from) || isNaN(to)) { + throw new Error("Invalid 'from' or 'to' arguments"); + } + + if (from == to) { + return; + } + + items.splice(from, 1, null); + + if (items[to] == null) { + items.splice(to, 1, source); + } else { + // Move cells up or down to closes empty cell + items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1); + items.splice(to, 0, source); + } + } + + switchToPrevious() { + const hotbarStore = HotbarStore.getInstance(); + let index = hotbarStore.activeHotbarIndex - 1; + + if (index < 0) { + index = hotbarStore.hotbars.length - 1; + } + + hotbarStore.setActiveHotbar(index); + } + + switchToNext() { + const hotbarStore = HotbarStore.getInstance(); + let index = hotbarStore.activeHotbarIndex + 1; + + if (index >= hotbarStore.hotbars.length) { + index = 0; + } + + hotbarStore.setActiveHotbar(index); + } + + /** + * Checks if entity already pinned to hotbar + * @returns boolean + */ + isAddedToActive(entity: CatalogEntity) { + return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid); + } + + getDisplayLabel(hotbar: Hotbar): string { + return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`; + } + + getDisplayIndex(hotbar: Hotbar): string { + const index = this.hotbarIndex(hotbar); + + if (index < 0) { + return "??"; + } + + return `${index + 1}`; + } +} + +/** + * This function ensures that there are always exactly `defaultHotbarCells` + * worth of items in the hotbar. + * @param hotbar The hotbar to modify + */ +function ensureExactHotbarItemLength(hotbar: Hotbar) { + // if there are not enough items + while (hotbar.items.length < defaultHotbarCells) { + hotbar.items.push(null); + } + + // if for some reason the hotbar was overfilled before, remove as many entries + // as needed, but prefer empty slots and items at the end first. + while (hotbar.items.length > defaultHotbarCells) { + const lastNull = hotbar.items.lastIndexOf(null); + + if (lastNull >= 0) { + hotbar.items.splice(lastNull, 1); + } else { + hotbar.items.length = defaultHotbarCells; + } + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 69a42d8383..f40b92aa21 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -51,7 +51,7 @@ import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; import { ClusterStore } from "../common/cluster-store"; -import { HotbarStore } from "../common/hotbar-store.injectable"; +import { HotbarStore } from "../common/hotbar-store"; import { UserStore } from "../common/user-store"; import { WeblinkStore } from "../common/weblink-store"; import { ExtensionsStore } from "../extensions/extensions-store"; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 9174d30fb2..b9f48b2b8c 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -40,7 +40,7 @@ import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; import * as initializers from "./initializers"; import logger from "../common/logger"; -import { HotbarStore } from "../common/hotbar-store.injectable"; +import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; import { ExtensionsStore } from "../extensions/extensions-store"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 7774377ccc..595ba6a81c 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -29,7 +29,7 @@ import { CatalogEntityStore } from "./catalog-entity.store"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; -import { HotbarStore } from "../../../common/hotbar-store.injectable"; +import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog"; import { CatalogAddButton } from "./catalog-add-button"; diff --git a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx index 260b0d5301..da89ff3f26 100644 --- a/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx +++ b/src/renderer/components/+catalog/hotbar-toggle-menu-item.tsx @@ -20,7 +20,7 @@ */ import React, { ReactNode, useState } from "react"; -import { HotbarStore } from "../../../common/hotbar-store.injectable"; +import { HotbarStore } from "../../../common/hotbar-store"; import { MenuItem } from "../menu"; import type { CatalogEntity } from "../../api/catalog-entity"; diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index 41ac450aae..e6a03444b6 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -31,7 +31,7 @@ import { saveKubeconfig } from "./save-config"; import { requestMain } from "../../../common/ipc"; import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc"; import { Notifications } from "../notifications"; -import { HotbarStore } from "../../../common/hotbar-store.injectable"; +import { HotbarStore } from "../../../common/hotbar-store"; import { boundMethod } from "autobind-decorator"; import { Dialog } from "../dialog"; import { Icon } from "../icon"; diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index 330bc2930d..334d9205ad 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -27,10 +27,11 @@ import { AppPaths } from "../../../../common/app-paths"; import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { type DiRender, renderFor } from "../../test-utils/renderFor"; -import hotbarManagerInjectable, { HotbarStore } from "../../../../common/hotbar-store.injectable"; +import hotbarManagerInjectable from "../../../../common/hotbar-store.injectable"; import { UserStore } from "../../../../common/user-store"; import { ThemeStore } from "../../../theme.store"; import { ConfirmDialog } from "../../confirm-dialog"; +import type { HotbarStore } from "../../../../common/hotbar-store"; jest.mock("electron", () => ({ app: { diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx index 2380a76f10..e5faf0cb76 100644 --- a/src/renderer/components/hotbar/hotbar-add-command.tsx +++ b/src/renderer/components/hotbar/hotbar-add-command.tsx @@ -21,24 +21,20 @@ import React from "react"; import { observer } from "mobx-react"; -import hotbarManagerInjectable, { HotbarStore } from "../../../common/hotbar-store.injectable"; import { Input, InputValidator } from "../input"; import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbar-types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; - -export const uniqueHotbarName: InputValidator = { - condition: ({ required }) => required, - message: () => "Hotbar with this name already exists", - validate: value => !HotbarStore.getInstance().getByName(value), -}; +import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; +import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; interface Dependencies { closeCommandOverlay: () => void; addHotbar: (data: CreateHotbarData, { setActive }?: CreateHotbarOptions) => void; + uniqueHotbarName: InputValidator; } -const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar }: Dependencies) => { +const NonInjectedHotbarAddCommand = observer(({ closeCommandOverlay, addHotbar, uniqueHotbarName }: Dependencies) => { const onSubmit = (name: string) => { if (!name.trim()) { return; @@ -71,6 +67,7 @@ export const HotbarAddCommand = withInjectables(NonInjectedHotbarA getProps: (di, props) => ({ closeCommandOverlay: di.inject(commandOverlayInjectable).close, addHotbar: di.inject(hotbarManagerInjectable).add, + uniqueHotbarName: di.inject(uniqueHotbarNameInjectable), ...props, }), }); diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index c55aff4f19..897d4c0360 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -26,7 +26,7 @@ import { observer } from "mobx-react"; import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { HotbarStore } from "../../../common/hotbar-store.injectable"; +import { HotbarStore } from "../../../common/hotbar-store"; import type { CatalogEntity } from "../../api/catalog-entity"; import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd"; import { HotbarSelector } from "./hotbar-selector"; diff --git a/src/renderer/components/hotbar/hotbar-rename-command.tsx b/src/renderer/components/hotbar/hotbar-rename-command.tsx index 6ca702bf5b..d4b576e019 100644 --- a/src/renderer/components/hotbar/hotbar-rename-command.tsx +++ b/src/renderer/components/hotbar/hotbar-rename-command.tsx @@ -23,11 +23,11 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { Select } from "../select"; import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; -import { Input } from "../input"; -import { uniqueHotbarName } from "./hotbar-add-command"; +import { Input, InputValidator } from "../input"; import type { Hotbar } from "../../../common/hotbar-types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; +import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; interface Dependencies { closeCommandOverlay: () => void; @@ -37,9 +37,10 @@ interface Dependencies { setHotbarName: (id: string, name: string) => void; getDisplayLabel: (hotbar: Hotbar) => string; }; + uniqueHotbarName: InputValidator; } -const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarManager }: Dependencies) => { +const NonInjectedHotbarRenameCommand = observer(({ closeCommandOverlay, hotbarManager, uniqueHotbarName }: Dependencies) => { const [hotbarId, setHotbarId] = useState(""); const [hotbarName, setHotbarName] = useState(""); @@ -100,6 +101,7 @@ export const HotbarRenameCommand = withInjectables(NonInjectedHotb getProps: (di, props) => ({ closeCommandOverlay: di.inject(commandOverlayInjectable).close, hotbarManager: di.inject(hotbarManagerInjectable), + uniqueHotbarName: di.inject(uniqueHotbarNameInjectable), ...props, }), }); diff --git a/src/renderer/components/input/validators/unique-hotbar-name.injectable.ts b/src/renderer/components/input/validators/unique-hotbar-name.injectable.ts new file mode 100644 index 0000000000..4f3e3f5900 --- /dev/null +++ b/src/renderer/components/input/validators/unique-hotbar-name.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import hotbarManagerInjectable from "../../../../common/hotbar-store.injectable"; +import type { InputValidator } from "../input_validators"; + +const uniqueHotbarNameInjectable = getInjectable({ + instantiate: di => ({ + condition: ({ required }) => required, + message: () => "Hotbar with this name already exists", + validate: value => !di.inject(hotbarManagerInjectable).getByName(value), + } as InputValidator), + lifecycle: lifecycleEnum.singleton, +}); + +export default uniqueHotbarNameInjectable; diff --git a/src/renderer/components/layout/sidebar-cluster.tsx b/src/renderer/components/layout/sidebar-cluster.tsx index 4c3c852d1b..83a337a590 100644 --- a/src/renderer/components/layout/sidebar-cluster.tsx +++ b/src/renderer/components/layout/sidebar-cluster.tsx @@ -22,7 +22,7 @@ import styles from "./sidebar-cluster.module.scss"; import { observable } from "mobx"; import React, { useState } from "react"; -import { HotbarStore } from "../../../common/hotbar-store.injectable"; +import { HotbarStore } from "../../../common/hotbar-store"; import { broadcastMessage } from "../../../common/ipc"; import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import { IpcRendererNavigationEvents } from "../../navigation/events";