mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 07:25:44 +01:00
Simplify local storage access
The caching for local storage seems way over-engineered. With this the context for the local storage is removed and the local storage is accessed directly. Pushed-by: k8s-git-ops<admin@cloudogu.com> Pushed-by: Rene Pfeuffer<rene.pfeuffer@cloudogu.com> Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
2
gradle/changelog/local_storage.yaml
Normal file
2
gradle/changelog/local_storage.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: First access to local storage returning the default value instead of the actual value
|
||||
64
scm-ui/ui-api/src/localStorage.ts
Normal file
64
scm-ui/ui-api/src/localStorage.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type LocalStorageSetter<T> = (value: T | ((previousValue: T) => T)) => void;
|
||||
|
||||
const determineInitialValue = <T>(key: string, initialValue: T) => {
|
||||
try {
|
||||
const itemFromStorage = localStorage.getItem(key);
|
||||
return itemFromStorage ? JSON.parse(itemFromStorage) : initialValue;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
return initialValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an api to access the browser's local storage for a given key.
|
||||
*
|
||||
* @param key The local storage key
|
||||
* @param initialValue Value to be used if the local storage does not yet have the given key defined
|
||||
*/
|
||||
export function useLocalStorage<T>(key: string, initialValue: T): [value: T, setValue: LocalStorageSetter<T>] {
|
||||
const initialValueOrValueFromStorage = useMemo(() => determineInitialValue(key, initialValue), [key, initialValue]);
|
||||
const [item, setItem] = useState(initialValueOrValueFromStorage);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: StorageEvent) => {
|
||||
if (event.key === key) {
|
||||
setItem(determineInitialValue(key, initialValue));
|
||||
}
|
||||
};
|
||||
window.addEventListener("storage", listener);
|
||||
return () => window.removeEventListener("storage", listener);
|
||||
}, [key, initialValue]);
|
||||
|
||||
const setValue: LocalStorageSetter<T> = (newValue) => {
|
||||
const computedNewValue = newValue instanceof Function ? newValue(item) : newValue;
|
||||
setItem(computedNewValue);
|
||||
const json = JSON.stringify(computedNewValue);
|
||||
localStorage.setItem(key, json);
|
||||
// storage event is no triggered in same tab
|
||||
window.dispatchEvent(
|
||||
new StorageEvent("storage", { key, oldValue: item, newValue: json, storageArea: localStorage })
|
||||
);
|
||||
};
|
||||
|
||||
return [item, setValue];
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
type LocalStorage = {
|
||||
getItem: <T>(key: string, fallback: T) => T;
|
||||
setItem: <T>(key: string, value: T) => void;
|
||||
preload: <T>(key: string, initialValue: T) => void;
|
||||
};
|
||||
|
||||
const LocalStorageContext = createContext<LocalStorage>(null as unknown as LocalStorage);
|
||||
|
||||
/**
|
||||
* Cache provider for local storage which enables listening to changes and triggering re-renders when writing.
|
||||
*
|
||||
* Only required once as a wrapper for the whole application.
|
||||
*
|
||||
* @see useLocalStorage
|
||||
*/
|
||||
export const LocalStorageProvider: FC = ({ children }) => {
|
||||
const [localStorageCache, setLocalStorageCache] = useState<Record<string, unknown>>({});
|
||||
|
||||
const setItem = useCallback(<T,>(key: string, value: T) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
setLocalStorageCache((prevState) => ({
|
||||
...prevState,
|
||||
[key]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const getItem = useCallback(
|
||||
<T,>(key: string, fallback: T): T => (key in localStorageCache ? (localStorageCache[key] as T) : fallback),
|
||||
[localStorageCache]
|
||||
);
|
||||
|
||||
const preload = useCallback(
|
||||
<T,>(key: string, initialValue: T) => {
|
||||
if (!(key in localStorageCache)) {
|
||||
let initialLoadResult: T | undefined;
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
initialLoadResult = item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
initialLoadResult = initialValue;
|
||||
}
|
||||
setItem(key, initialLoadResult);
|
||||
}
|
||||
},
|
||||
[localStorageCache, setItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<LocalStorageContext.Provider value={useMemo(() => ({ getItem, setItem, preload }), [getItem, preload, setItem])}>
|
||||
{children}
|
||||
</LocalStorageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an api to access the browser's local storage for a given key.
|
||||
*
|
||||
* @param key The local storage key
|
||||
* @param initialValue Value to be used if the local storage does not yet have the given key defined
|
||||
*/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [value: T, setValue: (value: T | ((previousConfig: T) => T)) => void] {
|
||||
const { getItem, setItem, preload } = useContext(LocalStorageContext);
|
||||
const value = useMemo(() => getItem(key, initialValue), [getItem, initialValue, key]);
|
||||
const setValue = useCallback(
|
||||
(newValue: T | ((previousConfig: T) => T)) =>
|
||||
// @ts-ignore T could be a function type, although this does not make sense because function types are not serializable to json
|
||||
setItem(key, typeof newValue === "function" ? newValue(value) : newValue),
|
||||
// eslint does not understand generics in certain circumstances
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[key, setItem, value]
|
||||
);
|
||||
useEffect(() => preload(key, initialValue), [initialValue, key, preload]);
|
||||
return useMemo(() => [value, setValue], [setValue, value]);
|
||||
}
|
||||
@@ -17,9 +17,9 @@
|
||||
import i18next from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { withI18next } from "storybook-addon-i18next";
|
||||
import React, {useEffect} from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import withApiProvider from "./withApiProvider";
|
||||
import { withThemes } from 'storybook-addon-themes/react';
|
||||
import { withThemes } from "storybook-addon-themes/react";
|
||||
|
||||
let i18n = i18next;
|
||||
|
||||
@@ -27,7 +27,7 @@ let i18n = i18next;
|
||||
// and not for storyshots
|
||||
if (!process.env.JEST_WORKER_ID) {
|
||||
const Backend = require("i18next-fetch-backend");
|
||||
i18n = i18n.use(Backend.default);
|
||||
i18n = i18n.use(Backend);
|
||||
}
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
@@ -58,10 +58,10 @@ export const decorators = [
|
||||
},
|
||||
}),
|
||||
withApiProvider,
|
||||
withThemes
|
||||
withThemes,
|
||||
];
|
||||
|
||||
const Decorator = ({children, themeName}) => {
|
||||
const Decorator = ({ children, themeName }) => {
|
||||
useEffect(() => {
|
||||
const link = document.querySelector("#ui-theme");
|
||||
if (link && link["data-theme"] !== themeName) {
|
||||
@@ -69,7 +69,7 @@ const Decorator = ({children, themeName}) => {
|
||||
link["data-theme"] = themeName;
|
||||
}
|
||||
}, [themeName]);
|
||||
return <>{children}</>
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const parameters = {
|
||||
|
||||
@@ -77755,16 +77755,9 @@ exports[`Storyshots Secondary Navigation Active when match 1`] = `
|
||||
<div>
|
||||
<p
|
||||
aria-label="secondaryNavigation.hideContent"
|
||||
className="SecondaryNavigation__MenuLabel-sc-8p1rgi-2 joMQbq menu-label is-clickable"
|
||||
className="SecondaryNavigation__MenuLabel-sc-8p1rgi-2 joMQbq menu-label"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="SecondaryNavigation__Icon-sc-8p1rgi-1 hDCYCs is-medium"
|
||||
>
|
||||
<i
|
||||
className="fas fa-caret-down"
|
||||
/>
|
||||
</i>
|
||||
Hitchhiker
|
||||
</p>
|
||||
<ul
|
||||
|
||||
@@ -19,7 +19,6 @@ import { storiesOf } from "@storybook/react";
|
||||
import Footer from "./Footer";
|
||||
import { Binder, BinderContext, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { Me } from "@scm-manager/ui-types";
|
||||
import { LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
import { EXTENSION_POINT } from "../avatar/Avatar";
|
||||
// @ts-ignore ignore unknown png
|
||||
import hitchhiker from "../__resources__/hitchhiker.png";
|
||||
@@ -56,7 +55,6 @@ const withBinder = (binder: Binder) => {
|
||||
};
|
||||
|
||||
storiesOf("Footer", module)
|
||||
.addDecorator((story) => <LocalStorageProvider>{story()}</LocalStorageProvider>)
|
||||
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||
.add("Default", () => {
|
||||
return <Footer me={trillian} version="2.0.0" links={{}} />;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React, { ReactElement } from "react";
|
||||
import { LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
import SecondaryNavigation from "./SecondaryNavigation";
|
||||
import SecondaryNavigationItem from "./SecondaryNavigationItem";
|
||||
import styled from "styled-components";
|
||||
@@ -45,7 +44,6 @@ const withRoute = (route: string) => {
|
||||
};
|
||||
|
||||
storiesOf("Secondary Navigation", module)
|
||||
.addDecorator((story) => <LocalStorageProvider>{story()}</LocalStorageProvider>)
|
||||
.addDecorator((story) => (
|
||||
<Columns className="columns">
|
||||
<div className="column is-3">{story()}</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { binder, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import ChangesetShortLink from "./repos/components/changesets/ChangesetShortLink";
|
||||
|
||||
import "./tokenExpired";
|
||||
import { ApiProvider, LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
import { ApiProvider } from "@scm-manager/ui-api";
|
||||
import { ShortcutDocsContextProvider } from "@scm-manager/ui-core"; // Makes sure that the global `define` function is registered and all provided modules are included in the final bundle at all times
|
||||
import "./_modules/provided-modules";
|
||||
|
||||
@@ -42,7 +42,6 @@ if (!root) {
|
||||
ReactDOM.render(
|
||||
<ApiProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<LocalStorageProvider>
|
||||
<ShortcutDocsContextProvider>
|
||||
<ActiveModalCountContextProvider>
|
||||
<Router basename={urls.contextPath}>
|
||||
@@ -50,7 +49,6 @@ ReactDOM.render(
|
||||
</Router>
|
||||
</ActiveModalCountContextProvider>
|
||||
</ShortcutDocsContextProvider>
|
||||
</LocalStorageProvider>
|
||||
</I18nextProvider>
|
||||
</ApiProvider>,
|
||||
root
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from "react";
|
||||
import EditRepoNavLink from "./EditRepoNavLink";
|
||||
import { mount, shallow } from "@scm-manager/ui-tests";
|
||||
import { LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
|
||||
describe("GeneralNavLink", () => {
|
||||
it("should render nothing, if the modify link is missing", () => {
|
||||
@@ -44,11 +43,7 @@ describe("GeneralNavLink", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<LocalStorageProvider>
|
||||
<EditRepoNavLink repository={repository} editUrl="" />
|
||||
</LocalStorageProvider>
|
||||
);
|
||||
const navLink = mount(<EditRepoNavLink repository={repository} editUrl="" />);
|
||||
expect(navLink.text()).toBe("repositoryRoot.menu.generalNavLink");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from "react";
|
||||
import { mount, shallow } from "@scm-manager/ui-tests";
|
||||
import "@scm-manager/ui-tests";
|
||||
import { LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
@@ -39,11 +38,7 @@ describe("PermissionsNavLink", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<LocalStorageProvider>
|
||||
<PermissionsNavLink repository={repository} permissionUrl="" />
|
||||
</LocalStorageProvider>
|
||||
);
|
||||
const navLink = mount(<PermissionsNavLink repository={repository} permissionUrl="" />);
|
||||
expect(navLink.text()).toBe("repositoryRoot.menu.permissionsNavLink");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from "react";
|
||||
import { mount, shallow } from "@scm-manager/ui-tests";
|
||||
import "@scm-manager/ui-tests";
|
||||
import { LocalStorageProvider } from "@scm-manager/ui-api";
|
||||
import RepositoryNavLink from "./RepositoryNavLink";
|
||||
|
||||
describe("RepositoryNavLink", () => {
|
||||
@@ -54,7 +53,6 @@ describe("RepositoryNavLink", () => {
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<LocalStorageProvider>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
@@ -62,7 +60,6 @@ describe("RepositoryNavLink", () => {
|
||||
label="Sources"
|
||||
activeOnlyWhenExact={true}
|
||||
/>
|
||||
</LocalStorageProvider>
|
||||
);
|
||||
expect(navLink.text()).toBe("Sources");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user