diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6cc73f47..0adcc7f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Changed
-* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu
+* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
+* [#255](https://github.com/shlinkio/shlink-web-client/issues/255) Improved how servers and settings are persisted in the local storage.
#### Deprecated
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..5ed710e8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,11 +1,20 @@
import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
import { Route, Switch } from 'react-router-dom';
import NotFound from './common/NotFound';
import './App.scss';
-const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ loadRealTimeUpdates }) => {
+const propTypes = {
+ fetchServers: PropTypes.func,
+ servers: PropTypes.object,
+};
+
+const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) => ({ fetchServers, servers }) => {
+ // On first load, try to fetch the remote servers if the list is empty
useEffect(() => {
- loadRealTimeUpdates();
+ if (Object.keys(servers).length === 0) {
+ fetchServers();
+ }
}, []);
return (
@@ -26,4 +35,6 @@ const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer, Settings) =
);
};
+App.propTypes = propTypes;
+
export default App;
diff --git a/src/common/Home.js b/src/common/Home.js
index 7c7cf396..63fbc80a 100644
--- a/src/common/Home.js
+++ b/src/common/Home.js
@@ -10,9 +10,9 @@ const propTypes = {
servers: PropTypes.object,
};
-const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
- const servers = values(list);
- const hasServers = !isEmpty(servers);
+const Home = ({ resetSelectedServer, servers }) => {
+ const serversList = values(servers);
+ const hasServers = !isEmpty(serversList);
useEffect(() => {
resetSelectedServer();
@@ -21,10 +21,9 @@ const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
return (
Welcome to Shlink
-
- {!loading && hasServers && Please, select a server.}
- {!loading && !hasServers && Please, add a server.}
- {loading && Trying to load servers...}
+
+ {hasServers && Please, select a server.}
+ {!hasServers && Please, add a server.}
);
diff --git a/src/container/index.js b/src/container/index.js
index 95383b13..51309435 100644
--- a/src/container/index.js
+++ b/src/container/index.js
@@ -29,7 +29,7 @@ const connect = (propsFromState, actionServiceNames = []) =>
);
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer', 'Settings');
-bottle.decorator('App', connect(null, [ 'loadRealTimeUpdates' ]));
+bottle.decorator('App', connect([ 'servers' ], [ 'fetchServers' ]));
provideCommonServices(bottle, connect, withRouter);
provideShortUrlsServices(bottle, connect);
diff --git a/src/container/store.js b/src/container/store.js
index 0d5b2b2d..41e9b9ce 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', 'servers' ],
+ namespace: 'shlink',
+ namespaceSeparator: '.',
+};
+
+const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
+ applyMiddleware(save(localStorageConfig), ReduxThunk)
));
export default store;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 8e81e18a..51b95d8c 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -1,5 +1,5 @@
import { combineReducers } from 'redux';
-import serversReducer from '../servers/reducers/server';
+import serversReducer from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
@@ -14,7 +14,7 @@ import tagsListReducer from '../tags/reducers/tagsList';
import tagDeleteReducer from '../tags/reducers/tagDelete';
import tagEditReducer from '../tags/reducers/tagEdit';
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
-import realTimeUpdatesReducer from '../settings/reducers/realTimeUpdates';
+import settingsReducer from '../settings/reducers/settings';
export default combineReducers({
servers: serversReducer,
@@ -32,5 +32,5 @@ export default combineReducers({
tagDelete: tagDeleteReducer,
tagEdit: tagEditReducer,
mercureInfo: mercureInfoReducer,
- realTimeUpdates: realTimeUpdatesReducer,
+ settings: settingsReducer,
});
diff --git a/src/servers/ServersDropdown.js b/src/servers/ServersDropdown.js
index c66cb22c..d997c403 100644
--- a/src/servers/ServersDropdown.js
+++ b/src/servers/ServersDropdown.js
@@ -4,53 +4,50 @@ import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from
import PropTypes from 'prop-types';
import { serverType } from './prop-types';
-const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component {
- static propTypes = {
- servers: PropTypes.object,
- selectedServer: serverType,
- listServers: PropTypes.func,
- history: PropTypes.shape({
- push: PropTypes.func,
- }),
- };
+const propTypes = {
+ servers: PropTypes.object,
+ selectedServer: serverType,
+ history: PropTypes.shape({
+ push: PropTypes.func,
+ }),
+};
- renderServers = () => {
- const { servers: { list, loading }, selectedServer } = this.props;
- const servers = values(list);
- const { push } = this.props.history;
- const loadServer = (id) => push(`/server/${id}/list-short-urls/1`);
+const ServersDropdown = (serversExporter) => {
+ const ServersDropdownComp = ({ servers, selectedServer, history }) => {
+ const serversList = values(servers);
+ const loadServer = (id) => history.push(`/server/${id}/list-short-urls/1`);
- if (loading) {
- return Trying to load servers...;
- }
+ const renderServers = () => {
+ if (isEmpty(serversList)) {
+ return Add a server first...;
+ }
- if (isEmpty(servers)) {
- return Add a server first...;
- }
+ return (
+
+ {serversList.map(({ name, id }) => (
+ loadServer(id)}>
+ {name}
+
+ ))}
+
+ serversExporter.exportServers()}>
+ Export servers
+
+
+ );
+ };
return (
-
- {servers.map(({ name, id }) => (
- loadServer(id)}>
- {name}
-
- ))}
-
- serversExporter.exportServers()}>
- Export servers
-
-
+
+ Servers
+ {renderServers()}
+
);
};
- componentDidMount = this.props.listServers;
+ ServersDropdownComp.propTypes = propTypes;
- render = () => (
-
- Servers
- {this.renderServers()}
-
- );
+ return ServersDropdownComp;
};
export default ServersDropdown;
diff --git a/src/servers/helpers/ServerError.js b/src/servers/helpers/ServerError.js
index 7389d394..14983987 100644
--- a/src/servers/helpers/ServerError.js
+++ b/src/servers/helpers/ServerError.js
@@ -13,7 +13,7 @@ const propTypes = {
};
export const ServerError = (DeleteServerButton) => {
- const ServerErrorComp = ({ type, servers: { list }, selectedServer }) => (
+ const ServerErrorComp = ({ type, servers, selectedServer }) => (
@@ -27,7 +27,7 @@ export const ServerError = (DeleteServerButton) => {
-
+
These are the Shlink servers currently configured. Choose one of
them or add a new one.
diff --git a/src/servers/reducers/remoteServers.js b/src/servers/reducers/remoteServers.js
new file mode 100644
index 00000000..675c36e7
--- /dev/null
+++ b/src/servers/reducers/remoteServers.js
@@ -0,0 +1,22 @@
+import { pipe, prop } from 'ramda';
+import { homepage } from '../../../package.json';
+import { createServers } from './servers';
+
+const responseToServersList = pipe(
+ prop('data'),
+ (value) => {
+ if (!Array.isArray(value)) {
+ throw new Error('Value is not an array');
+ }
+
+ return value;
+ },
+);
+
+export const fetchServers = ({ get }) => () => async (dispatch) => {
+ const remoteList = await get(`${homepage}/servers.json`)
+ .then(responseToServersList)
+ .catch(() => []);
+
+ dispatch(createServers(remoteList));
+};
diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js
index 64d94ae1..835df2a7 100644
--- a/src/servers/reducers/selectedServer.js
+++ b/src/servers/reducers/selectedServer.js
@@ -25,12 +25,15 @@ const getServerVersion = memoizeWith(identity, (serverId, health) => health().th
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
-export const selectServer = ({ findServerById }, buildShlinkApiClient, loadMercureInfo) => (serverId) => async (
- dispatch
+export const selectServer = (buildShlinkApiClient, loadMercureInfo) => (serverId) => async (
+ dispatch,
+ getState
) => {
dispatch(resetSelectedServer());
dispatch(resetShortUrlParams());
- const selectedServer = findServerById(serverId);
+
+ const { servers } = getState();
+ const selectedServer = servers[serverId];
if (!selectedServer) {
dispatch({
diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js
deleted file mode 100644
index e665751c..00000000
--- a/src/servers/reducers/server.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { handleActions } from 'redux-actions';
-import { pipe, isEmpty, assoc, map, prop } from 'ramda';
-import { v4 as uuid } from 'uuid';
-import { homepage } from '../../../package.json';
-
-/* eslint-disable padding-line-between-statements */
-export const FETCH_SERVERS_START = 'shlink/servers/FETCH_SERVERS_START';
-export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
-/* eslint-enable padding-line-between-statements */
-
-const initialState = {
- list: {},
- loading: false,
-};
-
-const assocId = (server) => assoc('id', server.id || uuid(), server);
-
-export default handleActions({
- [FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }),
- [FETCH_SERVERS]: (state, { list }) => ({ list, loading: false }),
-}, initialState);
-
-export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => {
- dispatch({ type: FETCH_SERVERS_START });
- const localList = listServers();
-
- if (!isEmpty(localList)) {
- dispatch({ type: FETCH_SERVERS, list: localList });
-
- return;
- }
-
- // If local list is empty, try to fetch it remotely (making sure it's an array) and calculate IDs for every server
- const getDataAsArrayWithIds = pipe(
- prop('data'),
- (value) => {
- if (!Array.isArray(value)) {
- throw new Error('Value is not an array');
- }
-
- return value;
- },
- map(assocId),
- );
- const remoteList = await get(`${homepage}/servers.json`)
- .then(getDataAsArrayWithIds)
- .catch(() => []);
-
- createServers(remoteList);
- dispatch({ type: FETCH_SERVERS, list: remoteList.reduce((map, server) => ({ ...map, [server.id]: server }), {}) });
-};
-
-export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction);
-
-export const editServer = ({ editServer }, listServersAction) => pipe(editServer, listServersAction);
-
-export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction);
-
-export const createServers = ({ createServers }, listServersAction) => pipe(
- map(assocId),
- createServers,
- listServersAction
-);
diff --git a/src/servers/reducers/servers.js b/src/servers/reducers/servers.js
new file mode 100644
index 00000000..61c60ac4
--- /dev/null
+++ b/src/servers/reducers/servers.js
@@ -0,0 +1,35 @@
+import { handleActions } from 'redux-actions';
+import { pipe, assoc, map, reduce, dissoc } from 'ramda';
+import { v4 as uuid } from 'uuid';
+
+/* eslint-disable padding-line-between-statements */
+export const EDIT_SERVER = 'shlink/servers/EDIT_SERVER';
+export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER';
+export const CREATE_SERVERS = 'shlink/servers/CREATE_SERVERS';
+/* eslint-enable padding-line-between-statements */
+
+const initialState = {};
+
+const assocId = (server) => assoc('id', server.id || uuid(), server);
+
+export default handleActions({
+ [CREATE_SERVERS]: (state, { newServers }) => ({ ...state, ...newServers }),
+ [DELETE_SERVER]: (state, { serverId }) => dissoc(serverId, state),
+ [EDIT_SERVER]: (state, { serverId, serverData }) => !state[serverId]
+ ? state
+ : assoc(serverId, { ...state[serverId], ...serverData }, state),
+}, initialState);
+
+export const createServer = (server) => createServers([ server ]);
+
+const serversListToMap = reduce((acc, server) => assoc(server.id, server, acc), {});
+
+export const createServers = pipe(
+ map(assocId),
+ serversListToMap,
+ (newServers) => ({ type: CREATE_SERVERS, newServers })
+);
+
+export const editServer = (serverId, serverData) => ({ type: EDIT_SERVER, serverId, serverData });
+
+export const deleteServer = ({ id }) => ({ type: DELETE_SERVER, serverId: id });
diff --git a/src/servers/services/ServersExporter.js b/src/servers/services/ServersExporter.js
index 9caf5ab9..01a33316 100644
--- a/src/servers/services/ServersExporter.js
+++ b/src/servers/services/ServersExporter.js
@@ -25,14 +25,14 @@ const saveCsv = (window, csv) => {
};
export default class ServersExporter {
- constructor(serversService, window, csvjson) {
- this.serversService = serversService;
+ constructor(storage, window, csvjson) {
+ this.storage = storage;
this.window = window;
this.csvjson = csvjson;
}
exportServers = async () => {
- const servers = values(this.serversService.listServers()).map(dissoc('id'));
+ const servers = values(this.storage.get('servers') || {}).map(dissoc('id'));
try {
const csv = this.csvjson.toCSV(servers, {
diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js
deleted file mode 100644
index 9ec44afa..00000000
--- a/src/servers/services/ServersService.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { assoc, dissoc, reduce } from 'ramda';
-
-const SERVERS_STORAGE_KEY = 'servers';
-
-export default class ServersService {
- constructor(storage) {
- this.storage = storage;
- }
-
- listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
-
- findServerById = (serverId) => this.listServers()[serverId];
-
- createServer = (server) => this.createServers([ server ]);
-
- createServers = (servers) => {
- const allServers = reduce(
- (serversObj, server) => assoc(server.id, server, serversObj),
- this.listServers(),
- servers
- );
-
- this.storage.set(SERVERS_STORAGE_KEY, allServers);
- };
-
- deleteServer = ({ id }) =>
- this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers()));
-
- editServer = (id, serverData) => {
- const allServers = this.listServers();
-
- if (!allServers[id]) {
- return;
- }
-
- this.storage.set(SERVERS_STORAGE_KEY, assoc(id, { ...allServers[id], ...serverData }, allServers));
- }
-}
diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js
index ce325ffd..a431d363 100644
--- a/src/servers/services/provideServices.js
+++ b/src/servers/services/provideServices.js
@@ -6,11 +6,11 @@ import DeleteServerButton from '../DeleteServerButton';
import { EditServer } from '../EditServer';
import ImportServersBtn from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
-import { createServer, createServers, deleteServer, editServer, listServers } from '../reducers/server';
+import { createServer, createServers, deleteServer, editServer } from '../reducers/servers';
+import { fetchServers } from '../reducers/remoteServers';
import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError';
import ServersImporter from './ServersImporter';
-import ServersService from './ServersService';
import ServersExporter from './ServersExporter';
const provideServices = (bottle, connect, withRouter) => {
@@ -23,7 +23,7 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
bottle.decorator('ServersDropdown', withRouter);
- bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers' ]));
+ bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ]));
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', withRouter);
@@ -43,16 +43,15 @@ const provideServices = (bottle, connect, withRouter) => {
// Services
bottle.constant('csvjson', csvjson);
bottle.service('ServersImporter', ServersImporter, 'csvjson');
- bottle.service('ServersService', ServersService, 'Storage');
- bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
+ bottle.service('ServersExporter', ServersExporter, 'Storage', 'window', 'csvjson');
// Actions
- bottle.serviceFactory('selectServer', selectServer, 'ServersService', 'buildShlinkApiClient', 'loadMercureInfo');
- bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
- bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
- bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
- bottle.serviceFactory('editServer', editServer, 'ServersService', 'listServers');
- bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios');
+ bottle.serviceFactory('selectServer', selectServer, 'buildShlinkApiClient', 'loadMercureInfo');
+ bottle.serviceFactory('createServer', () => createServer);
+ bottle.serviceFactory('createServers', () => createServers);
+ bottle.serviceFactory('deleteServer', () => deleteServer);
+ bottle.serviceFactory('editServer', () => editServer);
+ bottle.serviceFactory('fetchServers', fetchServers, 'axios');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
};
diff --git a/src/settings/RealTimeUpdates.js b/src/settings/RealTimeUpdates.js
index eccd585f..1425a0ac 100644
--- a/src/settings/RealTimeUpdates.js
+++ b/src/settings/RealTimeUpdates.js
@@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import Checkbox from '../utils/Checkbox';
-import { RealTimeUpdatesType } from './reducers/realTimeUpdates';
+import { SettingsType } from './reducers/settings';
const propTypes = {
- realTimeUpdates: RealTimeUpdatesType,
+ settings: SettingsType,
setRealTimeUpdates: PropTypes.func,
};
-const RealTimeUpdates = ({ realTimeUpdates, setRealTimeUpdates }) => (
+const RealTimeUpdates = ({ settings: { realTimeUpdates }, setRealTimeUpdates }) => (
Real-time updates
diff --git a/src/settings/reducers/realTimeUpdates.js b/src/settings/reducers/realTimeUpdates.js
deleted file mode 100644
index c5cd726f..00000000
--- a/src/settings/reducers/realTimeUpdates.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { handleActions } from 'redux-actions';
-import PropTypes from 'prop-types';
-
-export const LOAD_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/LOAD_REAL_TIME_UPDATES';
-
-export const RealTimeUpdatesType = PropTypes.shape({
- enabled: PropTypes.bool.isRequired,
-});
-
-const initialState = {
- enabled: true,
-};
-
-export default handleActions({
- [LOAD_REAL_TIME_UPDATES]: (state, { enabled }) => ({ ...state, enabled }),
-}, initialState);
-
-export const setRealTimeUpdates = ({ updateSettings }, loadRealTimeUpdatesAction) => (enabled) => {
- updateSettings({ realTimeUpdates: { enabled } });
-
- return loadRealTimeUpdatesAction();
-};
-
-export const loadRealTimeUpdates = ({ loadSettings }) => () => {
- const { realTimeUpdates = {} } = loadSettings();
- const { enabled = true } = realTimeUpdates;
-
- return {
- type: LOAD_REAL_TIME_UPDATES,
- enabled,
- };
-};
diff --git a/src/settings/reducers/settings.js b/src/settings/reducers/settings.js
new file mode 100644
index 00000000..6771dbb5
--- /dev/null
+++ b/src/settings/reducers/settings.js
@@ -0,0 +1,25 @@
+import { handleActions } from 'redux-actions';
+import PropTypes from 'prop-types';
+
+export const SET_REAL_TIME_UPDATES = 'shlink/realTimeUpdates/SET_REAL_TIME_UPDATES';
+
+export const SettingsType = PropTypes.shape({
+ realTimeUpdates: PropTypes.shape({
+ enabled: PropTypes.bool.isRequired,
+ }),
+});
+
+const initialState = {
+ realTimeUpdates: {
+ enabled: true,
+ },
+};
+
+export default handleActions({
+ [SET_REAL_TIME_UPDATES]: (state, { realTimeUpdates }) => ({ ...state, realTimeUpdates }),
+}, initialState);
+
+export const setRealTimeUpdates = (enabled) => ({
+ type: SET_REAL_TIME_UPDATES,
+ realTimeUpdates: { enabled },
+});
diff --git a/src/settings/services/SettingsService.js b/src/settings/services/SettingsService.js
deleted file mode 100644
index a3a39446..00000000
--- a/src/settings/services/SettingsService.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const SETTINGS_STORAGE_KEY = 'settings';
-
-export default class SettingsService {
- constructor(storage) {
- this.storage = storage;
- }
-
- loadSettings = () => this.storage.get(SETTINGS_STORAGE_KEY) || {};
-
- updateSettings = (settingsToUpdate) => this.storage.set(SETTINGS_STORAGE_KEY, {
- ...this.loadSettings(),
- ...settingsToUpdate,
- })
-}
diff --git a/src/settings/services/provideServices.js b/src/settings/services/provideServices.js
index ef70b49f..1c9e7863 100644
--- a/src/settings/services/provideServices.js
+++ b/src/settings/services/provideServices.js
@@ -1,21 +1,16 @@
import RealTimeUpdates from '../RealTimeUpdates';
import Settings from '../Settings';
-import { loadRealTimeUpdates, setRealTimeUpdates } from '../reducers/realTimeUpdates';
-import SettingsService from './SettingsService';
+import { setRealTimeUpdates } from '../reducers/settings';
const provideServices = (bottle, connect) => {
// Components
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates');
bottle.serviceFactory('RealTimeUpdates', () => RealTimeUpdates);
- bottle.decorator('RealTimeUpdates', connect([ 'realTimeUpdates' ], [ 'setRealTimeUpdates' ]));
-
- // Services
- bottle.service('SettingsService', SettingsService, 'Storage');
+ bottle.decorator('RealTimeUpdates', connect([ 'settings' ], [ 'setRealTimeUpdates' ]));
// Actions
- bottle.serviceFactory('setRealTimeUpdates', setRealTimeUpdates, 'SettingsService', 'loadRealTimeUpdates');
- bottle.serviceFactory('loadRealTimeUpdates', loadRealTimeUpdates, 'SettingsService');
+ bottle.serviceFactory('setRealTimeUpdates', () => setRealTimeUpdates);
};
export default provideServices;
diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js
index 3b36d459..e0c2f30c 100644
--- a/src/short-urls/ShortUrlsList.js
+++ b/src/short-urls/ShortUrlsList.js
@@ -9,7 +9,7 @@ import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir } from '../utils/utils';
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
import { bindToMercureTopic } from '../mercure/helpers';
-import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates';
+import { SettingsType } from '../settings/reducers/settings';
import { shortUrlType } from './reducers/shortUrlsList';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './ShortUrlsList.scss';
@@ -34,7 +34,7 @@ const propTypes = {
createNewVisit: PropTypes.func,
loadMercureInfo: PropTypes.func,
mercureInfo: MercureInfoType,
- realTimeUpdates: RealTimeUpdatesType,
+ settings: SettingsType,
};
// FIXME Replace with typescript: (ShortUrlsRow component)
@@ -52,7 +52,7 @@ const ShortUrlsList = (ShortUrlsRow) => {
createNewVisit,
loadMercureInfo,
mercureInfo,
- realTimeUpdates,
+ settings: { realTimeUpdates },
}) => {
const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState({
diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js
index 6a35ac3d..02a0d5fa 100644
--- a/src/short-urls/services/provideServices.js
+++ b/src/short-urls/services/provideServices.js
@@ -31,7 +31,7 @@ const provideServices = (bottle, connect) => {
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
bottle.decorator('ShortUrlsList', connect(
- [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'realTimeUpdates' ],
+ [ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'settings' ],
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisit', 'loadMercureInfo' ]
));
diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js
index 6882e478..12f5deb3 100644
--- a/src/visits/ShortUrlVisits.js
+++ b/src/visits/ShortUrlVisits.js
@@ -12,7 +12,7 @@ import { formatDate } from '../utils/helpers/date';
import { useToggle } from '../utils/helpers/hooks';
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
import { bindToMercureTopic } from '../mercure/helpers';
-import { RealTimeUpdatesType } from '../settings/reducers/realTimeUpdates';
+import { SettingsType } from '../settings/reducers/settings';
import SortableBarGraph from './SortableBarGraph';
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
import VisitsHeader from './VisitsHeader';
@@ -39,7 +39,7 @@ const propTypes = {
createNewVisit: PropTypes.func,
loadMercureInfo: PropTypes.func,
mercureInfo: MercureInfoType,
- realTimeUpdates: RealTimeUpdatesType,
+ settings: SettingsType,
};
const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
@@ -68,7 +68,7 @@ const ShortUrlVisits = ({ processStatsFromVisits, normalizeVisits }, OpenMapModa
createNewVisit,
loadMercureInfo,
mercureInfo,
- realTimeUpdates,
+ settings: { realTimeUpdates },
}) => {
const [ startDate, setStartDate ] = useState(undefined);
const [ endDate, setEndDate ] = useState(undefined);
diff --git a/src/visits/services/provideServices.js b/src/visits/services/provideServices.js
index f9f31b80..404bc4a1 100644
--- a/src/visits/services/provideServices.js
+++ b/src/visits/services/provideServices.js
@@ -11,7 +11,7 @@ const provideServices = (bottle, connect) => {
bottle.serviceFactory('MapModal', () => MapModal);
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsParser', 'OpenMapModalBtn');
bottle.decorator('ShortUrlVisits', connect(
- [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'realTimeUpdates' ],
+ [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisit', 'loadMercureInfo' ]
));
diff --git a/test/common/Home.test.js b/test/common/Home.test.js
index fa6e7993..0af589b2 100644
--- a/test/common/Home.test.js
+++ b/test/common/Home.test.js
@@ -6,7 +6,7 @@ describe('', () => {
let wrapped;
const defaultProps = {
resetSelectedServer: jest.fn(),
- servers: { loading: false, list: {} },
+ servers: {},
};
const createComponent = (props) => {
const actualProps = { ...defaultProps, ...props };
@@ -24,20 +24,12 @@ describe('', () => {
expect(wrapped.find('Link')).toHaveLength(1);
});
- it('shows message when loading servers', () => {
- const wrapped = createComponent({ servers: { loading: true } });
- const span = wrapped.find('span');
-
- expect(span).toHaveLength(1);
- expect(span.text()).toContain('Trying to load servers...');
- });
-
- it('Asks to select a server when not loadign and servers exist', () => {
- const list = [
- { name: 'foo', id: '1' },
- { name: 'bar', id: '2' },
- ];
- const wrapped = createComponent({ servers: { list } });
+ it('asks to select a server when servers exist', () => {
+ const servers = {
+ 1: { name: 'foo', id: '1' },
+ 2: { name: 'bar', id: '2' },
+ };
+ const wrapped = createComponent({ servers });
const span = wrapped.find('span');
expect(span).toHaveLength(1);
diff --git a/test/servers/ServersDropdown.test.js b/test/servers/ServersDropdown.test.js
index 2120e695..d90dbba2 100644
--- a/test/servers/ServersDropdown.test.js
+++ b/test/servers/ServersDropdown.test.js
@@ -8,12 +8,9 @@ describe('', () => {
let wrapped;
let ServersDropdown;
const servers = {
- list: {
- '1a': { name: 'foo', id: 1 },
- '2b': { name: 'bar', id: 2 },
- '3c': { name: 'baz', id: 3 },
- },
- loading: false,
+ '1a': { name: 'foo', id: 1 },
+ '2b': { name: 'bar', id: 2 },
+ '3c': { name: 'baz', id: 3 },
};
const history = {
push: jest.fn(),
@@ -26,7 +23,7 @@ describe('', () => {
afterEach(() => wrapped.unmount());
it('contains the list of servers, the divider and the export button', () =>
- expect(wrapped.find(DropdownItem)).toHaveLength(values(servers.list).length + 2));
+ expect(wrapped.find(DropdownItem)).toHaveLength(values(servers).length + 2));
it('contains a toggle with proper title', () =>
expect(wrapped.find(DropdownToggle)).toHaveLength(1));
@@ -40,7 +37,7 @@ describe('', () => {
it('shows a message when no servers exist yet', () => {
wrapped = shallow(
-
+
);
const item = wrapped.find(DropdownItem);
@@ -48,15 +45,4 @@ describe('', () => {
expect(item.prop('disabled')).toEqual(true);
expect(item.find('i').text()).toEqual('Add a server first...');
});
-
- it('shows a message when loading', () => {
- wrapped = shallow(
-
- );
- const item = wrapped.find(DropdownItem);
-
- expect(item).toHaveLength(1);
- expect(item.prop('disabled')).toEqual(true);
- expect(item.find('i').text()).toEqual('Trying to load servers...');
- });
});
diff --git a/test/servers/helpers/ServerError.test.js b/test/servers/helpers/ServerError.test.js
index 32e1871c..01811564 100644
--- a/test/servers/helpers/ServerError.test.js
+++ b/test/servers/helpers/ServerError.test.js
@@ -32,7 +32,7 @@ describe('', () => {
])('renders expected information for type "%s"', (type, textsToFind) => {
wrapper = shallow(
-
+
);
const wrapperText = wrapper.html();
diff --git a/test/servers/reducers/remoteServers.test.js b/test/servers/reducers/remoteServers.test.js
new file mode 100644
index 00000000..ba4afc80
--- /dev/null
+++ b/test/servers/reducers/remoteServers.test.js
@@ -0,0 +1,55 @@
+import { fetchServers } from '../../../src/servers/reducers/remoteServers';
+import { CREATE_SERVERS } from '../../../src/servers/reducers/servers';
+
+describe('remoteServersReducer', () => {
+ afterEach(jest.resetAllMocks);
+
+ describe('fetchServers', () => {
+ const axios = { get: jest.fn() };
+ const dispatch = jest.fn();
+
+ it.each([
+ [
+ Promise.resolve({
+ data: [
+ {
+ id: '111',
+ name: 'acel.me from servers.json',
+ url: 'https://acel.me',
+ apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
+ },
+ {
+ id: '222',
+ name: 'Local from servers.json',
+ url: 'http://localhost:8000',
+ apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
+ },
+ ],
+ }),
+ {
+ 111: {
+ id: '111',
+ name: 'acel.me from servers.json',
+ url: 'https://acel.me',
+ apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
+ },
+ 222: {
+ id: '222',
+ name: 'Local from servers.json',
+ url: 'http://localhost:8000',
+ apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
+ },
+ },
+ ],
+ [ Promise.resolve(''), {}],
+ [ Promise.reject({}), {}],
+ ])('tries to fetch servers from remote', async (mockedValue, expectedList) => {
+ axios.get.mockReturnValue(mockedValue);
+
+ await fetchServers(axios)()(dispatch);
+
+ expect(dispatch).toHaveBeenCalledWith({ type: CREATE_SERVERS, newServers: expectedList });
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js
index 2efd8291..9307841f 100644
--- a/test/servers/reducers/selectedServer.test.js
+++ b/test/servers/reducers/selectedServer.test.js
@@ -32,9 +32,7 @@ describe('selectedServerReducer', () => {
id: 'abc123',
};
const version = '1.19.0';
- const ServersServiceMock = {
- findServerById: jest.fn(() => selectedServer),
- };
+ const createGetStateMock = (id) => jest.fn().mockReturnValue({ servers: { [id]: selectedServer } });
const apiClientMock = {
health: jest.fn(),
};
@@ -49,6 +47,8 @@ describe('selectedServerReducer', () => {
[ 'latest', MAX_FALLBACK_VERSION, 'latest' ],
[ '%invalid_semver%', MIN_FALLBACK_VERSION, '%invalid_semver%' ],
])('dispatches proper actions', async (serverVersion, expectedVersion, expectedPrintableVersion) => {
+ const id = uuid();
+ const getState = createGetStateMock(id);
const expectedSelectedServer = {
...selectedServer,
version: expectedVersion,
@@ -57,7 +57,7 @@ describe('selectedServerReducer', () => {
apiClientMock.health.mockResolvedValue({ version: serverVersion });
- await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
+ await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
expect(dispatch).toHaveBeenCalledTimes(4);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER });
@@ -67,18 +67,23 @@ describe('selectedServerReducer', () => {
});
it('invokes dependencies', async () => {
- await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(() => {});
+ const id = uuid();
+ const getState = createGetStateMock(id);
- expect(ServersServiceMock.findServerById).toHaveBeenCalledTimes(1);
+ await selectServer(buildApiClient, loadMercureInfo)(id)(() => {}, getState);
+
+ expect(getState).toHaveBeenCalledTimes(1);
expect(buildApiClient).toHaveBeenCalledTimes(1);
});
it('dispatches error when health endpoint fails', async () => {
+ const id = uuid();
+ const getState = createGetStateMock(id);
const expectedSelectedServer = { ...selectedServer, serverNotReachable: true };
apiClientMock.health.mockRejectedValue({});
- await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
+ await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
expect(apiClientMock.health).toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
@@ -86,13 +91,13 @@ describe('selectedServerReducer', () => {
});
it('dispatches error when server is not found', async () => {
+ const id = uuid();
+ const getState = jest.fn(() => ({ servers: {} }));
const expectedSelectedServer = { serverNotFound: true };
- ServersServiceMock.findServerById.mockReturnValue(undefined);
+ await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
- await selectServer(ServersServiceMock, buildApiClient, loadMercureInfo)(uuid())(dispatch);
-
- expect(ServersServiceMock.findServerById).toHaveBeenCalled();
+ expect(getState).toHaveBeenCalled();
expect(apiClientMock.health).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(loadMercureInfo).not.toHaveBeenCalled();
diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js
deleted file mode 100644
index baa4e82f..00000000
--- a/test/servers/reducers/server.test.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import { values } from 'ramda';
-import reducer, {
- createServer,
- deleteServer,
- listServers,
- createServers,
- editServer,
- FETCH_SERVERS,
- FETCH_SERVERS_START,
-} from '../../../src/servers/reducers/server';
-
-describe('serverReducer', () => {
- const list = {
- abc123: { id: 'abc123' },
- def456: { id: 'def456' },
- };
- const expectedFetchServersResult = { type: FETCH_SERVERS, list };
- const ServersServiceMock = {
- listServers: jest.fn(() => list),
- createServer: jest.fn(),
- editServer: jest.fn(),
- deleteServer: jest.fn(),
- createServers: jest.fn(),
- };
-
- afterEach(jest.clearAllMocks);
-
- describe('reducer', () => {
- it('returns servers when action is FETCH_SERVERS', () =>
- expect(reducer({}, { type: FETCH_SERVERS, list })).toEqual({ loading: false, list }));
- });
-
- describe('action creators', () => {
- describe('listServers', () => {
- const axios = { get: jest.fn() };
- const dispatch = jest.fn();
- const NoListServersServiceMock = { ...ServersServiceMock, listServers: jest.fn(() => ({})) };
-
- it('fetches servers from local storage when found', async () => {
- await listServers(ServersServiceMock, axios)()(dispatch);
-
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START });
- expect(dispatch).toHaveBeenNthCalledWith(2, expectedFetchServersResult);
- expect(ServersServiceMock.listServers).toHaveBeenCalledTimes(1);
- expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
- expect(axios.get).not.toHaveBeenCalled();
- });
-
- it.each([
- [
- Promise.resolve({
- data: [
- {
- id: '111',
- name: 'acel.me from servers.json',
- url: 'https://acel.me',
- apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
- },
- {
- id: '222',
- name: 'Local from servers.json',
- url: 'http://localhost:8000',
- apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
- },
- ],
- }),
- {
- 111: {
- id: '111',
- name: 'acel.me from servers.json',
- url: 'https://acel.me',
- apiKey: '07fb8a96-8059-4094-a24c-80a7d5e7e9b0',
- },
- 222: {
- id: '222',
- name: 'Local from servers.json',
- url: 'http://localhost:8000',
- apiKey: '7a531c75-134e-4d5c-86e0-a71b7167b57a',
- },
- },
- ],
- [ Promise.resolve(''), {}],
- [ Promise.reject({}), {}],
- ])('tries to fetch servers from remote when not found locally', async (mockedValue, expectedList) => {
- axios.get.mockReturnValue(mockedValue);
-
- await listServers(NoListServersServiceMock, axios)()(dispatch);
-
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenNthCalledWith(1, { type: FETCH_SERVERS_START });
- expect(dispatch).toHaveBeenNthCalledWith(2, { type: FETCH_SERVERS, list: expectedList });
- expect(NoListServersServiceMock.listServers).toHaveBeenCalledTimes(1);
- expect(NoListServersServiceMock.createServer).not.toHaveBeenCalled();
- expect(NoListServersServiceMock.editServer).not.toHaveBeenCalled();
- expect(NoListServersServiceMock.deleteServer).not.toHaveBeenCalled();
- expect(NoListServersServiceMock.createServers).toHaveBeenCalledTimes(1);
- expect(axios.get).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('createServer', () => {
- it('adds new server and then fetches servers again', () => {
- const serverToCreate = { id: 'abc123' };
- const result = createServer(ServersServiceMock, () => expectedFetchServersResult)(serverToCreate);
-
- expect(result).toEqual(expectedFetchServersResult);
- expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServer).toHaveBeenCalledTimes(1);
- expect(ServersServiceMock.createServer).toHaveBeenCalledWith(serverToCreate);
- expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
- });
- });
-
- describe('editServer', () => {
- it('edits existing server and then fetches servers again', () => {
- const serverToEdit = { name: 'edited' };
- const result = editServer(ServersServiceMock, () => expectedFetchServersResult)('123', serverToEdit);
-
- expect(result).toEqual(expectedFetchServersResult);
- expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.editServer).toHaveBeenCalledTimes(1);
- expect(ServersServiceMock.editServer).toHaveBeenCalledWith('123', serverToEdit);
- expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
- });
- });
-
- describe('deleteServer', () => {
- it('deletes a server and then fetches servers again', () => {
- const serverToDelete = { id: 'abc123' };
- const result = deleteServer(ServersServiceMock, () => expectedFetchServersResult)(serverToDelete);
-
- expect(result).toEqual(expectedFetchServersResult);
- expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServers).not.toHaveBeenCalled();
- expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.deleteServer).toHaveBeenCalledTimes(1);
- expect(ServersServiceMock.deleteServer).toHaveBeenCalledWith(serverToDelete);
- });
- });
-
- describe('createServers', () => {
- it('creates multiple servers and then fetches servers again', () => {
- const serversToCreate = values(list);
- const result = createServers(ServersServiceMock, () => expectedFetchServersResult)(serversToCreate);
-
- expect(result).toEqual(expectedFetchServersResult);
- expect(ServersServiceMock.listServers).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.editServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.deleteServer).not.toHaveBeenCalled();
- expect(ServersServiceMock.createServers).toHaveBeenCalledTimes(1);
- expect(ServersServiceMock.createServers).toHaveBeenCalledWith(serversToCreate);
- });
- });
- });
-});
diff --git a/test/servers/reducers/servers.test.js b/test/servers/reducers/servers.test.js
new file mode 100644
index 00000000..319e18d2
--- /dev/null
+++ b/test/servers/reducers/servers.test.js
@@ -0,0 +1,85 @@
+import { values } from 'ramda';
+import reducer, {
+ createServer,
+ deleteServer,
+ createServers,
+ editServer,
+ EDIT_SERVER,
+ DELETE_SERVER,
+ CREATE_SERVERS,
+} from '../../../src/servers/reducers/servers';
+
+describe('serverReducer', () => {
+ const list = {
+ abc123: { id: 'abc123' },
+ def456: { id: 'def456' },
+ };
+
+ afterEach(jest.clearAllMocks);
+
+ describe('reducer', () => {
+ it('returns edited server when action is EDIT_SERVER', () =>
+ expect(reducer(
+ list,
+ { type: EDIT_SERVER, serverId: 'abc123', serverData: { foo: 'foo' } },
+ )).toEqual({
+ abc123: { id: 'abc123', foo: 'foo' },
+ def456: { id: 'def456' },
+ }));
+
+ it('removes server when action is DELETE_SERVER', () =>
+ expect(reducer(list, { type: DELETE_SERVER, serverId: 'abc123' })).toEqual({
+ def456: { id: 'def456' },
+ }));
+
+ it('appends server when action is CREATE_SERVERS', () =>
+ expect(reducer(list, {
+ type: CREATE_SERVERS,
+ newServers: {
+ ghi789: { id: 'ghi789' },
+ },
+ })).toEqual({
+ abc123: { id: 'abc123' },
+ def456: { id: 'def456' },
+ ghi789: { id: 'ghi789' },
+ }));
+ });
+
+ describe('action creators', () => {
+ describe('createServer', () => {
+ it('returns expected action', () => {
+ const serverToCreate = { id: 'abc123' };
+ const result = createServer(serverToCreate);
+
+ expect(result).toEqual(expect.objectContaining({ type: CREATE_SERVERS }));
+ });
+ });
+
+ describe('editServer', () => {
+ it('returns expected action', () => {
+ const serverData = { name: 'edited' };
+ const result = editServer('123', serverData);
+
+ expect(result).toEqual({ type: EDIT_SERVER, serverId: '123', serverData });
+ });
+ });
+
+ describe('deleteServer', () => {
+ it('returns expected action', () => {
+ const serverToDelete = { id: 'abc123' };
+ const result = deleteServer(serverToDelete);
+
+ expect(result).toEqual({ type: DELETE_SERVER, serverId: 'abc123' });
+ });
+ });
+
+ describe('createServers', () => {
+ it('returns expected action', () => {
+ const newServers = values(list);
+ const result = createServers(newServers);
+
+ expect(result).toEqual(expect.objectContaining({ type: CREATE_SERVERS }));
+ });
+ });
+ });
+});
diff --git a/test/servers/services/ServersExporter.test.js b/test/servers/services/ServersExporter.test.js
index 772df918..1dc2156e 100644
--- a/test/servers/services/ServersExporter.test.js
+++ b/test/servers/services/ServersExporter.test.js
@@ -18,8 +18,8 @@ describe('ServersExporter', () => {
},
},
});
- const serversServiceMock = {
- listServers: jest.fn(() => ({
+ const storageMock = {
+ get: jest.fn(() => ({
abc123: {
id: 'abc123',
name: 'foo',
@@ -48,19 +48,15 @@ describe('ServersExporter', () => {
global.console = { error: jest.fn() };
global.Blob = class Blob {};
global.URL = { createObjectURL: () => '' };
- serversServiceMock.listServers.mockReset();
});
afterEach(() => {
global.console = originalConsole;
+ jest.clearAllMocks();
});
it('logs an error if something fails', () => {
const csvjsonMock = createCsvjsonMock(true);
- const exporter = new ServersExporter(
- serversServiceMock,
- createWindowMock(),
- csvjsonMock,
- );
+ const exporter = new ServersExporter(storageMock, createWindowMock(), csvjsonMock);
exporter.exportServers();
@@ -70,30 +66,22 @@ describe('ServersExporter', () => {
it('makes use of msSaveBlob API when available', () => {
const windowMock = createWindowMock();
- const exporter = new ServersExporter(
- serversServiceMock,
- windowMock,
- createCsvjsonMock(),
- );
+ const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
exporter.exportServers();
- expect(serversServiceMock.listServers).toHaveBeenCalledTimes(1);
+ expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.navigator.msSaveBlob).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).not.toHaveBeenCalled();
});
it('makes use of download link API when available', () => {
const windowMock = createWindowMock(false);
- const exporter = new ServersExporter(
- serversServiceMock,
- windowMock,
- createCsvjsonMock(),
- );
+ const exporter = new ServersExporter(storageMock, windowMock, createCsvjsonMock());
exporter.exportServers();
- expect(serversServiceMock.listServers).toHaveBeenCalledTimes(1);
+ expect(storageMock.get).toHaveBeenCalledTimes(1);
expect(windowMock.document.createElement).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.appendChild).toHaveBeenCalledTimes(1);
expect(windowMock.document.body.removeChild).toHaveBeenCalledTimes(1);
diff --git a/test/servers/services/ServersService.test.js b/test/servers/services/ServersService.test.js
deleted file mode 100644
index 3c8b805f..00000000
--- a/test/servers/services/ServersService.test.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import ServersService from '../../../src/servers/services/ServersService';
-
-describe('ServersService', () => {
- const servers = {
- abc123: { id: 'abc123' },
- def456: { id: 'def456' },
- };
- const createService = (withServers = true) => {
- const storageMock = {
- set: jest.fn(),
- get: jest.fn(() => withServers ? servers : undefined),
- };
- const service = new ServersService(storageMock);
-
- return [ service, storageMock ];
- };
-
- describe('listServers', () => {
- it('returns an empty object when servers are not found in storage', () => {
- const [ service, storageMock ] = createService(false);
-
- const result = service.listServers();
-
- expect(result).toEqual({});
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).not.toHaveBeenCalled();
- });
-
- it('returns value from storage when found', () => {
- const [ service, storageMock ] = createService();
-
- const result = service.listServers();
-
- expect(result).toEqual(servers);
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).not.toHaveBeenCalled();
- });
- });
-
- describe('findServerById', () => {
- it('returns undefined when requested server is not found', () => {
- const [ service, storageMock ] = createService();
-
- const result = service.findServerById('ghi789');
-
- expect(result).toBeUndefined();
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).not.toHaveBeenCalled();
- });
-
- it('returns server from list when found', () => {
- const [ service, storageMock ] = createService();
-
- const result = service.findServerById('abc123');
-
- expect(result).toEqual({ id: 'abc123' });
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).not.toHaveBeenCalled();
- });
- });
-
- describe('createServer', () => {
- it('adds one server to the list', () => {
- const [ service, storageMock ] = createService();
-
- service.createServer({ id: 'ghi789' });
-
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
- abc123: { id: 'abc123' },
- def456: { id: 'def456' },
- ghi789: { id: 'ghi789' },
- });
- });
- });
-
- describe('createServers', () => {
- it('adds multiple servers to the list', () => {
- const [ service, storageMock ] = createService();
-
- service.createServers([{ id: 'ghi789' }, { id: 'jkl123' }]);
-
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
- abc123: { id: 'abc123' },
- def456: { id: 'def456' },
- ghi789: { id: 'ghi789' },
- jkl123: { id: 'jkl123' },
- });
- });
- });
-
- describe('deleteServer', () => {
- it('removes one server from the list', () => {
- const [ service, storageMock ] = createService();
-
- service.deleteServer({ id: 'abc123' });
-
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
- def456: { id: 'def456' },
- });
- });
- });
-
- describe('editServer', () => {
- it('dos nothing is provided server does not exist', () => {
- const [ service, storageMock ] = createService();
-
- service.editServer('notFound', {});
-
- expect(storageMock.set).not.toHaveBeenCalled();
- });
-
- it('updates the list with provided server data', () => {
- const [ service, storageMock ] = createService();
- const serverData = { name: 'foo', apiKey: 'bar' };
-
- service.editServer('abc123', serverData);
-
- expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), {
- abc123: { id: 'abc123', ...serverData },
- def456: { id: 'def456' },
- });
- });
- });
-});
diff --git a/test/settings/reducers/realTimeUpdates.test.js b/test/settings/reducers/realTimeUpdates.test.js
deleted file mode 100644
index cdd463e6..00000000
--- a/test/settings/reducers/realTimeUpdates.test.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import reducer, {
- LOAD_REAL_TIME_UPDATES,
- loadRealTimeUpdates,
- setRealTimeUpdates,
-} from '../../../src/settings/reducers/realTimeUpdates';
-
-describe('realTimeUpdatesReducer', () => {
- const SettingsServiceMock = {
- updateSettings: jest.fn(),
- loadSettings: jest.fn(),
- };
-
- afterEach(jest.clearAllMocks);
-
- describe('reducer', () => {
- it('returns realTimeUpdates when action is LOAD_REAL_TIME_UPDATES', () => {
- expect(reducer({}, { type: LOAD_REAL_TIME_UPDATES, enabled: true })).toEqual({ enabled: true });
- });
- });
-
- describe('loadRealTimeUpdates', () => {
- it.each([
- [{}, true ],
- [{ realTimeUpdates: {} }, true ],
- [{ realTimeUpdates: { enabled: true } }, true ],
- [{ realTimeUpdates: { enabled: false } }, false ],
- ])('loads settings and returns LOAD_REAL_TIME_UPDATES action', (loadedSettings, expectedEnabled) => {
- SettingsServiceMock.loadSettings.mockReturnValue(loadedSettings);
-
- const result = loadRealTimeUpdates(SettingsServiceMock)();
-
- expect(result).toEqual({ type: LOAD_REAL_TIME_UPDATES, enabled: expectedEnabled });
- expect(SettingsServiceMock.loadSettings).toHaveBeenCalled();
- });
- });
-
- describe('setRealTimeUpdates', () => {
- it.each([[ true ], [ false ]])('updates settings with provided value and then loads updates again', (enabled) => {
- const loadRealTimeUpdatesAction = jest.fn();
-
- setRealTimeUpdates(SettingsServiceMock, loadRealTimeUpdatesAction)(enabled);
-
- expect(SettingsServiceMock.updateSettings).toHaveBeenCalledWith({ realTimeUpdates: { enabled } });
- expect(loadRealTimeUpdatesAction).toHaveBeenCalled();
- });
- });
-});
diff --git a/test/settings/reducers/settings.test.js b/test/settings/reducers/settings.test.js
new file mode 100644
index 00000000..80f22c49
--- /dev/null
+++ b/test/settings/reducers/settings.test.js
@@ -0,0 +1,19 @@
+import reducer, { SET_REAL_TIME_UPDATES, setRealTimeUpdates } from '../../../src/settings/reducers/settings';
+
+describe('settingsReducer', () => {
+ const realTimeUpdates = { enabled: true };
+
+ describe('reducer', () => {
+ 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 result = setRealTimeUpdates(enabled);
+
+ expect(result).toEqual({ type: SET_REAL_TIME_UPDATES, realTimeUpdates: { enabled } });
+ });
+ });
+});
diff --git a/test/settings/services/SettingsService.test.js b/test/settings/services/SettingsService.test.js
deleted file mode 100644
index 9e9419db..00000000
--- a/test/settings/services/SettingsService.test.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import SettingsService from '../../../src/settings/services/SettingsService';
-
-describe('SettingsService', () => {
- const settings = { foo: 'bar' };
- const createService = (withSettings = true) => {
- const storageMock = {
- set: jest.fn(),
- get: jest.fn(() => withSettings ? settings : undefined),
- };
- const service = new SettingsService(storageMock);
-
- return [ service, storageMock ];
- };
-
- afterEach(jest.resetAllMocks);
-
- describe('loadSettings', () => {
- it.each([
- [ false, {}],
- [ true, settings ],
- ])('returns result if found in storage', (withSettings, expectedResult) => {
- const [ service, storageMock ] = createService(withSettings);
-
- const result = service.loadSettings();
-
- expect(result).toEqual(expectedResult);
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).not.toHaveBeenCalled();
- });
- });
-
- describe('updateSettings', () => {
- it.each([
- [ false, { hi: 'goodbye' }, { hi: 'goodbye' }],
- [ true, { hi: 'goodbye' }, { foo: 'bar', hi: 'goodbye' }],
- [ true, { foo: 'goodbye' }, { foo: 'goodbye' }],
- ])('appends provided data to existing settings', (withSettings, settingsToUpdate, expectedResult) => {
- const [ service, storageMock ] = createService(withSettings);
-
- service.updateSettings(settingsToUpdate);
-
- expect(storageMock.get).toHaveBeenCalledTimes(1);
- expect(storageMock.set).toHaveBeenCalledWith(expect.anything(), expectedResult);
- });
- });
-});
diff --git a/test/short-urls/ShortUrlsList.test.js b/test/short-urls/ShortUrlsList.test.js
index 050238b9..b66e9ac0 100644
--- a/test/short-urls/ShortUrlsList.test.js
+++ b/test/short-urls/ShortUrlsList.test.js
@@ -9,6 +9,7 @@ describe('', () => {
const ShortUrlsRow = () => '';
const listShortUrlsMock = jest.fn();
const resetShortUrlParamsMock = jest.fn();
+ const realTimeUpdates = { enabled: true };
const ShortUrlsList = shortUrlsListCreator(ShortUrlsRow);
@@ -37,6 +38,7 @@ describe('', () => {
]
}
mercureInfo={{ loading: true }}
+ settings={{ realTimeUpdates }}
/>
);
});
diff --git a/test/visits/ShortUrlVisits.test.js b/test/visits/ShortUrlVisits.test.js
index 0f221308..76cf0b28 100644
--- a/test/visits/ShortUrlVisits.test.js
+++ b/test/visits/ShortUrlVisits.test.js
@@ -21,6 +21,7 @@ describe('', () => {
const history = {
goBack: jest.fn(),
};
+ const realTimeUpdates = { enabled: true };
const createComponent = (shortUrlVisits) => {
const ShortUrlVisits = createShortUrlVisits({ processStatsFromVisits, normalizeVisits: identity }, () => '');
@@ -36,6 +37,7 @@ describe('', () => {
shortUrlDetail={{}}
cancelGetShortUrlVisits={identity}
matchMedia={() => ({ matches: false })}
+ settings={{ realTimeUpdates }}
/>
);