Skip to content

Commit

Permalink
feat(workspace): add hook to scope local and session storage by org a…
Browse files Browse the repository at this point in the history
…nd app (#13937)
  • Loading branch information
framitdavid authored Oct 29, 2024
1 parent d76a686 commit 7e13f6b
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 1 deletion.
3 changes: 3 additions & 0 deletions frontend/libs/studio-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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<UseOrgAppScopedStorage['storage']> = ['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 };
};
36 changes: 36 additions & 0 deletions frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts
Original file line number Diff line number Diff line change
@@ -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<UseOrgAppScopedStorage['storage'], ScopedStorage> = {
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<OrgAppParams>();

const storageKey: string = `${org}-${app}`;
const scopedStorage = new ScopedStorageImpl(supportedStorageMap[storage], storageKey);

return {
setItem: scopedStorage.setItem,
getItem: scopedStorage.getItem,
removeItem: scopedStorage.removeItem,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ type StorageKey = string;

export interface ScopedStorage extends Pick<Storage, 'setItem' | 'getItem' | 'removeItem'> {}

export interface ScopedStorageResult extends ScopedStorage {
setItem: <T>(key: string, value: T) => void;
getItem: <T>(key: string) => T;
removeItem: (key: string) => void;
}

export class ScopedStorageImpl implements ScopedStorage {
private readonly storageKey: StorageKey;
private readonly scopedStorage: ScopedStorage;
Expand All @@ -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<T>(key: string, value: T): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { ScopedStorageImpl, type ScopedStorage } from './ScopedStorage';
export { ScopedStorageImpl, type ScopedStorage, type ScopedStorageResult } from './ScopedStorage';
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 7e13f6b

Please sign in to comment.