From 89e75653d7cc267848489b67f7ee1571da7de81f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 6 Aug 2023 13:54:57 +0200 Subject: [PATCH] Delegate tag color storage to ShlinkWebComponent consuming app --- .../src/ShlinkWebComponent.tsx | 12 ++++-- shlink-web-component/src/index.ts | 2 + .../src/utils/services/ColorGenerator.ts | 8 ++-- .../src/utils/services/TagColorsStorage.ts | 5 +++ .../src/utils/services/provideServices.ts | 5 +-- .../utils/services/ColorGenerator.test.ts | 20 ++++----- .../test/utils/services/LocalStorage.test.ts | 43 ------------------- src/common/ShlinkWebComponentContainer.tsx | 4 +- src/common/services/provideServices.ts | 1 + src/utils/services/TagColorsStorage.ts | 15 +++++++ src/utils/services/provideServices.ts | 2 + .../ShlinkWebComponentContainer.test.tsx | 1 + 12 files changed, 53 insertions(+), 65 deletions(-) create mode 100644 shlink-web-component/src/utils/services/TagColorsStorage.ts delete mode 100644 shlink-web-component/test/utils/services/LocalStorage.test.ts create mode 100644 src/utils/services/TagColorsStorage.ts diff --git a/shlink-web-component/src/ShlinkWebComponent.tsx b/shlink-web-component/src/ShlinkWebComponent.tsx index e6cf09f5..20ffebbc 100644 --- a/shlink-web-component/src/ShlinkWebComponent.tsx +++ b/shlink-web-component/src/ShlinkWebComponent.tsx @@ -7,12 +7,14 @@ import type { ShlinkApiClient } from './api-contract'; import { FeaturesProvider, useFeatures } from './utils/features'; import type { SemVer } from './utils/helpers/version'; import { RoutesPrefixProvider } from './utils/routesPrefix'; +import type { TagColorsStorage } from './utils/services/TagColorsStorage'; import type { Settings } from './utils/settings'; import { SettingsProvider } from './utils/settings'; type ShlinkWebComponentProps = { - serverVersion: SemVer; + serverVersion: SemVer; // FIXME Consider making this optional and trying to resolve it if not set apiClient: ShlinkApiClient; + tagColorsStorage?: TagColorsStorage; routesPrefix?: string; settings?: Settings; createNotFound?: (nonPrefixedHomePath: string) => ReactNode; @@ -24,7 +26,7 @@ let apiClientRef: ShlinkApiClient; export const createShlinkWebComponent = ( bottle: Bottle, -): FC => ({ serverVersion, apiClient, settings, routesPrefix = '', createNotFound }) => { +): FC => ({ serverVersion, apiClient, settings, routesPrefix = '', createNotFound, tagColorsStorage }) => { const features = useFeatures(serverVersion); const mainContent = useRef(); const [theStore, setStore] = useState(); @@ -33,6 +35,10 @@ export const createShlinkWebComponent = ( apiClientRef = apiClient; bottle.value('apiClientFactory', () => apiClientRef); + if (tagColorsStorage) { + bottle.value('TagColorsStorage', tagColorsStorage); + } + // It's important to not try to resolve services before the API client has been registered, as many other services // depend on it const { container } = bottle; @@ -42,7 +48,7 @@ export const createShlinkWebComponent = ( // Load mercure info store.dispatch(loadMercureInfo(settings)); - }, [apiClient]); + }, [apiClient, tagColorsStorage]); return !theStore ? <> : ( diff --git a/shlink-web-component/src/index.ts b/shlink-web-component/src/index.ts index 56081ea7..7f586a6c 100644 --- a/shlink-web-component/src/index.ts +++ b/shlink-web-component/src/index.ts @@ -14,3 +14,5 @@ export type { TagsSettings, Settings, } from './utils/settings'; + +export type { TagColorsStorage } from './utils/services/TagColorsStorage'; diff --git a/shlink-web-component/src/utils/services/ColorGenerator.ts b/shlink-web-component/src/utils/services/ColorGenerator.ts index 3bc8a0ac..be13f53d 100644 --- a/shlink-web-component/src/utils/services/ColorGenerator.ts +++ b/shlink-web-component/src/utils/services/ColorGenerator.ts @@ -1,6 +1,6 @@ import { isNil } from 'ramda'; import { rangeOf } from '../helpers'; -import type { LocalStorage } from './LocalStorage'; +import type { TagColorsStorage } from './TagColorsStorage'; const HEX_COLOR_LENGTH = 6; const HEX_DIGITS = '0123456789ABCDEF'; @@ -19,8 +19,8 @@ export class ColorGenerator { private readonly colors: Record; private readonly lights: Record; - public constructor(private readonly storage: LocalStorage) { - this.colors = this.storage.get>('colors') ?? {}; + public constructor(private readonly storage?: TagColorsStorage) { + this.colors = this.storage?.getTagColors() ?? {}; this.lights = {}; } @@ -40,7 +40,7 @@ export class ColorGenerator { const normalizedKey = normalizeKey(key); this.colors[normalizedKey] = color; - this.storage.set('colors', this.colors); + this.storage?.storeTagColors(this.colors); return color; }; diff --git a/shlink-web-component/src/utils/services/TagColorsStorage.ts b/shlink-web-component/src/utils/services/TagColorsStorage.ts new file mode 100644 index 00000000..114093b1 --- /dev/null +++ b/shlink-web-component/src/utils/services/TagColorsStorage.ts @@ -0,0 +1,5 @@ +export type TagColorsStorage = { + getTagColors(): Record; + + storeTagColors(colors: Record): void; +}; diff --git a/shlink-web-component/src/utils/services/provideServices.ts b/shlink-web-component/src/utils/services/provideServices.ts index dc35a3dc..cdf6f7db 100644 --- a/shlink-web-component/src/utils/services/provideServices.ts +++ b/shlink-web-component/src/utils/services/provideServices.ts @@ -3,7 +3,6 @@ import { useTimeoutToggle } from '../helpers/hooks'; import { jsonToCsv } from '../helpers/json'; import { ColorGenerator } from './ColorGenerator'; import { ImageDownloader } from './ImageDownloader'; -import { LocalStorage } from './LocalStorage'; import { ReportExporter } from './ReportExporter'; export function provideServices(bottle: Bottle) { @@ -11,9 +10,7 @@ export function provideServices(bottle: Bottle) { bottle.constant('fetch', window.fetch.bind(window)); bottle.service('ImageDownloader', ImageDownloader, 'fetch', 'window'); - bottle.constant('localStorage', window.localStorage); - bottle.service('Storage', LocalStorage, 'localStorage'); - bottle.service('ColorGenerator', ColorGenerator, 'Storage'); + bottle.service('ColorGenerator', ColorGenerator, 'TagColorsStorage'); bottle.constant('jsonToCsv', jsonToCsv); bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); diff --git a/shlink-web-component/test/utils/services/ColorGenerator.test.ts b/shlink-web-component/test/utils/services/ColorGenerator.test.ts index 324aada5..33c799e0 100644 --- a/shlink-web-component/test/utils/services/ColorGenerator.test.ts +++ b/shlink-web-component/test/utils/services/ColorGenerator.test.ts @@ -1,13 +1,13 @@ import { MAIN_COLOR } from '@shlinkio/shlink-frontend-kit'; import { fromPartial } from '@total-typescript/shoehorn'; import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; -import type { LocalStorage } from '../../../src/utils/services/LocalStorage'; +import type { TagColorsStorage } from '../../../src/utils/services/TagColorsStorage'; describe('ColorGenerator', () => { let colorGenerator: ColorGenerator; - const storageMock = fromPartial({ - set: vi.fn(), - get: vi.fn().mockImplementation(() => undefined), + const storageMock = fromPartial({ + storeTagColors: vi.fn(), + getTagColors: vi.fn().mockImplementation(() => ({})), }); beforeEach(() => { @@ -20,14 +20,14 @@ describe('ColorGenerator', () => { colorGenerator.setColorForKey('foo', color); expect(colorGenerator.getColorForKey('foo')).toEqual(color); - expect(storageMock.set).toHaveBeenCalledTimes(1); - expect(storageMock.get).toHaveBeenCalledTimes(1); + expect(storageMock.storeTagColors).toHaveBeenCalledTimes(1); + expect(storageMock.getTagColors).toHaveBeenCalledTimes(1); }); it('generates a random color when none is available for requested key', () => { expect(colorGenerator.getColorForKey('bar')).toEqual(expect.stringMatching(/^#(?:[0-9a-fA-F]{6})$/)); - expect(storageMock.set).toHaveBeenCalledTimes(1); - expect(storageMock.get).toHaveBeenCalledTimes(1); + expect(storageMock.storeTagColors).toHaveBeenCalledTimes(1); + expect(storageMock.getTagColors).toHaveBeenCalledTimes(1); }); it('trims and lower cases keys before trying to match', () => { @@ -41,8 +41,8 @@ describe('ColorGenerator', () => { expect(colorGenerator.getColorForKey('FOO')).toEqual(color); expect(colorGenerator.getColorForKey('FOO ')).toEqual(color); expect(colorGenerator.getColorForKey(' FoO ')).toEqual(color); - expect(storageMock.set).toHaveBeenCalledTimes(1); - expect(storageMock.get).toHaveBeenCalledTimes(1); + expect(storageMock.storeTagColors).toHaveBeenCalledTimes(1); + expect(storageMock.getTagColors).toHaveBeenCalledTimes(1); }); describe('isColorLightForKey', () => { diff --git a/shlink-web-component/test/utils/services/LocalStorage.test.ts b/shlink-web-component/test/utils/services/LocalStorage.test.ts deleted file mode 100644 index c3e44066..00000000 --- a/shlink-web-component/test/utils/services/LocalStorage.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { fromPartial } from '@total-typescript/shoehorn'; -import { LocalStorage } from '../../../src/utils/services/LocalStorage'; - -describe('LocalStorage', () => { - const getItem = vi.fn((key) => (key === 'shlink.foo' ? JSON.stringify({ foo: 'bar' }) : null)); - const setItem = vi.fn(); - const localStorageMock = fromPartial({ getItem, setItem }); - let storage: LocalStorage; - - beforeEach(() => { - storage = new LocalStorage(localStorageMock); - }); - - describe('set', () => { - it('writes an stringified representation of provided value in local storage', () => { - const value = { bar: 'baz' }; - - storage.set('foo', value); - - expect(setItem).toHaveBeenCalledTimes(1); - expect(setItem).toHaveBeenCalledWith('shlink.foo', JSON.stringify(value)); - }); - }); - - describe('get', () => { - it('fetches item from local storage', () => { - storage.get('foo'); - expect(getItem).toHaveBeenCalledTimes(1); - }); - - it('returns parsed value when requested value is found in local storage', () => { - const value = storage.get('foo'); - - expect(value).toEqual({ foo: 'bar' }); - }); - - it('returns undefined when requested value is not found in local storage', () => { - const value = storage.get('bar'); - - expect(value).toBeUndefined(); - }); - }); -}); diff --git a/src/common/ShlinkWebComponentContainer.tsx b/src/common/ShlinkWebComponentContainer.tsx index c77d58e3..7c444b5c 100644 --- a/src/common/ShlinkWebComponentContainer.tsx +++ b/src/common/ShlinkWebComponentContainer.tsx @@ -1,4 +1,4 @@ -import type { Settings, ShlinkWebComponentType } from '@shlinkio/shlink-web-component'; +import type { Settings, ShlinkWebComponentType, TagColorsStorage } from '@shlinkio/shlink-web-component'; import type { FC } from 'react'; import { useEffect } from 'react'; import type { ShlinkApiClientBuilder } from '../api/services/ShlinkApiClientBuilder'; @@ -15,6 +15,7 @@ interface ShlinkWebComponentContainerProps { export const ShlinkWebComponentContainer = ( buildShlinkApiClient: ShlinkApiClientBuilder, + tagColorsStorage: TagColorsStorage, ShlinkWebComponent: ShlinkWebComponentType, ServerError: FC, ) => withSelectedServer(( @@ -38,6 +39,7 @@ export const ShlinkWebComponentContainer = ( apiClient={buildShlinkApiClient(selectedServer)} settings={settings} routesPrefix={routesPrefix} + tagColorsStorage={tagColorsStorage} createNotFound={(nonPrefixedHomePath) => ( List short URLs )} diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 56280d70..a1c38e2b 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -32,6 +32,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 'ShlinkWebComponentContainer', ShlinkWebComponentContainer, 'buildShlinkApiClient', + 'TagColorsStorage', 'ShlinkWebComponent', 'ServerError', ); diff --git a/src/utils/services/TagColorsStorage.ts b/src/utils/services/TagColorsStorage.ts new file mode 100644 index 00000000..38d2c75f --- /dev/null +++ b/src/utils/services/TagColorsStorage.ts @@ -0,0 +1,15 @@ +import type { TagColorsStorage as BaseTagColorsStorage } from '@shlinkio/shlink-web-component'; +import type { LocalStorage } from './LocalStorage'; + +export class TagColorsStorage implements BaseTagColorsStorage { + constructor(private readonly storage: LocalStorage) { + } + + getTagColors(): Record { + return this.storage.get>('colors') ?? {}; + } + + storeTagColors(colors: Record): void { + this.storage.set('colors', colors); + } +} diff --git a/src/utils/services/provideServices.ts b/src/utils/services/provideServices.ts index 43626843..c98e517a 100644 --- a/src/utils/services/provideServices.ts +++ b/src/utils/services/provideServices.ts @@ -2,10 +2,12 @@ import type Bottle from 'bottlejs'; import { csvToJson, jsonToCsv } from '../helpers/csvjson'; import { useTimeoutToggle } from '../helpers/hooks'; import { LocalStorage } from './LocalStorage'; +import { TagColorsStorage } from './TagColorsStorage'; export const provideServices = (bottle: Bottle) => { bottle.constant('localStorage', window.localStorage); bottle.service('Storage', LocalStorage, 'localStorage'); + bottle.service('TagColorsStorage', TagColorsStorage, 'Storage'); bottle.constant('csvToJson', csvToJson); bottle.constant('jsonToCsv', jsonToCsv); diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index 8def0346..4d804932 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -12,6 +12,7 @@ vi.mock('react-router-dom', async () => ({ describe('', () => { const ShlinkWebComponentContainer = createContainer( vi.fn().mockReturnValue(fromPartial({})), + fromPartial({}), () => <>ShlinkWebComponent, () => <>ServerError, );