mirror of
https://github.com/prymitive/karma
synced 2026-08-21 11:46:31 +00:00
fix(ui): get rid of mobx-stored
This commit is contained in:
committed by
Łukasz Mierzwa
parent
1cee18465d
commit
ec1b05cd37
Generated
+188
-92
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,6 @@
|
||||
"lodash.uniqueid": "4.0.1",
|
||||
"mobx": "6.15.0",
|
||||
"mobx-react-lite": "4.1.1",
|
||||
"mobx-stored": "1.1.0",
|
||||
"promise-retry": "2.0.1",
|
||||
"react": "19.2.4",
|
||||
"react-cool-dimensions": "3.0.1",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { observable, autorun, set, toJS } from "mobx";
|
||||
|
||||
interface LocalStoreResult<T> {
|
||||
value: T;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
function localStored<T extends object>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
): LocalStoreResult<T> {
|
||||
const initial: T = { ...defaultValue };
|
||||
|
||||
const fromStorage = localStorage.getItem(key);
|
||||
if (fromStorage) {
|
||||
try {
|
||||
Object.assign(initial, JSON.parse(fromStorage));
|
||||
} catch {
|
||||
// ignore malformed JSON, use defaults
|
||||
}
|
||||
}
|
||||
|
||||
const obsVal = observable(initial);
|
||||
|
||||
const disposeAutorun = autorun(
|
||||
() => {
|
||||
localStorage.setItem(key, JSON.stringify(toJS(obsVal)));
|
||||
},
|
||||
{ delay: 0 },
|
||||
);
|
||||
|
||||
const onStorageEvent = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue) {
|
||||
try {
|
||||
set(obsVal, JSON.parse(e.newValue));
|
||||
} catch {
|
||||
// ignore malformed JSON from other tabs
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("storage", onStorageEvent);
|
||||
|
||||
return {
|
||||
value: obsVal,
|
||||
destroy() {
|
||||
disposeAutorun();
|
||||
window.removeEventListener("storage", onStorageEvent);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { localStored };
|
||||
export type { LocalStoreResult };
|
||||
@@ -1,4 +1,10 @@
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { AnimationsConfiguration } from "./AnimationsConfiguration";
|
||||
@@ -26,7 +32,9 @@ describe("<AnimationsConfiguration />", () => {
|
||||
renderConfiguration();
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
settingsStore.themeConfig.setAnimations(true);
|
||||
act(() => {
|
||||
settingsStore.themeConfig.setAnimations(true);
|
||||
});
|
||||
expect(settingsStore.themeConfig.config.animations).toBe(true);
|
||||
fireEvent.click(checkbox);
|
||||
await waitFor(() => {
|
||||
@@ -38,7 +46,9 @@ describe("<AnimationsConfiguration />", () => {
|
||||
renderConfiguration();
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
settingsStore.themeConfig.setAnimations(false);
|
||||
act(() => {
|
||||
settingsStore.themeConfig.setAnimations(false);
|
||||
});
|
||||
expect(settingsStore.themeConfig.config.animations).toBe(false);
|
||||
fireEvent.click(checkbox);
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -19,10 +19,6 @@ beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.setItem("history.filters", "");
|
||||
});
|
||||
|
||||
const renderHistory = () => {
|
||||
return render(
|
||||
<History alertStore={alertStore} settingsStore={settingsStore} />,
|
||||
@@ -322,3 +318,205 @@ describe("<HistoryMenu />", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("History localStorage", () => {
|
||||
// Verifies that history entries persisted in localStorage by a previous
|
||||
// session are loaded and displayed when the component mounts.
|
||||
it("loads pre-populated localStorage on mount", async () => {
|
||||
const promise = Promise.resolve();
|
||||
const savedFilters = [
|
||||
[{ raw: "cluster=prod", name: "cluster", matcher: "=", value: "prod" }],
|
||||
[{ raw: "env=staging", name: "env", matcher: "=", value: "staging" }],
|
||||
];
|
||||
localStorage.setItem("filters", JSON.stringify({ filters: savedFilters }));
|
||||
|
||||
const { container } = renderHistory();
|
||||
const toggle = container.querySelector("button.cursor-pointer");
|
||||
fireEvent.click(toggle!);
|
||||
|
||||
expect(screen.getByText("cluster=prod")).toBeInTheDocument();
|
||||
expect(screen.getByText("env=staging")).toBeInTheDocument();
|
||||
expect(container.querySelectorAll("button.dropdown-item")).toHaveLength(2);
|
||||
await act(() => promise);
|
||||
});
|
||||
|
||||
// Verifies that localStored persists observable changes to localStorage
|
||||
// via a delayed reaction (setTimeout 0).
|
||||
it("localStored persists observable changes to localStorage", () => {
|
||||
const { localStored } = require("Common/LocalStore");
|
||||
const { runInAction } = require("mobx");
|
||||
const store = localStored("test_persist_key", { filters: [] as any[] });
|
||||
|
||||
// Flush the initial write (reaction fires via setTimeout 0)
|
||||
jest.runAllTimers();
|
||||
const afterInit = JSON.parse(
|
||||
localStorage.getItem("test_persist_key") || "{}",
|
||||
);
|
||||
expect(afterInit.filters).toEqual([]);
|
||||
|
||||
runInAction(() => {
|
||||
store.value.filters = [
|
||||
[{ raw: "foo=bar", name: "foo", matcher: "=", value: "bar" }],
|
||||
];
|
||||
});
|
||||
|
||||
// Flush the reaction that persists to localStorage
|
||||
jest.runAllTimers();
|
||||
const afterSet = JSON.parse(
|
||||
localStorage.getItem("test_persist_key") || "{}",
|
||||
);
|
||||
expect(afterSet.filters).toHaveLength(1);
|
||||
expect(afterSet.filters[0]).toEqual([
|
||||
{ raw: "foo=bar", name: "foo", matcher: "=", value: "bar" },
|
||||
]);
|
||||
|
||||
store.destroy();
|
||||
localStorage.removeItem("test_persist_key");
|
||||
});
|
||||
|
||||
// Verifies that all rapid sequential filter changes are captured in
|
||||
// history and none are lost due to timing.
|
||||
it("preserves all sequential filter changes in history", async () => {
|
||||
const promise = Promise.resolve();
|
||||
const { container } = renderHistory();
|
||||
|
||||
act(() => {
|
||||
alertStore.filters.setFilterValues([
|
||||
AppliedFilter("cluster", "=", "prod"),
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
alertStore.filters.setFilterValues([
|
||||
AppliedFilter("env", "=", "staging"),
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
alertStore.filters.setFilterValues([
|
||||
AppliedFilter("region", "=", "us-east"),
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
const toggle = container.querySelector("button.cursor-pointer");
|
||||
fireEvent.click(toggle!);
|
||||
|
||||
expect(container.querySelectorAll("button.dropdown-item")).toHaveLength(3);
|
||||
expect(screen.getByText("region=us-east")).toBeInTheDocument();
|
||||
expect(screen.getByText("env=staging")).toBeInTheDocument();
|
||||
expect(screen.getByText("cluster=prod")).toBeInTheDocument();
|
||||
await act(() => promise);
|
||||
});
|
||||
|
||||
// Simulates a second browser tab writing to localStorage and firing a
|
||||
// StorageEvent. Verifies the component picks up the cross-tab change.
|
||||
// mobx-stored's propagateChangesToMemory handler calls set(obsVal, newValue)
|
||||
// which replaces the observable state and re-establishes the persistence
|
||||
// autorun, so multiple timer flushes are needed.
|
||||
it("picks up StorageEvent from another tab", async () => {
|
||||
const promise = Promise.resolve();
|
||||
const { container } = renderHistory();
|
||||
|
||||
act(() => {
|
||||
alertStore.filters.setFilterValues([AppliedFilter("foo", "=", "bar")]);
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
const externalFilters = {
|
||||
filters: [
|
||||
[
|
||||
{
|
||||
raw: "external=filter",
|
||||
name: "external",
|
||||
matcher: "=",
|
||||
value: "filter",
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
const newValue = JSON.stringify(externalFilters);
|
||||
localStorage.setItem("filters", newValue);
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key: "filters",
|
||||
newValue: newValue,
|
||||
storageArea: localStorage,
|
||||
}),
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
const toggle = container.querySelector("button.cursor-pointer");
|
||||
fireEvent.click(toggle!);
|
||||
|
||||
expect(screen.getByText("external=filter")).toBeInTheDocument();
|
||||
await act(() => promise);
|
||||
});
|
||||
|
||||
// Demonstrates the cross-tab race condition. When tab B receives a
|
||||
// StorageEvent with tab A's history and then tab B's own alertStore
|
||||
// filters change, the History component's autorun rebuilds history
|
||||
// from only what's in its own alertStore. Entries from tab A that
|
||||
// are not in tab B's alertStore are preserved only if they were
|
||||
// already in the in-memory history.config.filters. If the
|
||||
// StorageEvent arrived after the autorun already ran, tab A's
|
||||
// entries could be lost.
|
||||
it("StorageEvent followed by filter change preserves cross-tab history", async () => {
|
||||
const promise = Promise.resolve();
|
||||
const { container } = renderHistory();
|
||||
|
||||
// Simulate tab A writing history with "cluster=prod" to localStorage
|
||||
// and firing a StorageEvent that tab B receives
|
||||
const tabAHistory = {
|
||||
filters: [
|
||||
[
|
||||
{
|
||||
raw: "cluster=prod",
|
||||
name: "cluster",
|
||||
matcher: "=",
|
||||
value: "prod",
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
const tabAValue = JSON.stringify(tabAHistory);
|
||||
localStorage.setItem("filters", tabAValue);
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", {
|
||||
key: "filters",
|
||||
newValue: tabAValue,
|
||||
storageArea: localStorage,
|
||||
}),
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
// Now tab B's own alertStore gets a different filter applied.
|
||||
// The History autorun should merge tab A's "cluster=prod" with
|
||||
// tab B's new "env=staging" in history.
|
||||
act(() => {
|
||||
alertStore.filters.setFilterValues([
|
||||
AppliedFilter("env", "=", "staging"),
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
const toggle = container.querySelector("button.cursor-pointer");
|
||||
fireEvent.click(toggle!);
|
||||
|
||||
// Both tab A's and tab B's filter sets should appear in history.
|
||||
// If "cluster=prod" is missing, the autorun overwrote tab A's data.
|
||||
expect(screen.getByText("env=staging")).toBeInTheDocument();
|
||||
expect(screen.getByText("cluster=prod")).toBeInTheDocument();
|
||||
expect(container.querySelectorAll("button.dropdown-item")).toHaveLength(2);
|
||||
await act(() => promise);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
|
||||
import { action, autorun } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { localStored } from "mobx-stored";
|
||||
|
||||
import { useFloating, shift, flip, offset, size } from "@floating-ui/react-dom";
|
||||
|
||||
@@ -26,6 +25,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
|
||||
import type { AlertStore, FilterT } from "Stores/AlertStore";
|
||||
import type { Settings } from "Stores/Settings";
|
||||
import { IsMobile } from "Common/Device";
|
||||
import { localStored } from "Common/LocalStore";
|
||||
import { DropdownSlide } from "Components/Animations/DropdownSlide";
|
||||
import HistoryLabel from "Components/Labels/HistoryLabel";
|
||||
import { useOnClickOutside } from "Hooks/useOnClickOutside";
|
||||
@@ -170,26 +170,27 @@ interface HistoryStorageT {
|
||||
}
|
||||
|
||||
class HistoryStorage {
|
||||
config: HistoryStorageT = localStored(
|
||||
"filters",
|
||||
{
|
||||
filters: [] as ReduceFilterT[][],
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
},
|
||||
);
|
||||
private store = localStored<HistoryStorageT>("filters", {
|
||||
filters: [] as ReduceFilterT[][],
|
||||
});
|
||||
|
||||
get config(): HistoryStorageT {
|
||||
return this.store.value;
|
||||
}
|
||||
|
||||
setFilters = action((newFilters: ReduceFilterT[][]) => {
|
||||
this.config.filters = newFilters;
|
||||
this.store.value.filters = newFilters;
|
||||
});
|
||||
|
||||
destroy = () => {
|
||||
this.store.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
const History: FC<{
|
||||
alertStore: AlertStore;
|
||||
settingsStore: Settings;
|
||||
}> = observer(({ alertStore, settingsStore }) => {
|
||||
// this will be dumped to local storage via mobx-stored
|
||||
const [history] = useState<HistoryStorage>(new HistoryStorage());
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const [maxHeight, setMaxHeight] = useState<number | null>(null);
|
||||
@@ -212,38 +213,40 @@ const History: FC<{
|
||||
|
||||
// every time this component updates we will rewrite history
|
||||
// (if there are changes)
|
||||
useEffect(
|
||||
() =>
|
||||
autorun(() => {
|
||||
// we don't store unapplied (we only have raw text for those, we need
|
||||
// name & value for coloring) or invalid filters
|
||||
// also check for value, name might be missing for fuzzy filters, but
|
||||
// the value should always be set
|
||||
const validAppliedFilters = alertStore.filters.values
|
||||
.filter((f) => f.applied && f.isValid && f.value)
|
||||
.map((f) => ReduceFilter(f));
|
||||
useEffect(() => {
|
||||
const disposeAutorun = autorun(() => {
|
||||
// we don't store unapplied (we only have raw text for those, we need
|
||||
// name & value for coloring) or invalid filters
|
||||
// also check for value, name might be missing for fuzzy filters, but
|
||||
// the value should always be set
|
||||
const validAppliedFilters = alertStore.filters.values
|
||||
.filter((f) => f.applied && f.isValid && f.value)
|
||||
.map((f) => ReduceFilter(f));
|
||||
|
||||
// don't store empty filters in history
|
||||
if (validAppliedFilters.length === 0) return;
|
||||
// make a JSON dump for comparing later with what's already stored
|
||||
const filtersJSON = JSON.stringify(validAppliedFilters);
|
||||
// don't store empty filters in history
|
||||
if (validAppliedFilters.length === 0) return;
|
||||
// make a JSON dump for comparing later with what's already stored
|
||||
const filtersJSON = JSON.stringify(validAppliedFilters);
|
||||
|
||||
// rewrite history putting current filter set on top, this will move
|
||||
// it up if user selects a filter set that was already in history
|
||||
const newHistory = [
|
||||
...[validAppliedFilters],
|
||||
...history.config.filters.filter(
|
||||
(f) => JSON.stringify(f) !== filtersJSON,
|
||||
),
|
||||
].slice(0, 8);
|
||||
if (
|
||||
JSON.stringify(newHistory) !== JSON.stringify(history.config.filters)
|
||||
) {
|
||||
history.setFilters(newHistory);
|
||||
}
|
||||
}),
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
// rewrite history putting current filter set on top, this will move
|
||||
// it up if user selects a filter set that was already in history
|
||||
const newHistory = [
|
||||
...[validAppliedFilters],
|
||||
...history.config.filters.filter(
|
||||
(f) => JSON.stringify(f) !== filtersJSON,
|
||||
),
|
||||
].slice(0, 8);
|
||||
if (
|
||||
JSON.stringify(newHistory) !== JSON.stringify(history.config.filters)
|
||||
) {
|
||||
history.setFilters(newHistory);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
disposeAutorun();
|
||||
history.destroy();
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useOnClickOutside(ref, hide, isVisible);
|
||||
|
||||
+50
-73
@@ -1,5 +1,6 @@
|
||||
import { action } from "mobx";
|
||||
import { localStored } from "mobx-stored";
|
||||
|
||||
import { localStored } from "Common/LocalStore";
|
||||
|
||||
import type { UIDefaults } from "Models/UI";
|
||||
import type { OptionT } from "Common/Select";
|
||||
@@ -9,25 +10,23 @@ interface SavedFiltersStorage {
|
||||
present: boolean;
|
||||
}
|
||||
class SavedFilters {
|
||||
config: SavedFiltersStorage = localStored(
|
||||
"savedFilters",
|
||||
{
|
||||
filters: [],
|
||||
present: false,
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
},
|
||||
);
|
||||
private store = localStored<SavedFiltersStorage>("savedFilters", {
|
||||
filters: [],
|
||||
present: false,
|
||||
});
|
||||
|
||||
get config(): SavedFiltersStorage {
|
||||
return this.store.value;
|
||||
}
|
||||
|
||||
save = action((newFilters: string[]) => {
|
||||
this.config.filters = newFilters;
|
||||
this.config.present = true;
|
||||
this.store.value.filters = newFilters;
|
||||
this.store.value.present = true;
|
||||
});
|
||||
|
||||
clear = action(() => {
|
||||
this.config.filters = [];
|
||||
this.config.present = false;
|
||||
this.store.value.filters = [];
|
||||
this.store.value.present = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,15 +34,15 @@ interface FetchConfigStorage {
|
||||
interval: number;
|
||||
}
|
||||
class FetchConfig {
|
||||
private store;
|
||||
config: FetchConfigStorage;
|
||||
setInterval: (newInterval: number) => void;
|
||||
|
||||
constructor(refresh: number) {
|
||||
this.config = localStored(
|
||||
"fetchConfig",
|
||||
{ interval: refresh },
|
||||
{ delay: 100 },
|
||||
);
|
||||
this.store = localStored<FetchConfigStorage>("fetchConfig", {
|
||||
interval: refresh,
|
||||
});
|
||||
this.config = this.store.value;
|
||||
|
||||
this.setInterval = action((newInterval) => {
|
||||
this.config.interval = newInterval;
|
||||
@@ -86,15 +85,12 @@ class AlertGroupConfig {
|
||||
collapseState: CollapseStateT,
|
||||
colorTitleBar: boolean,
|
||||
) {
|
||||
this.config = localStored(
|
||||
"alertGroupConfig",
|
||||
{
|
||||
defaultRenderCount: renderCount,
|
||||
defaultCollapseState: collapseState,
|
||||
colorTitleBar: colorTitleBar,
|
||||
},
|
||||
{ delay: 100 },
|
||||
);
|
||||
const store = localStored<AlertGroupConfigStorage>("alertGroupConfig", {
|
||||
defaultRenderCount: renderCount,
|
||||
defaultCollapseState: collapseState,
|
||||
colorTitleBar: colorTitleBar,
|
||||
});
|
||||
this.config = store.value;
|
||||
|
||||
this.setDefaultRenderCount = action((val: number) => {
|
||||
this.config.defaultRenderCount = val;
|
||||
@@ -116,11 +112,10 @@ class SilenceFormConfig {
|
||||
saveAuthor: (newAuthor: string) => void;
|
||||
|
||||
constructor() {
|
||||
this.config = localStored(
|
||||
"silenceFormConfig",
|
||||
{ author: "" },
|
||||
{ delay: 100 },
|
||||
);
|
||||
const store = localStored<SilenceFormConfigStorage>("silenceFormConfig", {
|
||||
author: "",
|
||||
});
|
||||
this.config = store.value;
|
||||
|
||||
this.saveAuthor = action((newAuthor: string) => {
|
||||
this.config.author = newAuthor;
|
||||
@@ -158,16 +153,13 @@ class GridConfig {
|
||||
setGroupWidth: (w: number) => void;
|
||||
|
||||
constructor(groupWidth: number) {
|
||||
this.config = localStored(
|
||||
"alertGridConfig",
|
||||
{
|
||||
sortOrder: this.options.default.value,
|
||||
sortLabel: null,
|
||||
reverseSort: null,
|
||||
groupWidth: groupWidth,
|
||||
},
|
||||
{ delay: 100 },
|
||||
);
|
||||
const store = localStored<GridConfigStorage>("alertGridConfig", {
|
||||
sortOrder: this.options.default.value as SortOrderT,
|
||||
sortLabel: null,
|
||||
reverseSort: null,
|
||||
groupWidth: groupWidth,
|
||||
});
|
||||
this.config = store.value;
|
||||
|
||||
this.setSortOrder = action((o: SortOrderT) => {
|
||||
this.config.sortOrder = o;
|
||||
@@ -192,15 +184,10 @@ class FilterBarConfig {
|
||||
setAutohide: (v: boolean) => void;
|
||||
|
||||
constructor(autohide: boolean) {
|
||||
this.config = localStored(
|
||||
"filterBarConfig",
|
||||
{
|
||||
autohide: autohide,
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
},
|
||||
);
|
||||
const store = localStored<FilterBarConfigStorage>("filterBarConfig", {
|
||||
autohide: autohide,
|
||||
});
|
||||
this.config = store.value;
|
||||
this.setAutohide = action((v: boolean) => {
|
||||
this.config.autohide = v;
|
||||
});
|
||||
@@ -229,16 +216,11 @@ class ThemeConfig {
|
||||
dark: { label: "Dark theme", value: "dark", wasCreated: false },
|
||||
});
|
||||
|
||||
this.config = localStored(
|
||||
"themeConfig",
|
||||
{
|
||||
theme: defaultTheme,
|
||||
animations: animations,
|
||||
},
|
||||
{
|
||||
delay: 0,
|
||||
},
|
||||
);
|
||||
const store = localStored<ThemeConfigStorage>("themeConfig", {
|
||||
theme: defaultTheme,
|
||||
animations: animations,
|
||||
});
|
||||
this.config = store.value;
|
||||
this.setTheme = action((v: ThemeT) => {
|
||||
this.config.theme = v;
|
||||
});
|
||||
@@ -258,16 +240,11 @@ class MultiGridConfig {
|
||||
setGridSortReverse: (v: boolean) => void;
|
||||
|
||||
constructor(gridLabel: string, gridSortReverse: boolean) {
|
||||
this.config = localStored(
|
||||
"multiGridConfig",
|
||||
{
|
||||
gridLabel: gridLabel,
|
||||
gridSortReverse: gridSortReverse,
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
},
|
||||
);
|
||||
const store = localStored<MultiGridConfigStorage>("multiGridConfig", {
|
||||
gridLabel: gridLabel,
|
||||
gridSortReverse: gridSortReverse,
|
||||
});
|
||||
this.config = store.value;
|
||||
|
||||
this.setGridLabel = action((l: string) => {
|
||||
this.config.gridLabel = l;
|
||||
|
||||
@@ -51,6 +51,7 @@ console.error = (...args: unknown[]) => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
useFetchGetMock.fetch.reset();
|
||||
(useFetchGet as jest.MockedFunction<typeof useFetchGetMock>).mockRestore();
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user