From 7e13f6b748c0986559f602b340eb7e697748c9b7 Mon Sep 17 00:00:00 2001 From: David Ovrelid <46874830+framitdavid@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:46:16 +0100 Subject: [PATCH] feat(workspace): add hook to scope local and session storage by org and app (#13937) --- frontend/libs/studio-hooks/package.json | 3 + .../src/hooks/useOrgAppScopedStorage.test.tsx | 69 +++++++++++++++++++ .../src/hooks/useOrgAppScopedStorage.ts | 36 ++++++++++ .../src/ScopedStorage/ScopedStorage.ts | 9 +++ .../src/ScopedStorage/index.ts | 2 +- yarn.lock | 2 + 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx create mode 100644 frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts diff --git a/frontend/libs/studio-hooks/package.json b/frontend/libs/studio-hooks/package.json index f4ff62184e8..46fbd7aad54 100644 --- a/frontend/libs/studio-hooks/package.json +++ b/frontend/libs/studio-hooks/package.json @@ -22,5 +22,8 @@ "jest-environment-jsdom": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "5.6.2" + }, + "peerDependencies": { + "react-router-dom": ">=6.0.0" } } diff --git a/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx new file mode 100644 index 00000000000..760240946b5 --- /dev/null +++ b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx @@ -0,0 +1,69 @@ +import { renderHook } from '@testing-library/react'; +import { type UseOrgAppScopedStorage, useOrgAppScopedStorage } from './useOrgAppScopedStorage'; +import { useParams } from 'react-router-dom'; + +jest.mock('react-router-dom', () => ({ + useParams: jest.fn(), +})); + +const mockedOrg: string = 'testOrg'; +const mockedApp: string = 'testApp'; +const scopedStorageKey: string = 'testOrg-testApp'; +const storagesToTest: Array = ['localStorage', 'sessionStorage']; + +describe('useOrgAppScopedStorage', () => { + afterEach(() => { + window.localStorage.clear(); + window.sessionStorage.clear(); + }); + + it.each(storagesToTest)( + 'initializes ScopedStorageImpl with correct storage scope, %s', + (storage) => { + const { result } = renderUseOrgAppScopedStorage({ storage }); + + result.current.setItem('key', 'value'); + + expect(result.current.setItem).toBeDefined(); + expect(result.current.getItem).toBeDefined(); + expect(result.current.removeItem).toBeDefined(); + expect(window[storage].getItem(scopedStorageKey)).toBe('{"key":"value"}'); + }, + ); + + it.each(storagesToTest)('should retrieve parsed objects from %s', (storage) => { + const { result } = renderUseOrgAppScopedStorage({ storage }); + + result.current.setItem('person', { name: 'John', age: 18 }); + + expect(result.current.getItem('person')).toEqual({ + name: 'John', + age: 18, + }); + }); + + it.each(storagesToTest)('should be possible to remove item from %s', (storage) => { + const { result } = renderUseOrgAppScopedStorage({ storage }); + + result.current.setItem('key', 'value'); + result.current.removeItem('key'); + expect(result.current.getItem('key')).toBeUndefined(); + }); + + it('should use localStorage as default storage', () => { + const { result } = renderUseOrgAppScopedStorage({}); + result.current.setItem('key', 'value'); + + expect(window.localStorage.getItem(scopedStorageKey)).toBe('{"key":"value"}'); + }); +}); + +const renderUseOrgAppScopedStorage = ({ storage }: UseOrgAppScopedStorage) => { + (useParams as jest.Mock).mockReturnValue({ org: mockedOrg, app: mockedApp }); + const { result } = renderHook(() => + useOrgAppScopedStorage({ + storage, + }), + ); + return { result }; +}; diff --git a/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts new file mode 100644 index 00000000000..b9582cdba59 --- /dev/null +++ b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts @@ -0,0 +1,36 @@ +import { useParams } from 'react-router-dom'; +import { + type ScopedStorage, + type ScopedStorageResult, + ScopedStorageImpl, +} from '@studio/pure-functions'; + +type OrgAppParams = { + org: string; + app: string; +}; + +const supportedStorageMap: Record = { + localStorage: window.localStorage, + sessionStorage: window.sessionStorage, +}; + +export type UseOrgAppScopedStorage = { + storage?: 'localStorage' | 'sessionStorage'; +}; + +type UseOrgAppScopedStorageResult = ScopedStorageResult; +export const useOrgAppScopedStorage = ({ + storage = 'localStorage', +}: UseOrgAppScopedStorage): UseOrgAppScopedStorageResult => { + const { org, app } = useParams(); + + const storageKey: string = `${org}-${app}`; + const scopedStorage = new ScopedStorageImpl(supportedStorageMap[storage], storageKey); + + return { + setItem: scopedStorage.setItem, + getItem: scopedStorage.getItem, + removeItem: scopedStorage.removeItem, + }; +}; diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts index ffbaaea8996..0fc48637a6f 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts @@ -2,6 +2,12 @@ type StorageKey = string; export interface ScopedStorage extends Pick {} +export interface ScopedStorageResult extends ScopedStorage { + setItem: (key: string, value: T) => void; + getItem: (key: string) => T; + removeItem: (key: string) => void; +} + export class ScopedStorageImpl implements ScopedStorage { private readonly storageKey: StorageKey; private readonly scopedStorage: ScopedStorage; @@ -12,6 +18,9 @@ export class ScopedStorageImpl implements ScopedStorage { ) { this.storageKey = this.key; this.scopedStorage = this.storage; + this.setItem = this.setItem.bind(this); + this.getItem = this.getItem.bind(this); + this.removeItem = this.removeItem.bind(this); } public setItem(key: string, value: T): void { diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts index 999823d8a7a..0b4b3d5747c 100644 --- a/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts +++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts @@ -1 +1 @@ -export { ScopedStorageImpl, type ScopedStorage } from './ScopedStorage'; +export { ScopedStorageImpl, type ScopedStorage, type ScopedStorageResult } from './ScopedStorage'; diff --git a/yarn.lock b/yarn.lock index 8c089c4c25b..44a03132739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4390,6 +4390,8 @@ __metadata: ts-jest: "npm:^29.1.1" typescript: "npm:5.6.2" uuid: "npm:10.0.0" + peerDependencies: + react-router-dom: ">=6.0.0" languageName: unknown linkType: soft