mirror of
https://github.com/gosticks/web.git
synced 2026-10-07 07:07:07 +00:00
Merge pull request #157 from pi-hole/feature/cache-web-preferences
Cache the web interface preferences in local storage
This commit is contained in:
Generated
+6
@@ -11629,6 +11629,12 @@
|
||||
"pretty-format": "^23.6.0"
|
||||
}
|
||||
},
|
||||
"jest-localstorage-mock": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.0.tgz",
|
||||
"integrity": "sha512-/mC1JxnMeuIlAaQBsDMilskC/x/BicsQ/BXQxEOw+5b1aGZkkOAqAF3nu8yq449CpzGtp5jJ5wCmDNxLgA2m6A==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-matcher-utils": {
|
||||
"version": "23.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"fetch-mock": "^7.3.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"jest-enzyme": "^7.0.1",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"multi-progress": "^2.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"node-sass-chokidar": "^1.3.4",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* Apply the web interface language preference
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { PreferencesContext } from "./context";
|
||||
import i18n from "i18next";
|
||||
|
||||
const applyLanguage = (language: string) => {
|
||||
if (i18n.language === language) {
|
||||
// Don't change the language if it's already correctly set
|
||||
return;
|
||||
}
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
i18n.changeLanguage(language);
|
||||
};
|
||||
|
||||
export default () => (
|
||||
<PreferencesContext.Consumer>
|
||||
{({ settings }) => {
|
||||
applyLanguage(settings.language);
|
||||
|
||||
return null;
|
||||
}}
|
||||
</PreferencesContext.Consumer>
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* Web Interface
|
||||
* React context object tests
|
||||
*
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import {
|
||||
defaultPreferences,
|
||||
loadInitialPreferences,
|
||||
WEB_PREFERENCES_STORAGE_KEY
|
||||
} from "../context";
|
||||
|
||||
it("loads the default preferences if none are cached", () => {
|
||||
const preferences = loadInitialPreferences();
|
||||
|
||||
expect(preferences).toEqual(defaultPreferences);
|
||||
});
|
||||
|
||||
it("loads the cached preferences when available", () => {
|
||||
const expectedPreferences: ApiPreferences = {
|
||||
language: "testLang",
|
||||
layout: "boxed"
|
||||
};
|
||||
|
||||
localStorage.setItem(
|
||||
WEB_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify(expectedPreferences)
|
||||
);
|
||||
|
||||
const actualPreferences = loadInitialPreferences();
|
||||
|
||||
expect(actualPreferences).toEqual(expectedPreferences);
|
||||
});
|
||||
@@ -22,12 +22,41 @@ export interface PreferencesContextType {
|
||||
refresh: (data?: ApiPreferences) => void;
|
||||
}
|
||||
|
||||
export const WEB_PREFERENCES_STORAGE_KEY = "webPreferences";
|
||||
|
||||
const initialStatus: StatusContextType = {
|
||||
status: "unknown",
|
||||
refresh: () => {}
|
||||
};
|
||||
|
||||
export const defaultPreferences: ApiPreferences = {
|
||||
layout: "boxed",
|
||||
language: "en"
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the initial web interface preferences. Cached preferences from local
|
||||
* storage will be used if available, otherwise default preferences will be
|
||||
* used.
|
||||
*/
|
||||
export const loadInitialPreferences = (): ApiPreferences => {
|
||||
const cachedPreferencesString = localStorage.getItem(
|
||||
WEB_PREFERENCES_STORAGE_KEY
|
||||
);
|
||||
|
||||
if (cachedPreferencesString === null) {
|
||||
return defaultPreferences;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(cachedPreferencesString);
|
||||
} catch (e) {
|
||||
return defaultPreferences;
|
||||
}
|
||||
};
|
||||
|
||||
const initialPreferences: PreferencesContextType = {
|
||||
settings: { layout: "boxed", language: "en" },
|
||||
settings: loadInitialPreferences(),
|
||||
refresh: () => {}
|
||||
};
|
||||
|
||||
@@ -101,11 +130,19 @@ export const PreferencesProvider = ({
|
||||
{children}
|
||||
</PreferencesContext.Provider>
|
||||
)}
|
||||
renderOk={(settings, refresh) => (
|
||||
<PreferencesContext.Provider value={{ settings, refresh }} {...props}>
|
||||
{children}
|
||||
</PreferencesContext.Provider>
|
||||
)}
|
||||
renderOk={(settings, refresh) => {
|
||||
// Update the cached settings
|
||||
localStorage.setItem(
|
||||
WEB_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify(settings)
|
||||
);
|
||||
|
||||
return (
|
||||
<PreferencesContext.Provider value={{ settings, refresh }} {...props}>
|
||||
{children}
|
||||
</PreferencesContext.Provider>
|
||||
);
|
||||
}}
|
||||
renderErr={(_, refresh) => (
|
||||
<PreferencesContext.Provider
|
||||
value={{ settings: { layout: "boxed", language: "en" }, refresh }}
|
||||
|
||||
@@ -16,7 +16,6 @@ import Alert, { AlertType } from "../common/Alert";
|
||||
import { Button, Col, Form, FormGroup, Input, Label } from "reactstrap";
|
||||
import { PreferencesContext } from "../common/context";
|
||||
import languages from "../../languages.json";
|
||||
import i18n from "i18next";
|
||||
|
||||
export interface PreferenceSettingsProps {
|
||||
settings: ApiPreferences;
|
||||
@@ -28,6 +27,8 @@ export interface PreferenceSettingsState {
|
||||
alertType: AlertType;
|
||||
showAlert: boolean;
|
||||
processing: boolean;
|
||||
translateMessage: boolean;
|
||||
error: { key: string; data: any } | null;
|
||||
settings: ApiPreferences;
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ class PreferenceSettings extends Component<
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
processing: false,
|
||||
translateMessage: true,
|
||||
error: null,
|
||||
// Initial value is the current settings
|
||||
settings: this.props.settings
|
||||
};
|
||||
@@ -100,13 +103,13 @@ class PreferenceSettings extends Component<
|
||||
saveSettings = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { t } = this.props;
|
||||
|
||||
this.setState({
|
||||
alertMessage: t("Processing..."),
|
||||
alertMessage: "Processing...",
|
||||
error: null,
|
||||
alertType: "info",
|
||||
showAlert: true,
|
||||
processing: true
|
||||
processing: true,
|
||||
translateMessage: true
|
||||
});
|
||||
|
||||
this.updateHandler = makeCancelable(
|
||||
@@ -115,40 +118,37 @@ class PreferenceSettings extends Component<
|
||||
this.updateHandler.promise
|
||||
.then(() => {
|
||||
this.setState({
|
||||
alertMessage: t("Successfully saved preferences"),
|
||||
alertMessage: "Successfully saved preferences",
|
||||
alertType: "success",
|
||||
showAlert: true,
|
||||
processing: false
|
||||
processing: false,
|
||||
translateMessage: true
|
||||
});
|
||||
|
||||
// Update the language
|
||||
i18n.changeLanguage(this.state.settings.language).then(() =>
|
||||
// Once the language is updated, update the alert message to use the
|
||||
// new language
|
||||
this.setState({ alertMessage: t("Successfully saved preferences") })
|
||||
);
|
||||
|
||||
// Update anyone using the preferences
|
||||
this.props.refresh(this.state.settings);
|
||||
})
|
||||
.catch(ignoreCancel)
|
||||
.catch(error => {
|
||||
let message = "";
|
||||
let apiError = null;
|
||||
let translateMessage = true;
|
||||
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
translateMessage = false;
|
||||
} else {
|
||||
// Translate the API's error message
|
||||
message = t("API Error: {{error}}", {
|
||||
error: t(error.key, error.data)
|
||||
});
|
||||
message = "API Error: {{error}}";
|
||||
apiError = error;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
alertMessage: message,
|
||||
error: apiError,
|
||||
alertType: "danger",
|
||||
showAlert: true,
|
||||
processing: false
|
||||
processing: false,
|
||||
translateMessage
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -157,12 +157,30 @@ class PreferenceSettings extends Component<
|
||||
this.setState({ showAlert: false });
|
||||
};
|
||||
|
||||
getAlertMessage = () => {
|
||||
const { t } = this.props;
|
||||
|
||||
if (this.state.error) {
|
||||
// Translate the API error
|
||||
return t(this.state.alertMessage, {
|
||||
error: t(this.state.error.key, this.state.error.data)
|
||||
});
|
||||
} else {
|
||||
// Check if the message should be translated
|
||||
if (this.state.translateMessage) {
|
||||
return t(this.state.alertMessage);
|
||||
} else {
|
||||
return this.state.alertMessage;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
const alert = this.state.showAlert ? (
|
||||
<Alert
|
||||
message={this.state.alertMessage}
|
||||
message={this.getAlertMessage()}
|
||||
type={this.state.alertType}
|
||||
onClick={this.hideAlert}
|
||||
/>
|
||||
|
||||
@@ -23,11 +23,13 @@ import {
|
||||
} from "../routes";
|
||||
import { GlobalContextProvider } from "../components/common/context";
|
||||
import LayoutApplier from "../components/common/LayoutApplier";
|
||||
import LanguageApplier from "../components/common/LanguageApplier";
|
||||
|
||||
export default (props: any) => (
|
||||
<div className="app">
|
||||
<GlobalContextProvider>
|
||||
<LayoutApplier />
|
||||
<LanguageApplier />
|
||||
<Header />
|
||||
<div className="app-body">
|
||||
<Sidebar items={nav} {...props} />
|
||||
|
||||
+7
-3
@@ -13,6 +13,7 @@ import Adapter from "enzyme-adapter-react-16";
|
||||
import "jest-enzyme";
|
||||
import api from "./util/api";
|
||||
import fetchMock from "fetch-mock";
|
||||
import "jest-localstorage-mock";
|
||||
|
||||
// Setup enzyme
|
||||
configure({ adapter: new Adapter() });
|
||||
@@ -29,11 +30,14 @@ jest.mock("react-i18next", () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// Temporary fix to reset logged in state for each test.
|
||||
// The permanent fix would be to not use a global variable, and instead give this information to components through
|
||||
// props. Redux would be good for this, if we want to add it to the project.
|
||||
beforeEach(() => {
|
||||
// Temporary fix to reset logged in state for each test.
|
||||
// The permanent fix would be to not use a global variable, and instead give this information to components through
|
||||
// props. Redux would be good for this, if we want to add it to the project.
|
||||
api.loggedIn = false;
|
||||
|
||||
// Clear local storage mock
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// Clear fetch mocks after each test
|
||||
|
||||
Reference in New Issue
Block a user