From acefc037c1d8cfcb2a947af10baeef8a6219761c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 24 Jan 2023 10:25:38 -0500 Subject: [PATCH] Move HotbarStore to new format Signed-off-by: Sebastian Malton --- .../core/src/common/base-store/base-store.ts | 2 +- packages/core/src/common/catalog/helpers.ts | 83 ++++ .../src/common/hotbars/store.injectable.ts | 18 +- packages/core/src/common/hotbars/store.ts | 420 ++++++++++-------- .../src/renderer/components/avatar/avatar.tsx | 40 +- .../components/hotbar/hotbar-selector.tsx | 2 +- .../utility-features/utilities/src/iter.ts | 2 + 7 files changed, 319 insertions(+), 248 deletions(-) create mode 100644 packages/core/src/common/catalog/helpers.ts diff --git a/packages/core/src/common/base-store/base-store.ts b/packages/core/src/common/base-store/base-store.ts index 8d31e7dc6e..360f284108 100644 --- a/packages/core/src/common/base-store/base-store.ts +++ b/packages/core/src/common/base-store/base-store.ts @@ -35,7 +35,7 @@ export interface BaseStoreParams extends Omit, "migrations"> { * * @param data the parsed information read from the stored JSON file */ - fromStore(data: T): void; + fromStore(data: Partial): void; /** * toJSON is called when syncing the store to the filesystem. It should diff --git a/packages/core/src/common/catalog/helpers.ts b/packages/core/src/common/catalog/helpers.ts new file mode 100644 index 0000000000..75cdf726aa --- /dev/null +++ b/packages/core/src/common/catalog/helpers.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { CatalogEntity } from "./catalog-entity"; +import GraphemeSplitter from "grapheme-splitter"; +import { hasOwnProperty, hasTypedProperty, isObject, isString, iter } from "@k8slens/utilities"; + +function getNameParts(name: string): string[] { + const byWhitespace = name.split(/\s+/); + + if (byWhitespace.length > 1) { + return byWhitespace; + } + + const byDashes = name.split(/[-_]+/); + + if (byDashes.length > 1) { + return byDashes; + } + + return name.split(/@+/); +} + +export function limitGraphemeLengthOf(src: string, count: number): string { + const splitter = new GraphemeSplitter(); + + return iter + .chain(splitter.iterateGraphemes(src)) + .take(count) + .join(""); +} + +export function computeDefaultShortName(name: string) { + if (!name || typeof name !== "string") { + return "??"; + } + + const [rawFirst, rawSecond, rawThird] = getNameParts(name); + const splitter = new GraphemeSplitter(); + const first = splitter.iterateGraphemes(rawFirst); + const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first; + const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty(); + + return iter.chain(iter.take(first, 1)) + .concat(iter.take(second, 1)) + .concat(iter.take(third, 1)) + .join(""); +} + +export function getShortName(entity: CatalogEntity): string { + return entity.metadata.shortName || computeDefaultShortName(entity.getName()); +} + +export function getIconColourHash(entity: CatalogEntity): string { + return `${entity.metadata.name}-${entity.metadata.source}`; +} + +export function getIconBackground(entity: CatalogEntity): string | undefined { + if (isObject(entity.spec.icon)) { + if (hasTypedProperty(entity.spec.icon, "background", isString)) { + return entity.spec.icon.background; + } + + return hasOwnProperty(entity.spec.icon, "src") + ? "transparent" + : undefined; + } + + return undefined; +} + +export function getIconMaterial(entity: CatalogEntity): string | undefined { + if ( + isObject(entity.spec.icon) + && hasTypedProperty(entity.spec.icon, "material", isString) + ) { + return entity.spec.icon.material; + } + + return undefined; +} diff --git a/packages/core/src/common/hotbars/store.injectable.ts b/packages/core/src/common/hotbars/store.injectable.ts index ea25840e11..69e565ba68 100644 --- a/packages/core/src/common/hotbars/store.injectable.ts +++ b/packages/core/src/common/hotbars/store.injectable.ts @@ -6,16 +6,10 @@ import { getInjectable } from "@ogre-tools/injectable"; import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import { HotbarStore } from "./store"; import loggerInjectable from "../logger.injectable"; -import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; -import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import storeMigrationsInjectable from "../base-store/migrations.injectable"; import { hotbarStoreMigrationInjectionToken } from "./migrations-token"; -import getBasenameOfPathInjectable from "../path/get-basename.injectable"; -import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix"; -import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; -import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging"; -import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; +import createBaseStoreInjectable from "../base-store/create-base-store.injectable"; +import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; const hotbarStoreInjectable = getInjectable({ id: "hotbar-store", @@ -23,15 +17,9 @@ const hotbarStoreInjectable = getInjectable({ instantiate: (di) => new HotbarStore({ catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), logger: di.inject(loggerInjectable), - directoryForUserData: di.inject(directoryForUserDataInjectable), - getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable), migrations: di.inject(storeMigrationsInjectable, hotbarStoreMigrationInjectionToken), - getBasenameOfPath: di.inject(getBasenameOfPathInjectable), - ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), - persistStateToConfig: di.inject(persistStateToConfigInjectionToken), - enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), - shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + createBaseStore: di.inject(createBaseStoreInjectable), }), }); diff --git a/packages/core/src/common/hotbars/store.ts b/packages/core/src/common/hotbars/store.ts index 709242f20e..e0ff3d3206 100644 --- a/packages/core/src/common/hotbars/store.ts +++ b/packages/core/src/common/hotbars/store.ts @@ -3,10 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, comparer, observable, makeObservable, computed } from "mobx"; -import type { BaseStoreDependencies } from "../base-store/base-store"; -import { BaseStore } from "../base-store/base-store"; -import { toJS } from "../utils"; +import type { IObservableValue } from "mobx"; +import { runInAction, action, comparer, observable } from "mobx"; +import type { BaseStore } from "../base-store/base-store"; import type { CatalogEntity } from "../catalog"; import { broadcastMessage } from "../ipc"; import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types"; @@ -15,34 +14,89 @@ import { hotbarTooManyItemsChannel } from "../ipc/hotbar"; import type { GeneralEntity } from "../catalog-entities"; import type { Logger } from "../logger"; import assert from "assert"; +import { getShortName } from "../catalog/helpers"; +import type { Migrations } from "conf/dist/source/types"; +import type { CreateBaseStore } from "../base-store/create-base-store.injectable"; export interface HotbarStoreModel { hotbars: Hotbar[]; activeHotbarId: string; } -interface Dependencies extends BaseStoreDependencies { +interface Dependencies { readonly catalogCatalogEntity: GeneralEntity; readonly logger: Logger; + readonly storeMigrationVersion: string; + readonly migrations: Migrations>; + createBaseStore: CreateBaseStore; } -export class HotbarStore extends BaseStore { - @observable hotbars: Hotbar[] = []; - @observable private _activeHotbarId!: string; +export class HotbarStore { + private readonly store: BaseStore; + + readonly hotbars = observable.array(); + + readonly activeHotbarId = observable.box() as IObservableValue; constructor(protected readonly dependencies: Dependencies) { - super(dependencies, { + this.store = this.dependencies.createBaseStore({ configName: "lens-hotbar-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names syncOptions: { equals: comparer.structural, }, + projectVersion: this.dependencies.storeMigrationVersion, + migrations: this.dependencies.migrations, + fromStore: action((data) => { + if (!data.hotbars || !data.hotbars.length) { + const hotbar = getEmptyHotbar("Default"); + const { + metadata: { + uid, + name, + source, + }, + } = this.dependencies.catalogCatalogEntity; + + hotbar.items[0] = { + entity: { + uid, + name, + source, + }, + }; + this.hotbars.replace([hotbar]); + } else { + this.hotbars.replace(data.hotbars); + } + + for (const hotbar of this.hotbars) { + ensureExactHotbarItemLength(hotbar); + } + + if (data.activeHotbarId) { + this.activeHotbarId.set(data.activeHotbarId); + } + + if (!this.activeHotbarId.get()) { + this.activeHotbarId.set(this.hotbars[0].id); + } + + const activeHotbarExists = this.hotbars.findIndex(hotbar => hotbar.id === this.activeHotbarId.get()) >= 0; + + if (!activeHotbarExists) { + this.activeHotbarId.set(this.hotbars[0].id); + } + }), + toJSON: () => ({ + hotbars: this.hotbars.toJSON(), + activeHotbarId: this.activeHotbarId.get(), + }), }); - makeObservable(this); } - @computed get activeHotbarId() { - return this._activeHotbarId; + load() { + this.store.load(); } /** @@ -50,69 +104,29 @@ export class HotbarStore extends BaseStore { * @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; + runInAction(() => { + if (typeof hotbar === "number") { + if (hotbar >= 0 && hotbar < this.hotbars.length) { + this.activeHotbarId.set(this.hotbars[hotbar].id); + } + } else if (typeof hotbar === "string") { + if (this.findById(hotbar)) { + this.activeHotbarId.set(hotbar); + } + } else { + if (this.hotbars.indexOf(hotbar) >= 0) { + this.activeHotbarId.set(hotbar.id); + } } - } else if (typeof hotbar === "string") { - if (this.findById(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._activeHotbarId = data.activeHotbarId; - } - - if (!this._activeHotbarId) { - this._activeHotbarId = this.hotbars[0].id; - } - } - - toJSON(): HotbarStoreModel { - return toJS({ - hotbars: this.hotbars, - activeHotbarId: this.activeHotbarId, }); } + private getActiveHotbarIndex() { + return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId.get()); + } + getActive(): Hotbar { - const hotbar = this.findById(this.activeHotbarId); + const hotbar = this.findById(this.activeHotbarId.get()); assert(hotbar, "There MUST always be an active hotbar"); @@ -127,96 +141,108 @@ export class HotbarStore extends BaseStore { return this.hotbars.find((hotbar) => hotbar.id === id); } - @action add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) { - const hotbar = getEmptyHotbar(data.name, data.id); + runInAction(() => { + const hotbar = getEmptyHotbar(data.name, data.id); - this.hotbars.push(hotbar); + this.hotbars.push(hotbar); - if (setActive) { - this._activeHotbarId = hotbar.id; - } - } - - @action - setHotbarName(id: string, name: string): void { - const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); - - if (index < 0) { - return this.dependencies.logger.warn( - `[HOTBAR-STORE]: cannot setHotbarName: unknown id`, - { id }, - ); - } - - this.hotbars[index].name = name; - } - - @action - remove(hotbar: Hotbar) { - assert(this.hotbars.length >= 2, "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); + if (setActive) { + this.activeHotbarId.set(hotbar.id); } - } else if (0 <= cellIndex && cellIndex < hotbar.items.length) { - hotbar.items[cellIndex] = newItem; - } else { - this.dependencies.logger.error( - `[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`, - { entityId: uid, hotbarId: hotbar.id, cellIndex }, - ); - } + }); + } + + setHotbarName(id: string, name: string): void { + runInAction(() => { + const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); + + if (index < 0) { + return this.dependencies.logger.warn( + `[HOTBAR-STORE]: cannot setHotbarName: unknown id`, + { id }, + ); + } + + this.hotbars[index].name = name; + }); + } + + remove(hotbar: Hotbar) { + runInAction(() => { + assert(this.hotbars.length >= 2, "Cannot remove the last hotbar"); + + this.hotbars.replace(this.hotbars.filter((h) => h.id !== hotbar.id)); + + if (this.activeHotbarId.get() === hotbar.id) { + this.activeHotbarId.set(this.hotbars[0].id); + } + }); + } + + addToHotbar(item: CatalogEntity, cellIndex?: number) { + runInAction(() => { + + const hotbar = this.getActive(); + const uid = item.getId(); + const name = item.getName(); + const shortName = getShortName(item); + + 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 (typeof shortName !== "string") { + throw new TypeError("CatalogEntity's SHORT_NAME must be a string"); + } + + if (this.isAddedToActive(item)) { + return; + } + + const entity = { + uid, + name, + source: item.metadata.source, + shortName, + }; + 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 { + this.dependencies.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); + runInAction(() => { + const hotbar = this.getActive(); + const index = hotbar.items.findIndex((item) => item?.entity.uid === uid); - if (index < 0) { - return; - } + if (index < 0) { + return; + } - hotbar.items[index] = null; + hotbar.items[index] = null; + }); } /** @@ -224,18 +250,19 @@ export class HotbarStore extends BaseStore { * @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); + runInAction(() => { + for (const hotbar of this.hotbars) { + const index = hotbar.items.findIndex((i) => i?.entity.uid === uid); - if (index >= 0) { - hotbar.items[index] = null; + if (index >= 0) { + hotbar.items[index] = null; + } } - } + }); } - findClosestEmptyIndex(from: number, direction = 1) { + private findClosestEmptyIndex(from: number, direction = 1) { let index = from; const hotbar = this.getActive(); @@ -246,56 +273,61 @@ export class HotbarStore extends BaseStore { return index; } - @action restackItems(from: number, to: number): void { - const { items } = this.getActive(); - const source = items[from]; - const moveDown = from < to; + runInAction(() => { + 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 < 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; - } + if (from == to) { + return; + } - items.splice(from, 1, null); + 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); - } + 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() { - let index = this.activeHotbarIndex - 1; + runInAction(() => { + let index = this.getActiveHotbarIndex() - 1; - if (index < 0) { - index = this.hotbars.length - 1; - } + if (index < 0) { + index = this.hotbars.length - 1; + } - this.setActiveHotbar(index); + this.setActiveHotbar(index); + }); } switchToNext() { - let index = this.activeHotbarIndex + 1; + runInAction(() => { + let index = this.getActiveHotbarIndex() + 1; - if (index >= this.hotbars.length) { - index = 0; - } + if (index >= this.hotbars.length) { + index = 0; + } - this.setActiveHotbar(index); + this.setActiveHotbar(index); + }); } /** @@ -316,7 +348,7 @@ export class HotbarStore extends BaseStore { } getDisplayIndex(hotbar: Hotbar): string { - const index = this.hotbarIndex(hotbar); + const index = this.hotbars.indexOf(hotbar); if (index < 0) { return "??"; diff --git a/packages/core/src/renderer/components/avatar/avatar.tsx b/packages/core/src/renderer/components/avatar/avatar.tsx index 84892b081c..991b7e761b 100644 --- a/packages/core/src/renderer/components/avatar/avatar.tsx +++ b/packages/core/src/renderer/components/avatar/avatar.tsx @@ -8,9 +8,9 @@ import styles from "./avatar.module.scss"; import type { ImgHTMLAttributes, MouseEventHandler } from "react"; import React from "react"; import randomColor from "randomcolor"; -import GraphemeSplitter from "grapheme-splitter"; import type { SingleOrMany } from "@k8slens/utilities"; -import { cssNames, isDefined, iter } from "@k8slens/utilities"; +import { cssNames } from "@k8slens/utilities"; +import { computeDefaultShortName } from "../../../common/catalog/helpers"; export interface AvatarProps { title: string; @@ -28,40 +28,6 @@ export interface AvatarProps { "data-testid"?: string; } -function getNameParts(name: string): string[] { - const byWhitespace = name.split(/\s+/); - - if (byWhitespace.length > 1) { - return byWhitespace; - } - - const byDashes = name.split(/[-_]+/); - - if (byDashes.length > 1) { - return byDashes; - } - - return name.split(/@+/); -} - -function getLabelFromTitle(title: string) { - if (!title) { - return "??"; - } - - const [rawFirst, rawSecond, rawThird] = getNameParts(title); - const splitter = new GraphemeSplitter(); - const first = splitter.iterateGraphemes(rawFirst); - const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first; - const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty(); - - return [ - ...iter.take(first, 1), - ...iter.take(second, 1), - ...iter.take(third, 1), - ].filter(isDefined).join(""); -} - export const Avatar = ({ title, variant = "rounded", @@ -104,6 +70,6 @@ export const Avatar = ({ alt={title} /> ) - : children || getLabelFromTitle(title)} + : children || computeDefaultShortName(title)} ); diff --git a/packages/core/src/renderer/components/hotbar/hotbar-selector.tsx b/packages/core/src/renderer/components/hotbar/hotbar-selector.tsx index 7e7b609300..f10d989953 100644 --- a/packages/core/src/renderer/components/hotbar/hotbar-selector.tsx +++ b/packages/core/src/renderer/components/hotbar/hotbar-selector.tsx @@ -59,7 +59,7 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
extends Iterable { flatMap(fn: (val: T) => U[]): Iterator; concat(src2: IterableIterator): Iterator; join(sep?: string): string; + take(count: number): Iterator; } function chain(src: IterableIterator): Iterator { @@ -26,6 +27,7 @@ function chain(src: IterableIterator): Iterator { join: (sep) => join(src, sep), collect: (fn) => fn(src), concat: (src2) => chain(concat(src, src2)), + take: (count) => chain(take(src, count)), [Symbol.iterator]: () => src, }; }