From 86bf1515d4d1eba412444002922101714dc22cff Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 26 Apr 2020 19:04:17 +0200 Subject: [PATCH] Added redux middleware to save parts of the store in the local storage transparently --- package-lock.json | 27 ++++++++++++++++ package.json | 1 + src/App.js | 36 +++++++++------------- src/container/index.js | 1 - src/container/store.js | 11 +++++-- src/settings/reducers/settings.js | 22 ++++--------- src/settings/services/provideServices.js | 5 ++- test/settings/reducers/settings.test.js | 39 +++--------------------- 8 files changed, 65 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a22485b..05204cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ab2033c1..2e2678a4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.js b/src/App.js index fd6c859c..edb9df36 100644 --- a/src/App.js +++ b/src/App.js @@ -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) => () => ( +
+ - return ( -
- - -
- - - - - - - - -
+
+ + + + + + + +
- ); -}; +
+); export default App; diff --git a/src/container/index.js b/src/container/index.js index 95383b13..3a8ac83a 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -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); diff --git a/src/container/store.js b/src/container/store.js index 0d5b2b2d..6d0de762 100644 --- a/src/container/store.js +++ b/src/container/store.js @@ -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; diff --git a/src/settings/reducers/settings.js b/src/settings/reducers/settings.js index 3160862b..6771dbb5 100644 --- a/src/settings/reducers/settings.js +++ b/src/settings/reducers/settings.js @@ -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 }, +}); diff --git a/src/settings/services/provideServices.js b/src/settings/services/provideServices.js index cbc24a12..08ba77d3 100644 --- a/src/settings/services/provideServices.js +++ b/src/settings/services/provideServices.js @@ -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; diff --git a/test/settings/reducers/settings.test.js b/test/settings/reducers/settings.test.js index 2eb7e5db..80f22c49 100644 --- a/test/settings/reducers/settings.test.js +++ b/test/settings/reducers/settings.test.js @@ -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 } }); }); }); });