From eb756f4da43b498d79980710b6c9609168f91620 Mon Sep 17 00:00:00 2001 From: Iku-turso Date: Tue, 22 Mar 2022 15:14:39 +0200 Subject: [PATCH] Make legacy unit tests for hotbar green and more simple by using the new legacy helper Signed-off-by: Iku-turso --- src/common/__tests__/hotbar-store.test.ts | 527 ++++++++++++---------- src/common/hotbar-store.injectable.ts | 371 ++++++++++++++- src/common/hotbar-store.ts | 330 +------------- 3 files changed, 656 insertions(+), 572 deletions(-) diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index df016a3f94..e93b3a927c 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -6,14 +6,17 @@ import { anyObject } from "jest-mock-extended"; import mockFs from "mock-fs"; import logger from "../../main/logger"; -import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; +import type { + CatalogEntity, + CatalogEntityData, + CatalogEntityKindData, +} from "../catalog"; import { HotbarStore } from "../hotbar-store"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import writeFileInjectable from "../fs/write-file.injectable"; import hotbarStoreInjectable from "../hotbar-store.injectable"; -import catalogCatalogEntityInjectable - from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; +import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { @@ -21,9 +24,11 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "1dfa26e2ebab15780a3547e9c7fa785c", name: "mycluster", @@ -31,12 +36,15 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ labels: {}, }, }), + getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "55b42c3c7ba3b04193416cda405269a5", name: "my_shiny_cluster", @@ -44,12 +52,15 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ labels: {}, }, }), + getMockCatalogEntity({ apiVersion: "v1", kind: "Cluster", + status: { phase: "Running", }, + metadata: { uid: "catalog-entity", name: "Catalog", @@ -61,7 +72,9 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({ }, })); -function getMockCatalogEntity(data: Partial & CatalogEntityKindData): CatalogEntity { +function getMockCatalogEntity( + data: Partial & CatalogEntityKindData, +): CatalogEntity { return { getName: jest.fn(() => data.metadata?.name), getId: jest.fn(() => data.metadata?.uid), @@ -116,275 +129,303 @@ const awsCluster = getMockCatalogEntity({ }); describe("HotbarStore", () => { + let di: DiContainer; + beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); - di.override(writeFileInjectable, () => () => undefined); - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + // @ts-ignore + di.permitSideEffects(hotbarStoreInjectable); - di.override(hotbarStoreInjectable, (di) => - HotbarStore.createInstance({ - catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), - }), - ); + // @ts-ignore + di.permitSideEffects(getConfigurationFileModelInjectable); - mockFs({ - "some-directory-for-user-data": { - "lens-hotbar-store.json": JSON.stringify({}), - }, - }); - - await di.runSetups(); - - di.inject(hotbarStoreInjectable); + // @ts-ignore + di.permitSideEffects(appVersionInjectable); }); afterEach(() => { mockFs.restore(); - HotbarStore.resetInstance(); }); - describe("load", () => { - it("loads one hotbar by default", () => { - expect(HotbarStore.getInstance().hotbars.length).toEqual(1); + describe("given no migrations", () => { + beforeEach(async () => { + await di.runSetups(); + }); + + describe("load", () => { + it("loads one hotbar by default", () => { + expect(HotbarStore.getInstance().hotbars.length).toEqual(1); + }); + }); + + describe("add", () => { + it("adds a hotbar", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.add({ name: "hottest" }); + expect(hotbarStore.hotbars.length).toEqual(2); + }); + }); + + describe("hotbar items", () => { + it("initially creates 12 empty cells", () => { + const hotbarStore = HotbarStore.getInstance(); + + expect(hotbarStore.getActive().items.length).toEqual(12); + }); + + it("initially adds catalog entity as first item", () => { + const hotbarStore = HotbarStore.getInstance(); + + expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); + }); + + it("adds items", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items.length).toEqual(2); + }); + + it("removes items", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.removeFromHotbar("test"); + hotbarStore.removeFromHotbar("catalog-entity"); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items).toStrictEqual([]); + }); + + it("does nothing if removing with invalid uid", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.removeFromHotbar("invalid uid"); + const items = hotbarStore.getActive().items.filter(Boolean); + + expect(items.length).toEqual(2); + }); + + it("moves item to empty cell", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + expect(hotbarStore.getActive().items[6]).toBeNull(); + + hotbarStore.restackItems(1, 5); + + expect(hotbarStore.getActive().items[5]).toBeTruthy(); + expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test"); + }); + + it("moves items down", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + // aws -> catalog + hotbarStore.restackItems(3, 0); + + const items = hotbarStore + .getActive() + .items.map((item) => item?.entity.uid || null); + + expect(items.slice(0, 4)).toEqual([ + "aws", + "catalog-entity", + "test", + "minikube", + ]); + }); + + it("moves items up", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(minikubeCluster); + hotbarStore.addToHotbar(awsCluster); + + // test -> aws + hotbarStore.restackItems(1, 3); + + const items = hotbarStore + .getActive() + .items.map((item) => item?.entity.uid || null); + + expect(items.slice(0, 4)).toEqual([ + "catalog-entity", + "minikube", + "aws", + "test", + ]); + }); + + it("logs an error if cellIndex is out of bounds", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.add({ name: "hottest", id: "hottest" }); + hotbarStore.setActiveHotbar("hottest"); + + const { error } = logger; + const mocked = jest.fn(); + + logger.error = mocked; + + hotbarStore.addToHotbar(testCluster, -1); + expect(mocked).toBeCalledWith( + "[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", + anyObject(), + ); + + hotbarStore.addToHotbar(testCluster, 12); + expect(mocked).toBeCalledWith( + "[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", + anyObject(), + ); + + hotbarStore.addToHotbar(testCluster, 13); + expect(mocked).toBeCalledWith( + "[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", + anyObject(), + ); + + logger.error = error; + }); + + it("throws an error if getId is invalid or returns not a string", () => { + const hotbarStore = HotbarStore.getInstance(); + + expect(() => hotbarStore.addToHotbar({} as any)).toThrowError( + TypeError, + ); + expect(() => + hotbarStore.addToHotbar({ getId: () => true } as any), + ).toThrowError(TypeError); + }); + + it("throws an error if getName is invalid or returns not a string", () => { + const hotbarStore = HotbarStore.getInstance(); + + expect(() => + hotbarStore.addToHotbar({ getId: () => "" } as any), + ).toThrowError(TypeError); + expect(() => + hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any), + ).toThrowError(TypeError); + }); + + it("does nothing when item moved to same cell", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.restackItems(1, 1); + + expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test"); + }); + + it("new items takes first empty cell", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + hotbarStore.addToHotbar(awsCluster); + hotbarStore.restackItems(0, 3); + hotbarStore.addToHotbar(minikubeCluster); + + expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); + }); + + it("throws if invalid arguments provided", () => { + // Prevent writing to stderr during this render. + const { error, warn } = console; + + console.error = jest.fn(); + console.warn = jest.fn(); + + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + + expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); + expect(() => hotbarStore.restackItems(2, -1)).toThrow(); + expect(() => hotbarStore.restackItems(14, 1)).toThrow(); + expect(() => hotbarStore.restackItems(11, 112)).toThrow(); + + // Restore writing to stderr. + console.error = error; + console.warn = warn; + }); + + it("checks if entity already pinned to hotbar", () => { + const hotbarStore = HotbarStore.getInstance(); + + hotbarStore.addToHotbar(testCluster); + + expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); + expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); + }); }); }); - describe("add", () => { - it("adds a hotbar", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.add({ name: "hottest" }); - expect(hotbarStore.hotbars.length).toEqual(2); - }); - }); - - describe("hotbar items", () => { - it("initially creates 12 empty cells", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(hotbarStore.getActive().items.length).toEqual(12); - }); - - it("initially adds catalog entity as first item", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); - }); - - it("adds items", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items.length).toEqual(2); - }); - - it("removes items", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.removeFromHotbar("test"); - hotbarStore.removeFromHotbar("catalog-entity"); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items).toStrictEqual([]); - }); - - it("does nothing if removing with invalid uid", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.removeFromHotbar("invalid uid"); - const items = hotbarStore.getActive().items.filter(Boolean); - - expect(items.length).toEqual(2); - }); - - it("moves item to empty cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - expect(hotbarStore.getActive().items[6]).toBeNull(); - - hotbarStore.restackItems(1, 5); - - expect(hotbarStore.getActive().items[5]).toBeTruthy(); - expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test"); - }); - - it("moves items down", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - // aws -> catalog - hotbarStore.restackItems(3, 0); - - const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); - - expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]); - }); - - it("moves items up", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(minikubeCluster); - hotbarStore.addToHotbar(awsCluster); - - // test -> aws - hotbarStore.restackItems(1, 3); - - const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null); - - expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]); - }); - - it("logs an error if cellIndex is out of bounds", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.add({ name: "hottest", id: "hottest" }); - hotbarStore.setActiveHotbar("hottest"); - - const { error } = logger; - const mocked = jest.fn(); - - logger.error = mocked; - - hotbarStore.addToHotbar(testCluster, -1); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - hotbarStore.addToHotbar(testCluster, 12); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - hotbarStore.addToHotbar(testCluster, 13); - expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject()); - - logger.error = error; - }); - - it("throws an error if getId is invalid or returns not a string", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError); - expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError); - }); - - it("throws an error if getName is invalid or returns not a string", () => { - const hotbarStore = HotbarStore.getInstance(); - - expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError); - expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError); - }); - - it("does nothing when item moved to same cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.restackItems(1, 1); - - expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test"); - }); - - it("new items takes first empty cell", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - hotbarStore.addToHotbar(awsCluster); - hotbarStore.restackItems(0, 3); - hotbarStore.addToHotbar(minikubeCluster); - - expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube"); - }); - - it("throws if invalid arguments provided", () => { - // Prevent writing to stderr during this render. - const { error, warn } = console; - - console.error = jest.fn(); - console.warn = jest.fn(); - - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - - expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); - expect(() => hotbarStore.restackItems(2, -1)).toThrow(); - expect(() => hotbarStore.restackItems(14, 1)).toThrow(); - expect(() => hotbarStore.restackItems(11, 112)).toThrow(); - - // Restore writing to stderr. - console.error = error; - console.warn = warn; - }); - - it("checks if entity already pinned to hotbar", () => { - const hotbarStore = HotbarStore.getInstance(); - - hotbarStore.addToHotbar(testCluster); - - expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy(); - expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy(); - }); - }); - - describe("pre beta-5 migrations", () => { - beforeEach(() => { - const mockOpts = { - "some-directory-for-user-data": { + describe("given pre beta-5 configurations", () => { + beforeEach(async () => { + const configurationToBeMigrated = { + "some-electron-app-path-for-user-data": { "lens-hotbar-store.json": JSON.stringify({ __internal__: { migrations: { version: "5.0.0-beta.3", }, }, - "hotbars": [ + hotbars: [ { - "id": "3caac17f-aec2-4723-9694-ad204465d935", - "name": "myhotbar", - "items": [ + id: "3caac17f-aec2-4723-9694-ad204465d935", + name: "myhotbar", + items: [ { - "entity": { - "uid": "1dfa26e2ebab15780a3547e9c7fa785c", + entity: { + uid: "1dfa26e2ebab15780a3547e9c7fa785c", }, }, { - "entity": { - "uid": "55b42c3c7ba3b04193416cda405269a5", + entity: { + uid: "55b42c3c7ba3b04193416cda405269a5", }, }, { - "entity": { - "uid": "176fd331968660832f62283219d7eb6e", + entity: { + uid: "176fd331968660832f62283219d7eb6e", }, }, { - "entity": { - "uid": "61c4fb45528840ebad1badc25da41d14", - "name": "user1-context", - "source": "local", + entity: { + uid: "61c4fb45528840ebad1badc25da41d14", + name: "user1-context", + source: "local", }, }, { - "entity": { - "uid": "27d6f99fe9e7548a6e306760bfe19969", - "name": "foo2", - "source": "local", + entity: { + uid: "27d6f99fe9e7548a6e306760bfe19969", + name: "foo2", + source: "local", }, }, null, { - "entity": { - "uid": "c0b20040646849bb4dcf773e43a0bf27", - "name": "multinode-demo", - "source": "local", + entity: { + uid: "c0b20040646849bb4dcf773e43a0bf27", + name: "multinode-demo", + source: "local", }, }, null, @@ -399,15 +440,15 @@ describe("HotbarStore", () => { }, }; - mockFs(mockOpts); - }); + mockFs(configurationToBeMigrated); - afterEach(() => { - mockFs.restore(); + await di.runSetups(); }); it("allows to retrieve a hotbar", () => { - const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935"); + const hotbar = HotbarStore.getInstance().getById( + "3caac17f-aec2-4723-9694-ad204465d935", + ); expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935"); }); diff --git a/src/common/hotbar-store.injectable.ts b/src/common/hotbar-store.injectable.ts index 968abcc1e5..910c0f26f5 100644 --- a/src/common/hotbar-store.injectable.ts +++ b/src/common/hotbar-store.injectable.ts @@ -2,16 +2,379 @@ * 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 { HotbarStore } from "./hotbar-store"; import catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; +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 logger from "../main/logger"; +import { broadcastMessage } from "./ipc"; +import { + defaultHotbarCells, + getEmptyHotbar, + Hotbar, + CreateHotbarData, + CreateHotbarOptions, +} from "./hotbar-types"; +import { hotbarTooManyItemsChannel } from "./ipc/hotbar"; +import type { GeneralEntity } from "./catalog-entities"; + +export interface HotbarStoreModel { + hotbars: Hotbar[]; + activeHotbarId: string; +} + +interface Dependencies { + catalogCatalogEntity: GeneralEntity; +} + +export class HotbarStore extends BaseStore { + readonly displayName = "HotbarStore"; + @observable hotbars: Hotbar[] = []; + @observable private _activeHotbarId: string; + + constructor(private dependencies: Dependencies) { + 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 }, + } = this.dependencies.catalogCatalogEntity; + 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.getId(); + const name = item.getName(); + + if (typeof uid !== "string") { + throw new TypeError("CatalogEntity's ID must be a string"); + } + + if (typeof name !== "string") { + throw new TypeError("CatalogEntity's NAME must be a string"); + } + + if (this.isAddedToActive(item)) { + return; + } + + const entity = { + uid, + name, + source: item.metadata.source, + }; + const newItem = { entity }; + + if (cellIndex === undefined) { + // Add item to empty cell + const emptyCellIndex = hotbar.items.indexOf(null); + + if (emptyCellIndex != -1) { + hotbar.items[emptyCellIndex] = newItem; + } else { + broadcastMessage(hotbarTooManyItemsChannel); + } + } 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 the active hotbar + */ + isAddedToActive(entity: CatalogEntity | null | undefined): boolean { + if (!entity) { + return false; + } + + return ( + this.getActive().items.findIndex( + (item) => item?.entity.uid === entity.getId(), + ) >= 0 + ); + } + + 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; + } + } +} const hotbarStoreInjectable = getInjectable({ id: "hotbar-store", - instantiate: (di) => HotbarStore.createInstance({ - catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), - }), + instantiate: (di) => { + HotbarStore.resetInstance(); + + const instance = HotbarStore.createInstance({ + catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), + }); + + return instance; + }, causesSideEffects: true, }); diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 34761e945a..3458e8d460 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -3,330 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -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 logger from "../main/logger"; -import { broadcastMessage } from "./ipc"; -import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types"; -import { hotbarTooManyItemsChannel } from "./ipc/hotbar"; -import type { GeneralEntity } from "./catalog-entities"; +import hotbarStoreInjectable from "./hotbar-store.injectable"; -export interface HotbarStoreModel { - hotbars: Hotbar[]; - activeHotbarId: string; -} +import { + asLegacyGlobalSingletonForExtensionApi, +} from "../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api"; -interface Dependencies { - catalogCatalogEntity: GeneralEntity; -} - -export class HotbarStore extends BaseStore { - readonly displayName = "HotbarStore"; - @observable hotbars: Hotbar[] = []; - @observable private _activeHotbarId: string; - - constructor(private dependencies: Dependencies) { - 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 }} = this.dependencies.catalogCatalogEntity; - 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.getId(); - const name = item.getName(); - - if (typeof uid !== "string") { - throw new TypeError("CatalogEntity's ID must be a string"); - } - - if (typeof name !== "string") { - throw new TypeError("CatalogEntity's NAME must be a string"); - } - - if (this.isAddedToActive(item)) { - return; - } - - const entity = { - uid, - name, - source: item.metadata.source, - }; - const newItem = { entity }; - - if (cellIndex === undefined) { - // Add item to empty cell - const emptyCellIndex = hotbar.items.indexOf(null); - - if (emptyCellIndex != -1) { - hotbar.items[emptyCellIndex] = newItem; - } else { - broadcastMessage(hotbarTooManyItemsChannel); - } - } 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 the active hotbar - */ - isAddedToActive(entity: CatalogEntity | null | undefined): boolean { - if (!entity) { - return false; - } - - return this.getActive().items.findIndex(item => item?.entity.uid === entity.getId()) >= 0; - } - - 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; - } - } -} +export const HotbarStore = asLegacyGlobalSingletonForExtensionApi(hotbarStoreInjectable);