mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-11 02:09:53 -06:00
Added redux middleware to save parts of the store in the local storage transparently
This commit is contained in:
parent
bbc47b387e
commit
86bf1515d4
27
package-lock.json
generated
27
package-lock.json
generated
@ -1463,6 +1463,14 @@
|
|||||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
|
||||||
"dev": true
|
"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": {
|
"@stryker-mutator/api": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-2.1.0.tgz",
|
||||||
@ -4246,6 +4254,11 @@
|
|||||||
"shallow-clone": "^0.1.2"
|
"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": {
|
"clone-regexp": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.1.tgz",
|
||||||
@ -11720,6 +11733,11 @@
|
|||||||
"kind-of": "^3.0.3"
|
"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": {
|
"object-hash": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
|
||||||
@ -11744,6 +11762,15 @@
|
|||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
"dev": true
|
"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": {
|
"object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
"@fortawesome/free-regular-svg-icons": "^5.11.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.5",
|
"@fortawesome/react-fontawesome": "^0.1.5",
|
||||||
|
"@shlinkio/redux-localstorage-simple": "^2.2.0",
|
||||||
"array-filter": "^1.0.0",
|
"array-filter": "^1.0.0",
|
||||||
"array-map": "^0.0.0",
|
"array-map": "^0.0.0",
|
||||||
"array-reduce": "^0.0.0",
|
"array-reduce": "^0.0.0",
|
||||||
|
|||||||
36
src/App.js
36
src/App.js
@ -1,29 +1,23 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import NotFound from './common/NotFound';
|
import NotFound from './common/NotFound';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => {
|
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => () => (
|
||||||
useEffect(() => {
|
<div className="container-fluid app-container">
|
||||||
loadRealTimeUpdates();
|
<MainHeader />
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
<div className="app">
|
||||||
<div className="container-fluid app-container">
|
<Switch>
|
||||||
<MainHeader />
|
<Route exact path="/" component={Home} />
|
||||||
|
<Route exact path="/settings" component={Settings} />
|
||||||
<div className="app">
|
<Route exact path="/server/create" component={CreateServer} />
|
||||||
<Switch>
|
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
||||||
<Route exact path="/" component={Home} />
|
<Route path="/server/:serverId" component={MenuLayout} />
|
||||||
<Route exact path="/settings" component={Settings} />
|
<Route component={NotFound} />
|
||||||
<Route exact path="/server/create" component={CreateServer} />
|
</Switch>
|
||||||
<Route exact path="/server/:serverId/edit" component={EditServer} />
|
|
||||||
<Route path="/server/:serverId" component={MenuLayout} />
|
|
||||||
<Route component={NotFound} />
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@ -29,7 +29,6 @@ const connect = (propsFromState, actionServiceNames = []) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
|
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
|
||||||
bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ]));
|
|
||||||
|
|
||||||
provideCommonServices(bottle, connect, withRouter);
|
provideCommonServices(bottle, connect, withRouter);
|
||||||
provideShortUrlsServices(bottle, connect);
|
provideShortUrlsServices(bottle, connect);
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
import ReduxThunk from 'redux-thunk';
|
import ReduxThunk from 'redux-thunk';
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
import { applyMiddleware, compose, createStore } from 'redux';
|
||||||
|
import { save, load } from '@shlinkio/redux-localstorage-simple';
|
||||||
import reducers from '../reducers';
|
import reducers from '../reducers';
|
||||||
|
|
||||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||||
: compose;
|
: compose;
|
||||||
|
|
||||||
const store = createStore(reducers, composeEnhancers(
|
const localStorageConfig = {
|
||||||
applyMiddleware(ReduxThunk)
|
states: [ 'settings' ],
|
||||||
|
namespace: 'shlink',
|
||||||
|
namespaceSeparator: '.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
|
||||||
|
applyMiddleware(save(localStorageConfig), ReduxThunk)
|
||||||
));
|
));
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
import PropTypes from 'prop-types';
|
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({
|
export const SettingsType = PropTypes.shape({
|
||||||
realTimeUpdates: PropTypes.shape({
|
realTimeUpdates: PropTypes.shape({
|
||||||
@ -16,20 +16,10 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
[LOAD_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
|
[SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => {
|
export const setRealTimeUpdates = (enabled) => ({
|
||||||
updateSettings({ realTimeUpdates: { enabled } });
|
type: SET_REAL_TIME_UPDATES,
|
||||||
|
realTimeUpdates: { enabled },
|
||||||
return loadRealTimeUpdatesAction();
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const loadRealTimeUpdates = ({ loadSettings }) => () => {
|
|
||||||
const { realTimeUpdates = {} } = loadSettings();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: LOAD_REAL_TIME_UPDATES,
|
|
||||||
realTimeUpdates,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import RealTimeUpdates from '../RealTimeUpdates';
|
import RealTimeUpdates from '../RealTimeUpdates';
|
||||||
import Settings from '../Settings';
|
import Settings from '../Settings';
|
||||||
import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/settings';
|
import { setRealTimeUpdates } from '../reducers/settings';
|
||||||
import SettingsService from './SettingsService';
|
import SettingsService from './SettingsService';
|
||||||
|
|
||||||
const provideServices = (bottle, connect) => {
|
const provideServices = (bottle, connect) => {
|
||||||
@ -14,8 +14,7 @@ const provideServices = (bottle, connect) => {
|
|||||||
bottle.service('SettingsService', SettingsService, 'Storage');
|
bottle.service('SettingsService', SettingsService, 'Storage');
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates');
|
bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
|
||||||
bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|||||||
@ -1,48 +1,19 @@
|
|||||||
import reducer, {
|
import reducer, { SET_REAL_TIME_UPDATES, setRealTimeUpdates } from '../../../src/settings/reducers/settings';
|
||||||
LOAD_REAL_TIME_UPDATES,
|
|
||||||
loadRealTimeUpdates,
|
|
||||||
setRealTimeUpdates,
|
|
||||||
} from '../../../src/settings/reducers/settings';
|
|
||||||
|
|
||||||
describe('settingsReducer', () => {
|
describe('settingsReducer', () => {
|
||||||
const SettingsServiceMock = {
|
|
||||||
updateSettings: jest.fn(),
|
|
||||||
loadSettings: jest.fn(),
|
|
||||||
};
|
|
||||||
const realTimeUpdates = { enabled: true };
|
const realTimeUpdates = { enabled: true };
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns realTimeUpdates when action is LOAD_REAL_TIME_UPDATES', () => {
|
it('returns realTimeUpdates when action is SET_REAL_TIME_UPDATES', () => {
|
||||||
expect(reducer({}, { type: LOAD_REAL_TIME_UPDATES, realTimeUpdates })).toEqual({ realTimeUpdates });
|
expect(reducer({}, { type: SET_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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setRealTimeUpdates', () => {
|
describe('setRealTimeUpdates', () => {
|
||||||
it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
|
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(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
|
||||||
|
|
||||||
expect(SettingsServiceMock.updateSettings).toHaveBeenCalledWith({ realTimeUpdates: { enabled } });
|
|
||||||
expect(loadRealTimeUpdatesAction).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user