Skip to content
Snippets Groups Projects
Unverified Commit ef4dd69a authored by Nemanja Glumac's avatar Nemanja Glumac Committed by GitHub
Browse files

Add `use-temp-storage` hook (#48987)

* Add `use-temp-storage` hook

* Remove try/catch

* Add reducer

* Add tests

* Use RTK to create a new slice and reducer
parent 23338803
No related branches found
No related tags found
No related merge requests found
......@@ -14,9 +14,23 @@ export interface AppBreadCrumbs {
show: boolean;
}
/**
* Storage for non-critical, ephemeral user preferences.
* Think of it as a sessionStorage alternative implemented in Redux.
* Only specific key/value pairs can be stored here,
* and then later used with the `use-temp-storage` hook.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type TempStorage = {};
export type TempStorageKey = keyof TempStorage;
export type TempStorageValue<Key extends TempStorageKey = TempStorageKey> =
TempStorage[Key];
export interface AppState {
errorPage: AppErrorDescriptor | null;
isNavbarOpen: boolean;
isDndAvailable: boolean;
isErrorDiagnosticsOpen: boolean;
tempStorage: TempStorage;
}
......@@ -5,5 +5,6 @@ export const createMockAppState = (opts?: Partial<AppState>): AppState => ({
errorPage: null,
isDndAvailable: false,
isErrorDiagnosticsOpen: false,
tempStorage: {},
...opts,
});
export * from "./use-temp-storage";
import { useCallback } from "react";
import { useDispatch, useSelector } from "metabase/lib/redux";
import { setTempSetting } from "metabase/redux/app";
import type {
State,
TempStorageKey,
TempStorageValue,
} from "metabase-types/store";
export const useTempStorage = <Key extends TempStorageKey>(
key: Key,
): [TempStorageValue<Key>, (newValue: TempStorageValue<Key>) => void] => {
const dispatch = useDispatch();
const value = useSelector((state: State) => state.app.tempStorage[key]);
const setValue = useCallback(
(newValue: TempStorageValue<Key>) => {
dispatch(setTempSetting({ key, value: newValue }));
},
[dispatch, key],
);
return [value, setValue];
};
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "__support__/ui";
import type {
TempStorage,
TempStorageKey,
TempStorageValue,
} from "metabase-types/store";
import {
createMockAppState,
createMockState,
} from "metabase-types/store/mocks";
import { useTempStorage } from "./use-temp-storage";
const TestComponent = ({
entry,
newValue,
}: {
entry: TempStorageKey;
newValue?: TempStorageValue;
}) => {
const [value, setValue] = useTempStorage(entry);
return (
<div>
{/* @ts-expect-error - The hook still doesn't accept any k/v pair */}
<button onClick={() => setValue(newValue)} />
<div data-testid="result">{`Value is: ${value}`}</div>
</div>
);
};
type SetupProps = {
tempStorage: TempStorage;
entry: TempStorageKey;
newValue?: TempStorageValue;
};
const setup = ({ tempStorage = {}, entry, newValue }: SetupProps) => {
const initialState = createMockState({
app: createMockAppState({ tempStorage }),
});
renderWithProviders(<TestComponent entry={entry} newValue={newValue} />, {
storeInitialState: initialState,
});
};
describe("useTempStorage hook", () => {
it("should return undefined for uninitialized key", () => {
const tempStorage = {
animal: undefined,
};
// @ts-expect-error - The hook still doesn't accept any k/v pair
setup({ tempStorage, entry: "animal" });
expect(screen.getByTestId("result")).toHaveTextContent(
"Value is: undefined",
);
});
it("should read and set the value", async () => {
const tempStorage = {
animal: "dog",
};
// @ts-expect-error - The hook still doesn't accept any k/v pair
setup({ tempStorage, entry: "animal", newValue: "cat" });
expect(screen.getByTestId("result")).toHaveTextContent("Value is: dog");
await userEvent.click(screen.getByRole("button"));
expect(screen.getByTestId("result")).toHaveTextContent("Value is: cat");
});
});
import { createAction } from "@reduxjs/toolkit";
import {
type PayloadAction,
createAction,
createSlice,
} from "@reduxjs/toolkit";
import { LOCATION_CHANGE, push } from "react-router-redux";
import {
......@@ -7,7 +11,12 @@ import {
shouldOpenInBlankWindow,
} from "metabase/lib/dom";
import { combineReducers, handleActions } from "metabase/lib/redux";
import type { Dispatch } from "metabase-types/store";
import type {
Dispatch,
TempStorage,
TempStorageKey,
TempStorageValue,
} from "metabase-types/store";
interface LocationChangeAction {
type: string; // "@@router/LOCATION_CHANGE"
......@@ -107,6 +116,24 @@ const isErrorDiagnosticsOpen = handleActions(
false,
);
const tempStorageSlice = createSlice({
name: "tempStorage",
initialState: {} as TempStorage,
reducers: {
setTempSetting: (
state,
action: PayloadAction<{
key: TempStorageKey;
value: TempStorageValue<TempStorageKey>;
}>,
) => {
state[action.payload.key] = action.payload.value;
},
},
});
export const { setTempSetting } = tempStorageSlice.actions;
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default combineReducers({
errorPage,
......@@ -118,4 +145,5 @@ export default combineReducers({
return true;
},
isErrorDiagnosticsOpen,
tempStorage: tempStorageSlice.reducer,
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment