Added redux middleware to save parts of the store in the local storage transparently

This commit is contained in:
Alejandro Celaya 2020-04-26 19:04:17 +02:00
parent bbc47b387e
commit 86bf1515d4
8 changed files with 65 additions and 77 deletions

27
package-lock.json generated
View file

@ -1463,6 +1463,14 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
"@shlinkio/redux-localstorage-simple": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@shlinkio/redux-localstorage-simple/-/redux-localstorage-simple-2.2.0.tgz",
"integrity": "sha512-2/VggbehDAM1dOH7rT3Qjr/MTp7qQ6VeTM+Ez4JnMUPtU9OxgV9FQbKqduasLT4EZhlRUhxwBp7K6WO3gROQDA==",
"requires": {
"object-merge": "2.5.1"
}
},
"@stryker-mutator/api": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-2.1.0.tgz",
@ -4246,6 +4254,11 @@
"shallow-clone": "^0.1.2"
}
},
"clone-function": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/clone-function/-/clone-function-1.0.6.tgz",
"integrity": "sha1-QoRxk3dQvKnEjsv7wW9uIy90oD0="
},
"clone-regexp": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
@ -11720,6 +11733,11 @@
"kind-of": "^3.0.3"
}
},
"object-foreach": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/object-foreach/-/object-foreach-0.1.2.tgz",
"integrity": "sha1-10IcW0DjtqPvV6xiQ2jSHY+NLew="
},
"object-hash": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
@ -11744,6 +11762,15 @@
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
"object-merge": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/object-merge/-/object-merge-2.5.1.tgz",
"integrity": "sha1-B36JFc446nKUeIRIxd0znjTfQic=",
"requires": {
"clone-function": ">=1.0.1",
"object-foreach": ">=0.1.2"
}
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",

View file

@ -27,6 +27,7 @@
"@fortawesome/free-regular-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"@shlinkio/redux-localstorage-simple": "^2.2.0",
"array-filter": "^1.0.0",
"array-map": "^0.0.0",
"array-reduce": "^0.0.0",

View file

@ -1,29 +1,23 @@
import React, { useEffect } from 'react';
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import NotFound from './common/NotFound';
import './App.scss';
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => {
useEffect(() => {
loadRealTimeUpdates();
}, []);
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => () => (
<div className="container-fluid app-container">
<MainHeader />
return (
<div className="container-fluid app-container">
<MainHeader />
<div className="app">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/server/create" component={CreateServer} />
<Route exact path="/server/:serverId/edit" component={EditServer} />
<Route path="/server/:serverId" component={MenuLayout} />
<Route component={NotFound} />
</Switch>
</div>
<div className="app">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/server/create" component={CreateServer} />
<Route exact path="/server/:serverId/edit" component={EditServer} />
<Route path="/server/:serverId" component={MenuLayout} />
<Route component={NotFound} />
</Switch>
</div>
);
};
</div>
);
export default App;

View file

@ -29,7 +29,6 @@ const connect = (propsFromState, actionServiceNames = []) =>
);
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ]));
provideCommonServices(bottle, connect, withRouter);
provideShortUrlsServices(bottle, connect);

View file

@ -1,13 +1,20 @@
import ReduxThunk from 'redux-thunk';
import { applyMiddleware, compose, createStore } from 'redux';
import { save, load } from '@shlinkio/redux-localstorage-simple';
import reducers from '../reducers';
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
: compose;
const store = createStore(reducers, composeEnhancers(
applyMiddleware(ReduxThunk)
const localStorageConfig = {
states: [ 'settings' ],
namespace: 'shlink',
namespaceSeparator: '.',
};
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
applyMiddleware(save(localStorageConfig), ReduxThunk)
));
export default store;

View file

@ -1,7 +1,7 @@
import { handleActions } from 'redux-actions';
import PropTypes from 'prop-types';
export const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES';
export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
export const SettingsType = PropTypes.shape({
realTimeUpdates: PropTypes.shape({
@ -16,20 +16,10 @@ const initialState = {
};
export default handleActions({
[LOAD_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
}, initialState);
export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => {
updateSettings({ realTimeUpdates: { enabled } });
return loadRealTimeUpdatesAction();
};
export const loadRealTimeUpdates = ({ loadSettings }) => () => {
const { realTimeUpdates = {} } = loadSettings();
return {
type: LOAD_REAL_TIME_UPDATES,
realTimeUpdates,
};
};
export const setRealTimeUpdates = (enabled) => ({
type: SET_REAL_TIME_UPDATES,
realTimeUpdates: { enabled },
});

View file

@ -1,6 +1,6 @@
import RealTimeUpdates from '../RealTimeUpdates';
import Settings from '../Settings';
import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/settings';
import { setRealTimeUpdates } from '../reducers/settings';
import SettingsService from './SettingsService';
const provideServices = (bottle, connect) => {
@ -14,8 +14,7 @@ const provideServices = (bottle, connect) => {
bottle.service('SettingsService', SettingsService, 'Storage');
// Actions
bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates');
bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService');
bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
};
export default provideServices;

View file

@ -1,48 +1,19 @@
import reducer, {
LOAD_REAL_TIME_UPDATES,
loadRealTimeUpdates,
setRealTimeUpdates,
} from '../../../src/settings/reducers/settings';
import reducer, { SET_REAL_TIME_UPDATES, setRealTimeUpdates } from '../../../src/settings/reducers/settings';
describe('settingsReducer', () => {
const SettingsServiceMock = {
updateSettings: jest.fn(),
loadSettings: jest.fn(),
};
const realTimeUpdates = { enabled: true };
afterEach(jest.clearAllMocks);
describe('reducer', () => {
it('returns realTimeUpdates when action is LOAD_REAL_TIME_UPDATES', () => {
expect(reducer({}, { type: LOAD_REAL_TIME_UPDATES, realTimeUpdates })).toEqual({ realTimeUpdates });
});
});
describe('loadRealTimeUpdates', () => {
it.each([[ true ], [ false ]])('loads settings and returns LOAD_REAL_TIME_UPDATES action', (enabled) => {
const realTimeUpdates = { enabled };
SettingsServiceMock.loadSettings.mockReturnValue({ realTimeUpdates });
const result = loadRealTimeUpdates(SettingsServiceMock)();
expect(result).toEqual({
type: LOAD_REAL_TIME_UPDATES,
realTimeUpdates,
});
expect(SettingsServiceMock.loadSettings).toHaveBeenCalled();
it('returns realTimeUpdates when action is SET_REAL_TIME_UPDATES', () => {
expect(reducer({}, { type: SET_REAL_TIME_UPDATES, realTimeUpdates })).toEqual({ realTimeUpdates });
});
});
describe('setRealTimeUpdates', () => {
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
const loadRealTimeUpdatesAction = jest.fn();
const result = setRealTimeUpdates(enabled);
setRealTimeUpdates(SettingsServiceMock, loadRealTimeUpdatesAction)(enabled);
expect(SettingsServiceMock.updateSettings).toHaveBeenCalledWith({ realTimeUpdates: { enabled } });
expect(loadRealTimeUpdatesAction).toHaveBeenCalled();
expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
});
});
});