From 79a518b02d340f8e6decb2f6e941e34fdb62a974 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 17 Dec 2018 20:03:36 +0100 Subject: [PATCH 01/18] Registered first components as services --- package.json | 1 + src/App.js | 28 +++++++++++++--------------- src/common/Home.js | 10 ++-------- src/common/MainHeader.js | 8 +++----- src/common/ScrollToTop.js | 9 ++------- src/container/index.js | 20 ++++++++++++++++++++ src/container/store.js | 13 +++++++++++++ src/index.js | 15 +++------------ yarn.lock | 4 ++++ 9 files changed, 61 insertions(+), 47 deletions(-) create mode 100644 src/container/index.js create mode 100644 src/container/store.js diff --git a/package.json b/package.json index bd22d155..21c36f8d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@fortawesome/react-fontawesome": "0.0.19", "axios": "^0.18.0", "bootstrap": "^4.1.1", + "bottlejs": "^1.7.1", "chart.js": "^2.7.2", "classnames": "^2.2.6", "csvjson": "^5.1.0", diff --git a/src/App.js b/src/App.js index f7203923..dce4f6e5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,23 +1,21 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import './App.scss'; -import Home from './common/Home'; -import MainHeader from './common/MainHeader'; import MenuLayout from './common/MenuLayout'; import CreateServer from './servers/CreateServer'; -export default function App() { - return ( -
- +const App = (MainHeader, Home) => () => ( +
+ -
- - - - - -
+
+ + + + +
- ); -} +
+); + +export default App; diff --git a/src/common/Home.js b/src/common/Home.js index a3293d49..7acf4959 100644 --- a/src/common/Home.js +++ b/src/common/Home.js @@ -1,15 +1,13 @@ import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; -import { isEmpty, pick, values } from 'ramda'; +import { isEmpty, values } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { ListGroup, ListGroupItem } from 'reactstrap'; import PropTypes from 'prop-types'; -import { resetSelectedServer } from '../servers/reducers/selectedServer'; import './Home.scss'; -export class HomeComponent extends React.Component { +export default class Home extends React.Component { static propTypes = { resetSelectedServer: PropTypes.func, servers: PropTypes.object, @@ -50,7 +48,3 @@ export class HomeComponent extends React.Component { ); } } - -const Home = connect(pick([ 'servers' ]), { resetSelectedServer })(HomeComponent); - -export default Home; diff --git a/src/common/MainHeader.js b/src/common/MainHeader.js index 76836714..aa95cb06 100644 --- a/src/common/MainHeader.js +++ b/src/common/MainHeader.js @@ -2,7 +2,7 @@ import plusIcon from '@fortawesome/fontawesome-free-solid/faPlus'; import arrowIcon from '@fortawesome/fontawesome-free-solid/faChevronDown'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; -import { Link, withRouter } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; import classnames from 'classnames'; import PropTypes from 'prop-types'; @@ -10,7 +10,7 @@ import ServersDropdown from '../servers/ServersDropdown'; import './MainHeader.scss'; import shlinkLogo from './shlink-logo-white.png'; -export class MainHeaderComponent extends React.Component { +const MainHeader = () => class MainHeaderComponent extends React.Component { static propTypes = { location: PropTypes.object, }; @@ -62,8 +62,6 @@ export class MainHeaderComponent extends React.Component { ); } -} - -const MainHeader = withRouter(MainHeaderComponent); +}; export default MainHeader; diff --git a/src/common/ScrollToTop.js b/src/common/ScrollToTop.js index 789f1a33..c8624da0 100644 --- a/src/common/ScrollToTop.js +++ b/src/common/ScrollToTop.js @@ -1,8 +1,7 @@ import React from 'react'; -import { withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; -export class ScrollToTopComponent extends React.Component { +export default class ScrollToTop extends React.Component { static propTypes = { location: PropTypes.object, window: PropTypes.shape({ @@ -11,7 +10,7 @@ export class ScrollToTopComponent extends React.Component { children: PropTypes.node, }; static defaultProps = { - window, + window: global.window, }; componentDidUpdate(prevProps) { @@ -26,7 +25,3 @@ export class ScrollToTopComponent extends React.Component { return this.props.children; } } - -const ScrollToTop = withRouter(ScrollToTopComponent); - -export default ScrollToTop; diff --git a/src/container/index.js b/src/container/index.js new file mode 100644 index 00000000..2009c749 --- /dev/null +++ b/src/container/index.js @@ -0,0 +1,20 @@ +import Bottle from 'bottlejs'; +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { pick } from 'ramda'; +import App from '../App'; +import ScrollToTop from '../common/ScrollToTop'; +import MainHeader from '../common/MainHeader'; +import { resetSelectedServer } from '../servers/reducers/selectedServer'; +import Home from '../common/Home'; +import store from './store'; + +const bottle = new Bottle(); + +bottle.constant('store', store); +bottle.serviceFactory('App', App, 'MainHeader', 'Home'); +bottle.serviceFactory('MainHeader', () => withRouter(MainHeader())); +bottle.serviceFactory('ScrollToTop', () => withRouter(ScrollToTop)); +bottle.serviceFactory('Home', () => connect(pick([ 'servers' ]), { resetSelectedServer })(Home)); + +export default bottle.container; diff --git a/src/container/store.js b/src/container/store.js new file mode 100644 index 00000000..0d5b2b2d --- /dev/null +++ b/src/container/store.js @@ -0,0 +1,13 @@ +import ReduxThunk from 'redux-thunk'; +import { applyMiddleware, compose, createStore } from 'redux'; +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) +)); + +export default store; diff --git a/src/index.js b/src/index.js index c0a4a375..62c93f92 100644 --- a/src/index.js +++ b/src/index.js @@ -3,23 +3,14 @@ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; -import { applyMiddleware, compose, createStore } from 'redux'; -import ReduxThunk from 'redux-thunk'; import { homepage } from '../package.json'; -import App from './App'; -import './index.scss'; -import ScrollToTop from './common/ScrollToTop'; -import reducers from './reducers'; import registerServiceWorker from './registerServiceWorker'; +import container from './container'; import '../node_modules/react-datepicker/dist/react-datepicker.css'; import './common/react-tagsinput.scss'; +import './index.scss'; -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 { App, ScrollToTop, store } = container; render( diff --git a/yarn.lock b/yarn.lock index c0d1c02c..14d4ca64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1316,6 +1316,10 @@ bootstrap@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be" +bottlejs@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/bottlejs/-/bottlejs-1.7.1.tgz#f2673c42feb2ba092d94b8add390e66b3f7d948f" + boxen@1.3.0, boxen@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" From 5e6ad14a85bba23b9478d3a179e24ec512faced2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 17 Dec 2018 20:24:31 +0100 Subject: [PATCH 02/18] More components migrated for dependency injection --- src/App.js | 4 +--- src/common/MenuLayout.js | 15 ++---------- src/container/index.js | 24 +++++++++++++++++--- src/servers/CreateServer.js | 14 ++---------- src/servers/reducers/selectedServer.js | 5 ++-- test/App.test.js | 7 ++++-- test/common/Home.test.js | 4 ++-- test/servers/CreateServer.test.js | 8 ++----- test/servers/reducers/selectedServer.test.js | 4 ++-- 9 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/App.js b/src/App.js index dce4f6e5..b351422c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,8 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import './App.scss'; -import MenuLayout from './common/MenuLayout'; -import CreateServer from './servers/CreateServer'; -const App = (MainHeader, Home) => () => ( +const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 60e33a31..e2034104 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -1,15 +1,11 @@ import React from 'react'; -import { Route, Switch, withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { pick } from 'ramda'; +import { Route, Switch } from 'react-router-dom'; import Swipeable from 'react-swipeable'; import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import * as PropTypes from 'prop-types'; import ShortUrlsVisits from '../visits/ShortUrlVisits'; -import { selectServer } from '../servers/reducers/selectedServer'; import CreateShortUrl from '../short-urls/CreateShortUrl'; import ShortUrls from '../short-urls/ShortUrls'; import './MenuLayout.scss'; @@ -17,7 +13,7 @@ import TagsList from '../tags/TagsList'; import { serverType } from '../servers/prop-types'; import AsideMenu from './AsideMenu'; -export class MenuLayoutComponent extends React.Component { +export default class MenuLayout extends React.Component { static propTypes = { match: PropTypes.object, selectServer: PropTypes.func, @@ -104,10 +100,3 @@ export class MenuLayoutComponent extends React.Component { ); } } - -const MenuLayout = compose( - connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }), - withRouter -)(MenuLayoutComponent); - -export default MenuLayout; diff --git a/src/container/index.js b/src/container/index.js index 2009c749..faf0ee7a 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -1,20 +1,38 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; +import { compose } from 'redux'; import { pick } from 'ramda'; import App from '../App'; import ScrollToTop from '../common/ScrollToTop'; import MainHeader from '../common/MainHeader'; -import { resetSelectedServer } from '../servers/reducers/selectedServer'; +import { resetSelectedServer, selectServer } from '../servers/reducers/selectedServer'; import Home from '../common/Home'; +import MenuLayout from '../common/MenuLayout'; +import { createServer } from '../servers/reducers/server'; +import CreateServer from '../servers/CreateServer'; import store from './store'; const bottle = new Bottle(); bottle.constant('store', store); -bottle.serviceFactory('App', App, 'MainHeader', 'Home'); -bottle.serviceFactory('MainHeader', () => withRouter(MainHeader())); bottle.serviceFactory('ScrollToTop', () => withRouter(ScrollToTop)); +bottle.serviceFactory('MainHeader', () => withRouter(MainHeader())); bottle.serviceFactory('Home', () => connect(pick([ 'servers' ]), { resetSelectedServer })(Home)); +bottle.serviceFactory( + 'MenuLayout', + () => compose( + connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }), + withRouter + )(MenuLayout) +); +bottle.serviceFactory( + 'CreateServer', + () => connect( + pick([ 'selectedServer' ]), + { createServer, resetSelectedServer } + )(CreateServer) +); +bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); export default bottle.container; diff --git a/src/servers/CreateServer.js b/src/servers/CreateServer.js index 3875ba66..4a601d26 100644 --- a/src/servers/CreateServer.js +++ b/src/servers/CreateServer.js @@ -1,17 +1,14 @@ -import { assoc, dissoc, pick, pipe } from 'ramda'; +import { assoc, dissoc, pipe } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import { v4 as uuid } from 'uuid'; import PropTypes from 'prop-types'; import { stateFlagTimeout } from '../utils/utils'; -import { resetSelectedServer } from './reducers/selectedServer'; -import { createServer } from './reducers/server'; import './CreateServer.scss'; import ImportServersBtn from './helpers/ImportServersBtn'; const SHOW_IMPORT_MSG_TIME = 4000; -export class CreateServerComponent extends React.Component { +export default class CreateServer extends React.Component { static propTypes = { createServer: PropTypes.func, history: PropTypes.shape({ @@ -92,10 +89,3 @@ export class CreateServerComponent extends React.Component { ); } } - -const CreateServer = connect( - pick([ 'selectedServer' ]), - { createServer, resetSelectedServer } -)(CreateServerComponent); - -export default CreateServer; diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 891f5ce2..eca53474 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,4 +1,3 @@ -import { curry } from 'ramda'; import shlinkApiClient from '../../api/ShlinkApiClient'; import serversService from '../../servers/services/ServersService'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; @@ -23,7 +22,7 @@ export default function reducer(state = defaultState, action) { export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); -export const _selectServer = (shlinkApiClient, serversService, serverId) => (dispatch) => { +export const _selectServer = (shlinkApiClient, serversService) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); const selectedServer = serversService.findServerById(serverId); @@ -36,4 +35,4 @@ export const _selectServer = (shlinkApiClient, serversService, serverId) => (dis }); }; -export const selectServer = curry(_selectServer)(shlinkApiClient, serversService); +export const selectServer = _selectServer(shlinkApiClient, serversService); diff --git a/test/App.test.js b/test/App.test.js index 63b6cdc8..1a112ae7 100644 --- a/test/App.test.js +++ b/test/App.test.js @@ -1,13 +1,16 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Route } from 'react-router-dom'; -import App from '../src/App'; -import MainHeader from '../src/common/MainHeader'; +import { identity } from 'ramda'; +import appFactory from '../src/App'; describe('', () => { let wrapper; + const MainHeader = () => ''; beforeEach(() => { + const App = appFactory(MainHeader, identity, identity, identity); + wrapper = shallow(); }); afterEach(() => wrapper.unmount()); diff --git a/test/common/Home.test.js b/test/common/Home.test.js index d44d9b0e..83e01ba2 100644 --- a/test/common/Home.test.js +++ b/test/common/Home.test.js @@ -2,7 +2,7 @@ import { shallow } from 'enzyme'; import { values } from 'ramda'; import React from 'react'; import * as sinon from 'sinon'; -import { HomeComponent } from '../../src/common/Home'; +import Home from '../../src/common/Home'; describe('', () => { let wrapped; @@ -15,7 +15,7 @@ describe('', () => { const createComponent = (props) => { const actualProps = { ...defaultProps, ...props }; - wrapped = shallow(); + wrapped = shallow(); return wrapped; }; diff --git a/test/servers/CreateServer.test.js b/test/servers/CreateServer.test.js index ecfde7ce..c9ce859f 100644 --- a/test/servers/CreateServer.test.js +++ b/test/servers/CreateServer.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { identity } from 'ramda'; import sinon from 'sinon'; -import { CreateServerComponent } from '../../src/servers/CreateServer'; +import CreateServer from '../../src/servers/CreateServer'; import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn'; describe('', () => { @@ -17,11 +17,7 @@ describe('', () => { historyMock.push.resetHistory(); wrapper = shallow( - + ); }); afterEach(() => wrapper.unmount()); diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index e8cf174f..442172da 100644 --- a/test/servers/reducers/selectedServer.test.js +++ b/test/servers/reducers/selectedServer.test.js @@ -49,7 +49,7 @@ describe('selectedServerReducer', () => { const dispatch = sinon.spy(); const expectedDispatchCalls = 2; - _selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(dispatch); + _selectServer(ShlinkApiClientMock, ServersServiceMock)(serverId)(dispatch); expect(dispatch.callCount).toEqual(expectedDispatchCalls); expect(dispatch.firstCall.calledWith({ type: RESET_SHORT_URL_PARAMS })).toEqual(true); @@ -60,7 +60,7 @@ describe('selectedServerReducer', () => { }); it('invokes dependencies', () => { - _selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(() => {}); + _selectServer(ShlinkApiClientMock, ServersServiceMock)(serverId)(() => {}); expect(ShlinkApiClientMock.setConfig.callCount).toEqual(1); expect(ServersServiceMock.findServerById.callCount).toEqual(1); From 5616d045ab7c0515067fe956f518e60611767def Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 17 Dec 2018 22:18:47 +0100 Subject: [PATCH 03/18] Migrated a lot more components to new DI system --- src/common/AsideMenu.js | 97 ++++++++++--------- src/common/MainHeader.js | 5 +- src/common/MenuLayout.js | 9 +- src/container/index.js | 107 +++++++++++++++++---- src/index.js | 3 +- src/servers/CreateServer.js | 7 +- src/servers/DeleteServerButton.js | 7 +- src/servers/DeleteServerModal.js | 13 +-- src/servers/ServersDropdown.js | 23 +---- src/servers/helpers/ImportServersBtn.js | 14 +-- src/short-urls/SearchBar.js | 73 +++++++------- src/short-urls/ShortUrls.js | 22 ++--- src/short-urls/ShortUrlsList.js | 17 +--- src/short-urls/helpers/ShortUrlsRow.js | 8 +- src/short-urls/helpers/ShortUrlsRowMenu.js | 2 +- src/tags/TagsList.js | 10 +- src/tags/helpers/Tag.js | 27 +++--- test/servers/ServersDropdown.test.js | 6 +- 18 files changed, 237 insertions(+), 213 deletions(-) diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index 339496fd..10f8940a 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -6,9 +6,8 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import DeleteServerButton from '../servers/DeleteServerButton'; -import './AsideMenu.scss'; import { serverType } from '../servers/prop-types'; +import './AsideMenu.scss'; const defaultProps = { className: '', @@ -20,51 +19,57 @@ const propTypes = { showOnMobile: PropTypes.bool, }; -export default function AsideMenu({ selectedServer, className, showOnMobile }) { - const serverId = selectedServer ? selectedServer.id : ''; - const asideClass = classnames('aside-menu', className, { - 'aside-menu--hidden': !showOnMobile, - }); - const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls'); +const AsideMenu = (DeleteServerButton) => { + const AsideMenu = ({ selectedServer, className, showOnMobile }) => { + const serverId = selectedServer ? selectedServer.id : ''; + const asideClass = classnames('aside-menu', className, { + 'aside-menu--hidden': !showOnMobile, + }); + const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls'); - return ( - + ); + }; -AsideMenu.defaultProps = defaultProps; -AsideMenu.propTypes = propTypes; + AsideMenu.defaultProps = defaultProps; + AsideMenu.propTypes = propTypes; + + return AsideMenu; +}; + +export default AsideMenu; diff --git a/src/common/MainHeader.js b/src/common/MainHeader.js index aa95cb06..075ae7be 100644 --- a/src/common/MainHeader.js +++ b/src/common/MainHeader.js @@ -6,11 +6,10 @@ import { Link } from 'react-router-dom'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import ServersDropdown from '../servers/ServersDropdown'; -import './MainHeader.scss'; import shlinkLogo from './shlink-logo-white.png'; +import './MainHeader.scss'; -const MainHeader = () => class MainHeaderComponent extends React.Component { +const MainHeader = (ServersDropdown) => class MainHeader extends React.Component { static propTypes = { location: PropTypes.object, }; diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index e2034104..39731d0e 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -7,13 +7,10 @@ import classnames from 'classnames'; import * as PropTypes from 'prop-types'; import ShortUrlsVisits from '../visits/ShortUrlVisits'; import CreateShortUrl from '../short-urls/CreateShortUrl'; -import ShortUrls from '../short-urls/ShortUrls'; import './MenuLayout.scss'; -import TagsList from '../tags/TagsList'; import { serverType } from '../servers/prop-types'; -import AsideMenu from './AsideMenu'; -export default class MenuLayout extends React.Component { +const MenuLayout = (TagsList, ShortUrls, AsideMenu) => class MenuLayout extends React.Component { static propTypes = { match: PropTypes.object, selectServer: PropTypes.func, @@ -99,4 +96,6 @@ export default class MenuLayout extends React.Component { ); } -} +}; + +export default MenuLayout; diff --git a/src/container/index.js b/src/container/index.js index faf0ee7a..50289a82 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -2,37 +2,110 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { pick } from 'ramda'; +import { assoc, pick } from 'ramda'; +import csvjson from 'csvjson'; +import axios from 'axios'; import App from '../App'; import ScrollToTop from '../common/ScrollToTop'; import MainHeader from '../common/MainHeader'; import { resetSelectedServer, selectServer } from '../servers/reducers/selectedServer'; import Home from '../common/Home'; import MenuLayout from '../common/MenuLayout'; -import { createServer } from '../servers/reducers/server'; +import { createServer, createServers, deleteServer, listServers } from '../servers/reducers/server'; import CreateServer from '../servers/CreateServer'; -import store from './store'; +import ServersDropdown from '../servers/ServersDropdown'; +import TagsList from '../tags/TagsList'; +import { filterTags, forceListTags } from '../tags/reducers/tagsList'; +import ShortUrls from '../short-urls/ShortUrls'; +import SearchBar from '../short-urls/SearchBar'; +import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; +import ShortUrlsList from '../short-urls/ShortUrlsList'; +import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams'; +import Tag from '../tags/helpers/Tag'; +import { ColorGenerator } from '../utils/ColorGenerator'; +import { Storage } from '../utils/Storage'; +import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; +import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu'; +import { ShlinkApiClient } from '../api/ShlinkApiClient'; +import DeleteServerModal from '../servers/DeleteServerModal'; +import DeleteServerButton from '../servers/DeleteServerButton'; +import AsideMenu from '../common/AsideMenu'; +import ImportServersBtn from '../servers/helpers/ImportServersBtn'; +import { ServersImporter } from '../servers/services/ServersImporter'; +import { ServersExporter } from '../servers/services/ServersExporter'; +import { ServersService } from '../servers/services/ServersService'; const bottle = new Bottle(); -bottle.constant('store', store); -bottle.serviceFactory('ScrollToTop', () => withRouter(ScrollToTop)); -bottle.serviceFactory('MainHeader', () => withRouter(MainHeader())); -bottle.serviceFactory('Home', () => connect(pick([ 'servers' ]), { resetSelectedServer })(Home)); -bottle.serviceFactory( +bottle.constant('ScrollToTop', ScrollToTop); +bottle.decorator('ScrollToTop', withRouter); + +bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); +bottle.decorator('MainHeader', withRouter); + +bottle.serviceFactory('Home', () => Home); +bottle.decorator('Home', connect(pick([ 'servers' ]), { resetSelectedServer })); + +bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu'); +bottle.decorator( 'MenuLayout', - () => compose( + compose( connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }), withRouter - )(MenuLayout) -); -bottle.serviceFactory( - 'CreateServer', - () => connect( - pick([ 'selectedServer' ]), - { createServer, resetSelectedServer } - )(CreateServer) + ) ); + +bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn'); +bottle.decorator('CreateServer', connect(pick([ 'selectedServer' ]), { createServer, resetSelectedServer })); + bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); +bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); +bottle.decorator('ServersDropdown', connect(pick([ 'servers', 'selectedServer' ]), { listServers, selectServer })); + +bottle.serviceFactory('TagsList', () => TagsList); +bottle.decorator('TagsList', connect(pick([ 'tagsList' ]), { forceListTags, filterTags })); + +bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); +bottle.decorator('ShortUrls', connect( + (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) +)); + +bottle.serviceFactory('SearchBar', SearchBar, 'Tag'); +bottle.decorator('SearchBar', connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })); + +bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); +bottle.decorator('ShortUrlsList', connect( + pick([ 'selectedServer', 'shortUrlsListParams' ]), + { listShortUrls, resetShortUrlParams } +)); + +bottle.serviceFactory('Tag', Tag, 'ColorGenerator'); + +bottle.constant('localStorage', global.localStorage); +bottle.service('Storage', Storage, 'localStorage'); +bottle.service('ColorGenerator', ColorGenerator, 'Storage'); + +bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'Tag', 'ShortUrlsRowMenu'); + +bottle.serviceFactory('ShortUrlsRowMenu', () => ShortUrlsRowMenu); + +bottle.constant('axios', axios); +bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios'); + +bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); +bottle.decorator('DeleteServerModal', compose(withRouter, connect(null, { deleteServer }))); + +bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); +bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); + +bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); +bottle.decorator('ImportServersBtn', connect(null, { createServers })); + +bottle.constant('csvjson', csvjson); +bottle.constant('window', global.window); +bottle.service('ServersImporter', ServersImporter, 'csvjson'); +bottle.service('ServersService', ServersService, 'Storage'); +bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); + export default bottle.container; diff --git a/src/index.js b/src/index.js index 62c93f92..8f30306f 100644 --- a/src/index.js +++ b/src/index.js @@ -6,11 +6,12 @@ import { BrowserRouter } from 'react-router-dom'; import { homepage } from '../package.json'; import registerServiceWorker from './registerServiceWorker'; import container from './container'; +import store from './container/store'; import '../node_modules/react-datepicker/dist/react-datepicker.css'; import './common/react-tagsinput.scss'; import './index.scss'; -const { App, ScrollToTop, store } = container; +const { App, ScrollToTop } = container; render( diff --git a/src/servers/CreateServer.js b/src/servers/CreateServer.js index 4a601d26..fbea083e 100644 --- a/src/servers/CreateServer.js +++ b/src/servers/CreateServer.js @@ -4,11 +4,10 @@ import { v4 as uuid } from 'uuid'; import PropTypes from 'prop-types'; import { stateFlagTimeout } from '../utils/utils'; import './CreateServer.scss'; -import ImportServersBtn from './helpers/ImportServersBtn'; const SHOW_IMPORT_MSG_TIME = 4000; -export default class CreateServer extends React.Component { +const CreateServer = (ImportServersBtn) => class CreateServer extends React.Component { static propTypes = { createServer: PropTypes.func, history: PropTypes.shape({ @@ -88,4 +87,6 @@ export default class CreateServer extends React.Component {
); } -} +}; + +export default CreateServer; diff --git a/src/servers/DeleteServerButton.js b/src/servers/DeleteServerButton.js index 7d161fc1..98b6bcff 100644 --- a/src/servers/DeleteServerButton.js +++ b/src/servers/DeleteServerButton.js @@ -2,10 +2,9 @@ import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; import PropTypes from 'prop-types'; -import DeleteServerModal from './DeleteServerModal'; import { serverType } from './prop-types'; -export default class DeleteServerButton extends React.Component { +const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component { static propTypes = { server: serverType, className: PropTypes.string, @@ -36,4 +35,6 @@ export default class DeleteServerButton extends React.Component { ); } -} +}; + +export default DeleteServerButton; diff --git a/src/servers/DeleteServerModal.js b/src/servers/DeleteServerModal.js index ca1b8f72..da056acf 100644 --- a/src/servers/DeleteServerModal.js +++ b/src/servers/DeleteServerModal.js @@ -1,10 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import { compose } from 'redux'; -import { deleteServer } from './reducers/server'; import { serverType } from './prop-types'; const propTypes = { @@ -17,7 +13,7 @@ const propTypes = { }), }; -export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServer, history }) => { +const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) => { const closeModal = () => { deleteServer(server); toggle(); @@ -42,11 +38,6 @@ export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServe ); }; -DeleteServerModalComponent.propTypes = propTypes; - -const DeleteServerModal = compose( - withRouter, - connect(null, { deleteServer }) -)(DeleteServerModalComponent); +DeleteServerModal.propTypes = propTypes; export default DeleteServerModal; diff --git a/src/servers/ServersDropdown.js b/src/servers/ServersDropdown.js index 84c96fac..5fa9f977 100644 --- a/src/servers/ServersDropdown.js +++ b/src/servers/ServersDropdown.js @@ -1,30 +1,20 @@ -import { isEmpty, pick, values } from 'ramda'; +import { isEmpty, values } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; import PropTypes from 'prop-types'; -import { selectServer } from '../servers/reducers/selectedServer'; -import serversExporter from '../servers/services/ServersExporter'; -import { listServers } from './reducers/server'; import { serverType } from './prop-types'; -export class ServersDropdownComponent extends React.Component { - static defaultProps = { - serversExporter, - }; +const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component { static propTypes = { servers: PropTypes.object, - serversExporter: PropTypes.shape({ - exportServers: PropTypes.func, - }), selectedServer: serverType, selectServer: PropTypes.func, listServers: PropTypes.func, }; renderServers = () => { - const { servers, selectedServer, selectServer, serversExporter } = this.props; + const { servers, selectedServer, selectServer } = this.props; if (isEmpty(servers)) { return Add a server first...; @@ -68,11 +58,6 @@ export class ServersDropdownComponent extends React.Component { ); } -} - -const ServersDropdown = connect( - pick([ 'servers', 'selectedServer' ]), - { listServers, selectServer } -)(ServersDropdownComponent); +}; export default ServersDropdown; diff --git a/src/servers/helpers/ImportServersBtn.js b/src/servers/helpers/ImportServersBtn.js index b5653674..29fb88d8 100644 --- a/src/servers/helpers/ImportServersBtn.js +++ b/src/servers/helpers/ImportServersBtn.js @@ -1,20 +1,15 @@ import React from 'react'; -import { connect } from 'react-redux'; import { UncontrolledTooltip } from 'reactstrap'; import { assoc } from 'ramda'; import { v4 as uuid } from 'uuid'; import PropTypes from 'prop-types'; -import { createServers } from '../reducers/server'; -import serversImporter, { serversImporterType } from '../services/ServersImporter'; -export class ImportServersBtnComponent extends React.Component { +const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component { static defaultProps = { - serversImporter, onImport: () => ({}), }; static propTypes = { onImport: PropTypes.func, - serversImporter: serversImporterType, createServers: PropTypes.func, fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]), }; @@ -25,7 +20,8 @@ export class ImportServersBtnComponent extends React.Component { } render() { - const { serversImporter: { importServersFromFile }, onImport, createServers } = this.props; + const { importServersFromFile } = serversImporter; + const { onImport, createServers } = this.props; const onChange = (e) => importServersFromFile(e.target.files[0]) .then((servers) => servers.map((server) => assoc('id', uuid(), server))) @@ -56,8 +52,6 @@ export class ImportServersBtnComponent extends React.Component { ); } -} - -const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent); +}; export default ImportServersBtn; diff --git a/src/short-urls/SearchBar.js b/src/short-urls/SearchBar.js index 6cd5cf3d..640682b3 100644 --- a/src/short-urls/SearchBar.js +++ b/src/short-urls/SearchBar.js @@ -1,55 +1,54 @@ import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; -import { connect } from 'react-redux'; -import { isEmpty, pick } from 'ramda'; +import { isEmpty } from 'ramda'; import PropTypes from 'prop-types'; -import Tag from '../tags/helpers/Tag'; import SearchField from '../utils/SearchField'; -import { listShortUrls } from './reducers/shortUrlsList'; -import './SearchBar.scss'; import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; +import './SearchBar.scss'; const propTypes = { listShortUrls: PropTypes.func, shortUrlsListParams: shortUrlsListParamsType, }; -export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) { - const selectedTags = shortUrlsListParams.tags || []; +const SearchBar = (Tag) => { + const SearchBar = ({ listShortUrls, shortUrlsListParams }) => { + const selectedTags = shortUrlsListParams.tags || []; - return ( -
- listShortUrls({ ...shortUrlsListParams, searchTerm }) - } - /> + return ( +
+ listShortUrls({ ...shortUrlsListParams, searchTerm }) + } + /> - {!isEmpty(selectedTags) && ( -

- -   - {selectedTags.map((tag) => ( - listShortUrls( - { - ...shortUrlsListParams, - tags: selectedTags.filter((selectedTag) => selectedTag !== tag), - } - )} - /> - ))} -

- )} -
- ); -} + {!isEmpty(selectedTags) && ( +

+ +   + {selectedTags.map((tag) => ( + listShortUrls( + { + ...shortUrlsListParams, + tags: selectedTags.filter((selectedTag) => selectedTag !== tag), + } + )} + /> + ))} +

+ )} +
+ ); + }; -SearchBarComponent.propTypes = propTypes; + SearchBar.propTypes = propTypes; -const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent); + return SearchBar; +}; export default SearchBar; diff --git a/src/short-urls/ShortUrls.js b/src/short-urls/ShortUrls.js index eadfc38f..22b2ed15 100644 --- a/src/short-urls/ShortUrls.js +++ b/src/short-urls/ShortUrls.js @@ -1,27 +1,21 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { assoc } from 'ramda'; import Paginator from './Paginator'; -import SearchBar from './SearchBar'; -import ShortUrlsList from './ShortUrlsList'; -export function ShortUrlsComponent(props) { - const { match: { params } } = props; +const ShortUrls = (SearchBar, ShortUrlsList) => (props) => { + const { match: { params }, shortUrlsList } = props; + const { page, serverId } = params; + const { data = [], pagination } = shortUrlsList; // Using a key on a component makes react to create a new instance every time the key changes - const urlsListKey = `${params.serverId}_${params.page}`; + const urlsListKey = `${serverId}_${page}`; return (
- - + +
); -} - -const ShortUrls = connect( - (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) -)(ShortUrlsComponent); +}; export default ShortUrls; diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js index 9ebeb2d0..58851562 100644 --- a/src/short-urls/ShortUrlsList.js +++ b/src/short-urls/ShortUrlsList.js @@ -1,17 +1,15 @@ import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown'; import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; -import { head, isEmpty, keys, pick, values } from 'ramda'; +import { head, isEmpty, keys, values } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import qs from 'qs'; import PropTypes from 'prop-types'; import { serverType } from '../servers/prop-types'; import SortingDropdown from '../utils/SortingDropdown'; import { determineOrderDir } from '../utils/utils'; -import { ShortUrlsRow } from './helpers/ShortUrlsRow'; -import { listShortUrls, shortUrlType } from './reducers/shortUrlsList'; -import { resetShortUrlParams, shortUrlsListParamsType } from './reducers/shortUrlsListParams'; +import { shortUrlType } from './reducers/shortUrlsList'; +import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; import './ShortUrlsList.scss'; const SORTABLE_FIELDS = { @@ -21,7 +19,7 @@ const SORTABLE_FIELDS = { visits: 'Visits', }; -export class ShortUrlsListComponent extends React.Component { +const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component { static propTypes = { listShortUrls: PropTypes.func, resetShortUrlParams: PropTypes.func, @@ -167,11 +165,6 @@ export class ShortUrlsListComponent extends React.Component { ); } -} - -const ShortUrlsList = connect( - pick([ 'selectedServer', 'shortUrlsListParams' ]), - { listShortUrls, resetShortUrlParams } -)(ShortUrlsListComponent); +}; export default ShortUrlsList; diff --git a/src/short-urls/helpers/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js index f9d64420..072eab04 100644 --- a/src/short-urls/helpers/ShortUrlsRow.js +++ b/src/short-urls/helpers/ShortUrlsRow.js @@ -2,16 +2,14 @@ import { isEmpty } from 'ramda'; import React from 'react'; import Moment from 'react-moment'; import PropTypes from 'prop-types'; -import Tag from '../../tags/helpers/Tag'; import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams'; import { serverType } from '../../servers/prop-types'; import ExternalLink from '../../utils/ExternalLink'; import { shortUrlType } from '../reducers/shortUrlsList'; import { stateFlagTimeout } from '../../utils/utils'; -import { ShortUrlsRowMenu } from './ShortUrlsRowMenu'; import './ShortUrlsRow.scss'; -export class ShortUrlsRow extends React.Component { +const ShortUrlsRow = (Tag, ShortUrlsRowMenu) => class ShortUrlsRow extends React.Component { static propTypes = { refreshList: PropTypes.func, shortUrlsListParams: shortUrlsListParamsType, @@ -72,4 +70,6 @@ export class ShortUrlsRow extends React.Component { ); } -} +}; + +export default ShortUrlsRow; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index c1348a14..f32c5de9 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -19,7 +19,7 @@ import EditTagsModal from './EditTagsModal'; import DeleteShortUrlModal from './DeleteShortUrlModal'; import './ShortUrlsRowMenu.scss'; -export class ShortUrlsRowMenu extends React.Component { +export default class ShortUrlsRowMenu extends React.Component { static propTypes = { completeShortUrl: PropTypes.string, onCopyToClipboard: PropTypes.func, diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index 1971817e..edf1f1c2 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -1,16 +1,14 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { pick, splitEvery } from 'ramda'; +import { splitEvery } from 'ramda'; import PropTypes from 'prop-types'; import MuttedMessage from '../utils/MuttedMessage'; import SearchField from '../utils/SearchField'; -import { filterTags, forceListTags } from './reducers/tagsList'; import TagCard from './TagCard'; const { ceil } = Math; const TAGS_GROUPS_AMOUNT = 4; -export class TagsListComponent extends React.Component { +export default class TagsList extends React.Component { static propTypes = { filterTags: PropTypes.func, forceListTags: PropTypes.func, @@ -83,7 +81,3 @@ export class TagsListComponent extends React.Component { ); } } - -const TagsList = connect(pick([ 'tagsList' ]), { forceListTags, filterTags })(TagsListComponent); - -export default TagsList; diff --git a/src/tags/helpers/Tag.js b/src/tags/helpers/Tag.js index a08085a9..dad93103 100644 --- a/src/tags/helpers/Tag.js +++ b/src/tags/helpers/Tag.js @@ -1,31 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; -import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; import './Tag.scss'; const propTypes = { - colorGenerator: colorGeneratorType, text: PropTypes.string, children: PropTypes.node, clearable: PropTypes.bool, onClick: PropTypes.func, onClose: PropTypes.func, }; -const defaultProps = { - colorGenerator, -}; -export default function Tag( - { - colorGenerator, +const Tag = (colorGenerator) => { + const Tag = ({ text, children, clearable, - onClick = () => ({}), - onClose = () => ({}), - } -) { - return ( + onClick = () => {}, + onClose = () => {}, + }) => ( ×} ); -} -Tag.defaultProps = defaultProps; -Tag.propTypes = propTypes; + Tag.propTypes = propTypes; + + return Tag; +}; + +export default Tag; diff --git a/test/servers/ServersDropdown.test.js b/test/servers/ServersDropdown.test.js index cbda33e7..836b4274 100644 --- a/test/servers/ServersDropdown.test.js +++ b/test/servers/ServersDropdown.test.js @@ -2,7 +2,7 @@ import { identity, values } from 'ramda'; import React from 'react'; import { shallow } from 'enzyme'; import { DropdownItem, DropdownToggle } from 'reactstrap'; -import { ServersDropdownComponent } from '../../src/servers/ServersDropdown'; +import ServersDropdown from '../../src/servers/ServersDropdown'; describe('', () => { let wrapped; @@ -13,7 +13,7 @@ describe('', () => { }; beforeEach(() => { - wrapped = shallow(); + wrapped = shallow(); }); afterEach(() => wrapped.unmount()); @@ -31,7 +31,7 @@ describe('', () => { }); it('contains a message when no servers exist yet', () => { - wrapped = shallow(); + wrapped = shallow(); const item = wrapped.find(DropdownItem); expect(item).toHaveLength(1); From bec755b121c3d807fb511d3d61a0e169346d9a9a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 17 Dec 2018 22:32:51 +0100 Subject: [PATCH 04/18] Fixed tests --- test/common/AsideMenu.test.js | 7 +++++-- test/servers/CreateServer.test.js | 6 ++++-- test/servers/DeleteServerButton.test.js | 4 +++- test/servers/DeleteServerModal.test.js | 4 ++-- test/servers/ServersDropdown.test.js | 4 +++- test/servers/helpers/ImportServersBtn.test.js | 11 ++++------- test/short-urls/SearchBar.test.js | 15 ++++++++------- test/short-urls/ShortUrls.test.js | 10 ++++++---- test/tags/TagsList.test.js | 2 +- 9 files changed, 36 insertions(+), 27 deletions(-) diff --git a/test/common/AsideMenu.test.js b/test/common/AsideMenu.test.js index f2241ee4..75500821 100644 --- a/test/common/AsideMenu.test.js +++ b/test/common/AsideMenu.test.js @@ -1,12 +1,15 @@ import { shallow } from 'enzyme'; import React from 'react'; import { NavLink } from 'react-router-dom'; -import AsideMenu from '../../src/common/AsideMenu'; +import asideMenuCreator from '../../src/common/AsideMenu'; describe('', () => { let wrapped; + const DeleteServerButton = () => ''; beforeEach(() => { + const AsideMenu = asideMenuCreator(DeleteServerButton); + wrapped = shallow(); }); afterEach(() => wrapped.unmount()); @@ -20,6 +23,6 @@ describe('', () => { }); it('contains a button to delete server', () => { - expect(wrapped.find('DeleteServerButton')).toHaveLength(1); + expect(wrapped.find(DeleteServerButton)).toHaveLength(1); }); }); diff --git a/test/servers/CreateServer.test.js b/test/servers/CreateServer.test.js index c9ce859f..ed6aff67 100644 --- a/test/servers/CreateServer.test.js +++ b/test/servers/CreateServer.test.js @@ -2,11 +2,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { identity } from 'ramda'; import sinon from 'sinon'; -import CreateServer from '../../src/servers/CreateServer'; -import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn'; +import createServerConstruct from '../../src/servers/CreateServer'; describe('', () => { let wrapper; + const ImportServersBtn = () => ''; const createServerMock = sinon.fake(); const historyMock = { push: sinon.fake(), @@ -16,6 +16,8 @@ describe('', () => { createServerMock.resetHistory(); historyMock.push.resetHistory(); + const CreateServer = createServerConstruct(ImportServersBtn); + wrapper = shallow( ); diff --git a/test/servers/DeleteServerButton.test.js b/test/servers/DeleteServerButton.test.js index 9da480a8..fbe11b09 100644 --- a/test/servers/DeleteServerButton.test.js +++ b/test/servers/DeleteServerButton.test.js @@ -1,12 +1,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import DeleteServerButton from '../../src/servers/DeleteServerButton'; +import deleteServerButtonConstruct from '../../src/servers/DeleteServerButton'; import DeleteServerModal from '../../src/servers/DeleteServerModal'; describe('', () => { let wrapper; beforeEach(() => { + const DeleteServerButton = deleteServerButtonConstruct(DeleteServerModal); + wrapper = shallow(); }); afterEach(() => wrapper.unmount()); diff --git a/test/servers/DeleteServerModal.test.js b/test/servers/DeleteServerModal.test.js index c5b879c2..ab06004d 100644 --- a/test/servers/DeleteServerModal.test.js +++ b/test/servers/DeleteServerModal.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import { DeleteServerModalComponent } from '../../src/servers/DeleteServerModal'; +import DeleteServerModal from '../../src/servers/DeleteServerModal'; describe('', () => { let wrapper; @@ -17,7 +17,7 @@ describe('', () => { historyMock.push.resetHistory(); wrapper = shallow( - ', () => { let wrapped; + let ServersDropdown; const servers = { '1a': { name: 'foo', id: 1 }, '2b': { name: 'bar', id: 2 }, @@ -13,6 +14,7 @@ describe('', () => { }; beforeEach(() => { + ServersDropdown = serversDropdownCreator({}); wrapped = shallow(); }); afterEach(() => wrapped.unmount()); diff --git a/test/servers/helpers/ImportServersBtn.test.js b/test/servers/helpers/ImportServersBtn.test.js index 58d6c970..269cd300 100644 --- a/test/servers/helpers/ImportServersBtn.test.js +++ b/test/servers/helpers/ImportServersBtn.test.js @@ -2,7 +2,7 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { UncontrolledTooltip } from 'reactstrap'; -import { ImportServersBtnComponent } from '../../../src/servers/helpers/ImportServersBtn'; +import importServersBtnConstruct from '../../../src/servers/helpers/ImportServersBtn'; describe('', () => { let wrapper; @@ -21,13 +21,10 @@ describe('', () => { serversImporterMock.importServersFromFile.resetHistory(); fileRef.current.click.resetHistory(); + const ImportServersBtn = importServersBtnConstruct(serversImporterMock); + wrapper = shallow( - + ); }); afterEach(() => wrapper.unmount()); diff --git a/test/short-urls/SearchBar.test.js b/test/short-urls/SearchBar.test.js index 75bc122e..ae811817 100644 --- a/test/short-urls/SearchBar.test.js +++ b/test/short-urls/SearchBar.test.js @@ -1,13 +1,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import { SearchBarComponent } from '../../src/short-urls/SearchBar'; +import searchBarCreator from '../../src/short-urls/SearchBar'; import SearchField from '../../src/utils/SearchField'; -import Tag from '../../src/tags/helpers/Tag'; describe('', () => { let wrapper; const listShortUrlsMock = sinon.spy(); + const Tag = () => ''; + const SearchBar = searchBarCreator(Tag); afterEach(() => { listShortUrlsMock.resetHistory(); @@ -18,13 +19,13 @@ describe('', () => { }); it('renders a SearchField', () => { - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find(SearchField)).toHaveLength(1); }); it('renders no tags when the list of tags is empty', () => { - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find(Tag)).toHaveLength(0); }); @@ -32,13 +33,13 @@ describe('', () => { it('renders the proper amount of tags', () => { const tags = [ 'foo', 'bar', 'baz' ]; - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find(Tag)).toHaveLength(tags.length); }); it('updates short URLs list when search field changes', () => { - wrapper = shallow(); + wrapper = shallow(); const searchField = wrapper.find(SearchField); expect(listShortUrlsMock.callCount).toEqual(0); @@ -48,7 +49,7 @@ describe('', () => { it('updates short URLs list when a tag is removed', () => { wrapper = shallow( - + ); const tag = wrapper.find(Tag).first(); diff --git a/test/short-urls/ShortUrls.test.js b/test/short-urls/ShortUrls.test.js index 9cccc9e9..d2327a31 100644 --- a/test/short-urls/ShortUrls.test.js +++ b/test/short-urls/ShortUrls.test.js @@ -1,12 +1,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ShortUrlsComponent as ShortUrls } from '../../src/short-urls/ShortUrls'; +import shortUrlsCreator from '../../src/short-urls/ShortUrls'; import Paginator from '../../src/short-urls/Paginator'; -import ShortUrlsList from '../../src/short-urls/ShortUrlsList'; -import SearchBar from '../../src/short-urls/SearchBar'; -describe('', () => { +describe('', () => { let wrapper; + const SearchBar = () => ''; + const ShortUrlsList = () => ''; beforeEach(() => { const params = { @@ -14,6 +14,8 @@ describe('', () => { page: '1', }; + const ShortUrls = shortUrlsCreator(SearchBar, ShortUrlsList); + wrapper = shallow(); }); afterEach(() => wrapper.unmount()); diff --git a/test/tags/TagsList.test.js b/test/tags/TagsList.test.js index bf37bd4f..ade440d9 100644 --- a/test/tags/TagsList.test.js +++ b/test/tags/TagsList.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { identity, range } from 'ramda'; import * as sinon from 'sinon'; -import { TagsListComponent as TagsList } from '../../src/tags/TagsList'; +import TagsList from '../../src/tags/TagsList'; import MuttedMessage from '../../src/utils/MuttedMessage'; import TagCard from '../../src/tags/TagCard'; import SearchField from '../../src/utils/SearchField'; From bab1e57ab16bf0562051deed3c458f33edd60da9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 17 Dec 2018 23:11:55 +0100 Subject: [PATCH 05/18] Registered remaining short URLs components in DI container --- src/common/MenuLayout.js | 3 +- src/container/index.js | 33 +++++++++++++++++-- src/short-urls/CreateShortUrl.js | 15 +++------ src/short-urls/helpers/DeleteShortUrlModal.js | 22 +++---------- src/short-urls/helpers/EditTagsModal.js | 19 ++--------- src/short-urls/helpers/ShortUrlsRowMenu.js | 8 ++--- src/tags/helpers/TagsSelector.js | 17 +++------- test/short-urls/CreateShortUrl.test.js | 6 ++-- .../helpers/DeleteShortUrlModal.test.js | 2 +- 9 files changed, 56 insertions(+), 69 deletions(-) diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 39731d0e..fc18676d 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -6,11 +6,10 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import * as PropTypes from 'prop-types'; import ShortUrlsVisits from '../visits/ShortUrlVisits'; -import CreateShortUrl from '../short-urls/CreateShortUrl'; import './MenuLayout.scss'; import { serverType } from '../servers/prop-types'; -const MenuLayout = (TagsList, ShortUrls, AsideMenu) => class MenuLayout extends React.Component { +const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl) => class MenuLayout extends React.Component { static propTypes = { match: PropTypes.object, selectServer: PropTypes.func, diff --git a/src/container/index.js b/src/container/index.js index 50289a82..c4d45ac4 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -15,7 +15,7 @@ import { createServer, createServers, deleteServer, listServers } from '../serve import CreateServer from '../servers/CreateServer'; import ServersDropdown from '../servers/ServersDropdown'; import TagsList from '../tags/TagsList'; -import { filterTags, forceListTags } from '../tags/reducers/tagsList'; +import { filterTags, forceListTags, listTags } from '../tags/reducers/tagsList'; import ShortUrls from '../short-urls/ShortUrls'; import SearchBar from '../short-urls/SearchBar'; import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; @@ -34,6 +34,13 @@ import ImportServersBtn from '../servers/helpers/ImportServersBtn'; import { ServersImporter } from '../servers/services/ServersImporter'; import { ServersExporter } from '../servers/services/ServersExporter'; import { ServersService } from '../servers/services/ServersService'; +import CreateShortUrl from '../short-urls/CreateShortUrl'; +import { createShortUrl, resetCreateShortUrl } from '../short-urls/reducers/shortUrlCreation'; +import TagsSelector from '../tags/helpers/TagsSelector'; +import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; +import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-urls/reducers/shortUrlDeletion'; +import EditTagsModal from '../short-urls/helpers/EditTagsModal'; +import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; const bottle = new Bottle(); @@ -46,7 +53,7 @@ bottle.decorator('MainHeader', withRouter); bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', connect(pick([ 'servers' ]), { resetSelectedServer })); -bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu'); +bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu', 'CreateShortUrl'); bottle.decorator( 'MenuLayout', compose( @@ -88,7 +95,7 @@ bottle.service('ColorGenerator', ColorGenerator, 'Storage'); bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'Tag', 'ShortUrlsRowMenu'); -bottle.serviceFactory('ShortUrlsRowMenu', () => ShortUrlsRowMenu); +bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); bottle.constant('axios', axios); bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios'); @@ -108,4 +115,24 @@ bottle.service('ServersImporter', ServersImporter, 'csvjson'); bottle.service('ServersService', ServersService, 'Storage'); bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); +bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); +bottle.decorator('CreateShortUrl', connect(pick([ 'shortUrlCreationResult' ]), { + createShortUrl, + resetCreateShortUrl, +})); + +bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator'); +bottle.decorator('TagsSelector', connect(pick([ 'tagsList' ]), { listTags })); + +bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); +bottle.decorator('DeleteShortUrlModal', connect( + pick([ 'shortUrlDeletion' ]), + { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } +)); +bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); +bottle.decorator('EditTagsModal', connect( + pick([ 'shortUrlTags' ]), + { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } +)); + export default bottle.container; diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index 5e0163f7..5c8cfbce 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -1,20 +1,18 @@ import downIcon from '@fortawesome/fontawesome-free-solid/faAngleDoubleDown'; import upIcon from '@fortawesome/fontawesome-free-solid/faAngleDoubleUp'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; -import { assoc, dissoc, isNil, pick, pipe, replace, trim } from 'ramda'; +import { assoc, dissoc, isNil, pipe, replace, trim } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import { Collapse } from 'reactstrap'; import * as PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; -import TagsSelector from '../tags/helpers/TagsSelector'; import CreateShortUrlResult from './helpers/CreateShortUrlResult'; -import { createShortUrl, createShortUrlResultType, resetCreateShortUrl } from './reducers/shortUrlCreation'; +import { createShortUrlResultType } from './reducers/shortUrlCreation'; const normalizeTag = pipe(trim, replace(/ /g, '-')); const formatDate = (date) => isNil(date) ? date : date.format(); -export class CreateShortUrlComponent extends React.Component { +const CreateShortUrl = (TagsSelector) => class CreateShortUrl extends React.Component { static propTypes = { createShortUrl: PropTypes.func, shortUrlCreationResult: createShortUrlResultType, @@ -122,11 +120,6 @@ export class CreateShortUrlComponent extends React.Component {
); } -} - -const CreateShortUrl = connect(pick([ 'shortUrlCreationResult' ]), { - createShortUrl, - resetCreateShortUrl, -})(CreateShortUrlComponent); +}; export default CreateShortUrl; diff --git a/src/short-urls/helpers/DeleteShortUrlModal.js b/src/short-urls/helpers/DeleteShortUrlModal.js index 40bd790d..1d2f0cc0 100644 --- a/src/short-urls/helpers/DeleteShortUrlModal.js +++ b/src/short-urls/helpers/DeleteShortUrlModal.js @@ -1,18 +1,11 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; +import React from 'react'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import PropTypes from 'prop-types'; -import { pick, identity } from 'ramda'; +import { identity } from 'ramda'; import { shortUrlType } from '../reducers/shortUrlsList'; -import { - deleteShortUrl, - resetDeleteShortUrl, - shortUrlDeleted, - shortUrlDeletionType, -} from '../reducers/shortUrlDeletion'; -import './QrCodeModal.scss'; +import { shortUrlDeletionType } from '../reducers/shortUrlDeletion'; -export class DeleteShortUrlModalComponent extends Component { +export default class DeleteShortUrlModal extends React.Component { static propTypes = { shortUrl: shortUrlType, toggle: PropTypes.func, @@ -94,10 +87,3 @@ export class DeleteShortUrlModalComponent extends Component { ); } } - -const DeleteShortUrlModal = connect( - pick([ 'shortUrlDeletion' ]), - { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } -)(DeleteShortUrlModalComponent); - -export default DeleteShortUrlModal; diff --git a/src/short-urls/helpers/EditTagsModal.js b/src/short-urls/helpers/EditTagsModal.js index 83d94470..d98214ec 100644 --- a/src/short-urls/helpers/EditTagsModal.js +++ b/src/short-urls/helpers/EditTagsModal.js @@ -1,19 +1,11 @@ import React from 'react'; -import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import PropTypes from 'prop-types'; -import { pick } from 'ramda'; -import TagsSelector from '../../tags/helpers/TagsSelector'; -import { - editShortUrlTags, - resetShortUrlsTags, - shortUrlTagsType, - shortUrlTagsEdited, -} from '../reducers/shortUrlTags'; +import { shortUrlTagsType } from '../reducers/shortUrlTags'; import ExternalLink from '../../utils/ExternalLink'; import { shortUrlType } from '../reducers/shortUrlsList'; -export class EditTagsModalComponent extends React.Component { +const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Component { static propTypes = { isOpen: PropTypes.bool.isRequired, toggle: PropTypes.func.isRequired, @@ -88,11 +80,6 @@ export class EditTagsModalComponent extends React.Component { ); } -} - -const EditTagsModal = connect( - pick([ 'shortUrlTags' ]), - { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } -)(EditTagsModalComponent); +}; export default EditTagsModal; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index f32c5de9..884c5d68 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -15,11 +15,9 @@ import { serverType } from '../../servers/prop-types'; import { shortUrlType } from '../reducers/shortUrlsList'; import PreviewModal from './PreviewModal'; import QrCodeModal from './QrCodeModal'; -import EditTagsModal from './EditTagsModal'; -import DeleteShortUrlModal from './DeleteShortUrlModal'; import './ShortUrlsRowMenu.scss'; -export default class ShortUrlsRowMenu extends React.Component { +const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component { static propTypes = { completeShortUrl: PropTypes.string, onCopyToClipboard: PropTypes.func, @@ -105,4 +103,6 @@ export default class ShortUrlsRowMenu extends React.Component { ); } -} +}; + +export default ShortUrlsRowMenu; diff --git a/src/tags/helpers/TagsSelector.js b/src/tags/helpers/TagsSelector.js index 4a218448..fdbfeffd 100644 --- a/src/tags/helpers/TagsSelector.js +++ b/src/tags/helpers/TagsSelector.js @@ -1,26 +1,21 @@ import React from 'react'; -import { connect } from 'react-redux'; import TagsInput from 'react-tagsinput'; import PropTypes from 'prop-types'; import Autosuggest from 'react-autosuggest'; -import { pick, identity } from 'ramda'; -import { listTags } from '../reducers/tagsList'; -import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; -import './TagsSelector.scss'; +import { identity } from 'ramda'; import TagBullet from './TagBullet'; +import './TagsSelector.scss'; -export class TagsSelectorComponent extends React.Component { +const TagsSelector = (colorGenerator) => class TagsSelector extends React.Component { static propTypes = { tags: PropTypes.arrayOf(PropTypes.string).isRequired, onChange: PropTypes.func.isRequired, placeholder: PropTypes.string, - colorGenerator: colorGeneratorType, tagsList: PropTypes.shape({ tags: PropTypes.arrayOf(PropTypes.string), }), }; static defaultProps = { - colorGenerator, placeholder: 'Add tags to the URL', }; @@ -31,7 +26,7 @@ export class TagsSelectorComponent extends React.Component { } render() { - const { tags, onChange, placeholder, colorGenerator, tagsList } = this.props; + const { tags, onChange, placeholder, tagsList } = this.props; const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => ( {getTagDisplayValue(tag)} @@ -86,8 +81,6 @@ export class TagsSelectorComponent extends React.Component { /> ); } -} - -const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent); +}; export default TagsSelector; diff --git a/test/short-urls/CreateShortUrl.test.js b/test/short-urls/CreateShortUrl.test.js index 041fb1c3..0443369d 100644 --- a/test/short-urls/CreateShortUrl.test.js +++ b/test/short-urls/CreateShortUrl.test.js @@ -3,18 +3,20 @@ import { shallow } from 'enzyme'; import moment from 'moment'; import * as sinon from 'sinon'; import { identity } from 'ramda'; -import { CreateShortUrlComponent as CreateShortUrl } from '../../src/short-urls/CreateShortUrl'; -import TagsSelector from '../../src/tags/helpers/TagsSelector'; +import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl'; import DateInput from '../../src/utils/DateInput'; describe('', () => { let wrapper; + const TagsSelector = () => ''; const shortUrlCreationResult = { loading: false, }; const createShortUrl = sinon.spy(); beforeEach(() => { + const CreateShortUrl = createShortUrlsCreator(TagsSelector); + wrapper = shallow( ); diff --git a/test/short-urls/helpers/DeleteShortUrlModal.test.js b/test/short-urls/helpers/DeleteShortUrlModal.test.js index fdc115a2..6118f732 100644 --- a/test/short-urls/helpers/DeleteShortUrlModal.test.js +++ b/test/short-urls/helpers/DeleteShortUrlModal.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { identity } from 'ramda'; import * as sinon from 'sinon'; -import { DeleteShortUrlModalComponent as DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal'; +import DeleteShortUrlModal from '../../../src/short-urls/helpers/DeleteShortUrlModal'; describe('', () => { let wrapper; From d6e53918a24a2b2b6b6d23ab25bac43c56c58758 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 04:34:37 +0100 Subject: [PATCH 06/18] Created function which dynamically resolve action services from the container for connected components --- src/container/index.js | 53 ++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/container/index.js b/src/container/index.js index c4d45ac4..da38f008 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -1,7 +1,6 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; -import { compose } from 'redux'; import { assoc, pick } from 'ramda'; import csvjson from 'csvjson'; import axios from 'axios'; @@ -43,6 +42,18 @@ import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; const bottle = new Bottle(); +const { container } = bottle; + +const mapActionService = (map, actionName) => { + map[actionName] = container[actionName]; + + return map; +}; +const connectDecorator = (propsFromState, actionServiceNames) => + connect( + pick(propsFromState), + Array.isArray(actionServiceNames) ? actionServiceNames.reduce(mapActionService, {}) : actionServiceNames + ); bottle.constant('ScrollToTop', ScrollToTop); bottle.decorator('ScrollToTop', withRouter); @@ -51,27 +62,22 @@ bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.decorator('MainHeader', withRouter); bottle.serviceFactory('Home', () => Home); -bottle.decorator('Home', connect(pick([ 'servers' ]), { resetSelectedServer })); +bottle.decorator('Home', connectDecorator([ 'servers' ], { resetSelectedServer })); bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu', 'CreateShortUrl'); -bottle.decorator( - 'MenuLayout', - compose( - connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }), - withRouter - ) -); +bottle.decorator('MenuLayout', connectDecorator([ 'selectedServer', 'shortUrlsListParams' ], { selectServer })); +bottle.decorator('MenuLayout', withRouter); bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn'); -bottle.decorator('CreateServer', connect(pick([ 'selectedServer' ]), { createServer, resetSelectedServer })); +bottle.decorator('CreateServer', connectDecorator([ 'selectedServer' ], { createServer, resetSelectedServer })); bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); -bottle.decorator('ServersDropdown', connect(pick([ 'servers', 'selectedServer' ]), { listServers, selectServer })); +bottle.decorator('ServersDropdown', connectDecorator([ 'servers', 'selectedServer' ], { listServers, selectServer })); bottle.serviceFactory('TagsList', () => TagsList); -bottle.decorator('TagsList', connect(pick([ 'tagsList' ]), { forceListTags, filterTags })); +bottle.decorator('TagsList', connectDecorator([ 'tagsList' ], { forceListTags, filterTags })); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); bottle.decorator('ShortUrls', connect( @@ -79,11 +85,11 @@ bottle.decorator('ShortUrls', connect( )); bottle.serviceFactory('SearchBar', SearchBar, 'Tag'); -bottle.decorator('SearchBar', connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })); +bottle.decorator('SearchBar', connectDecorator([ 'shortUrlsListParams' ], { listShortUrls })); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); -bottle.decorator('ShortUrlsList', connect( - pick([ 'selectedServer', 'shortUrlsListParams' ]), +bottle.decorator('ShortUrlsList', connectDecorator( + [ 'selectedServer', 'shortUrlsListParams' ], { listShortUrls, resetShortUrlParams } )); @@ -101,7 +107,8 @@ bottle.constant('axios', axios); bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios'); bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); -bottle.decorator('DeleteServerModal', compose(withRouter, connect(null, { deleteServer }))); +bottle.decorator('DeleteServerModal', withRouter); +bottle.decorator('DeleteServerModal', connect(null, { deleteServer })); bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); @@ -116,23 +123,23 @@ bottle.service('ServersService', ServersService, 'Storage'); bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); -bottle.decorator('CreateShortUrl', connect(pick([ 'shortUrlCreationResult' ]), { +bottle.decorator('CreateShortUrl', connectDecorator([ 'shortUrlCreationResult' ], { createShortUrl, resetCreateShortUrl, })); bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator'); -bottle.decorator('TagsSelector', connect(pick([ 'tagsList' ]), { listTags })); +bottle.decorator('TagsSelector', connectDecorator([ 'tagsList' ], { listTags })); bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); -bottle.decorator('DeleteShortUrlModal', connect( - pick([ 'shortUrlDeletion' ]), +bottle.decorator('DeleteShortUrlModal', connectDecorator( + [ 'shortUrlDeletion' ], { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } )); bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); -bottle.decorator('EditTagsModal', connect( - pick([ 'shortUrlTags' ]), +bottle.decorator('EditTagsModal', connectDecorator( + [ 'shortUrlTags' ], { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } )); -export default bottle.container; +export default container; From 12ddeebedfdf3e7067c99a6f465eb52541a8466b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 04:54:32 +0100 Subject: [PATCH 07/18] Registered first actions as services --- src/container/index.js | 7 ++++++- src/short-urls/reducers/shortUrlTags.js | 10 +++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/container/index.js b/src/container/index.js index da38f008..8d4f97da 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -136,10 +136,15 @@ bottle.decorator('DeleteShortUrlModal', connectDecorator( [ 'shortUrlDeletion' ], { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } )); + +bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'ShlinkApiClient'); +bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); +bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); + bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); bottle.decorator('EditTagsModal', connectDecorator( [ 'shortUrlTags' ], - { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } + [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); export default container; diff --git a/src/short-urls/reducers/shortUrlTags.js b/src/short-urls/reducers/shortUrlTags.js index 597c5a89..c1f5646d 100644 --- a/src/short-urls/reducers/shortUrlTags.js +++ b/src/short-urls/reducers/shortUrlTags.js @@ -1,6 +1,5 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { pick } from 'ramda'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START'; @@ -40,8 +39,7 @@ export default function reducer(state = defaultState, action) { }; case EDIT_SHORT_URL_TAGS: return { - shortCode: action.shortCode, - tags: action.tags, + ...pick([ 'shortCode', 'tags' ], action), saving: false, error: false, }; @@ -52,7 +50,7 @@ export default function reducer(state = defaultState, action) { } } -export const _editShortUrlTags = (shlinkApiClient, shortCode, tags) => async (dispatch) => { +export const editShortUrlTags = (shlinkApiClient) => (shortCode, tags) => async (dispatch) => { dispatch({ type: EDIT_SHORT_URL_TAGS_START }); try { @@ -66,8 +64,6 @@ export const _editShortUrlTags = (shlinkApiClient, shortCode, tags) => async (di } }; -export const editShortUrlTags = curry(_editShortUrlTags)(shlinkApiClient); - export const resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS }); export const shortUrlTagsEdited = (shortCode, tags) => ({ From 7bd4b39b5a28db18087026113fb684802e840fb9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 05:03:38 +0100 Subject: [PATCH 08/18] Added lazy loading to action services --- src/container/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/container/index.js b/src/container/index.js index 8d4f97da..88f8b6a3 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -45,7 +45,8 @@ const bottle = new Bottle(); const { container } = bottle; const mapActionService = (map, actionName) => { - map[actionName] = container[actionName]; + // Wrap actual action service in a function so that it is lazily created the first time it is called + map[actionName] = (...args) => container[actionName](...args); return map; }; @@ -137,14 +138,14 @@ bottle.decorator('DeleteShortUrlModal', connectDecorator( { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } )); -bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'ShlinkApiClient'); -bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); -bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); - bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); bottle.decorator('EditTagsModal', connectDecorator( [ 'shortUrlTags' ], [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); +bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'ShlinkApiClient'); +bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); +bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); + export default container; From 4f54e3315ff00deeaa39a5e4a9bbb4693f4c4b3a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 10:14:25 +0100 Subject: [PATCH 09/18] Simplified ShlinkApiClient and moved runtime creation logic to external service --- src/api/ShlinkApiClient.js | 22 +++++---------------- src/api/ShlinkApiClientBuilder.js | 18 +++++++++++++++++ src/container/index.js | 17 +++++++++------- src/servers/reducers/selectedServer.js | 7 ++----- src/short-urls/reducers/shortUrlCreation.js | 9 ++++++--- src/short-urls/reducers/shortUrlDeletion.js | 9 ++++++--- src/short-urls/reducers/shortUrlTags.js | 4 +++- src/short-urls/reducers/shortUrlsList.js | 9 ++++++--- src/tags/reducers/tagDelete.js | 9 ++++++--- src/tags/reducers/tagEdit.js | 12 ++++++++--- src/tags/reducers/tagsList.js | 11 ++++++----- src/visits/reducers/shortUrlDetail.js | 9 ++++++--- src/visits/reducers/shortUrlVisits.js | 9 ++++++--- test/api/ShlinkApiClient.test.js | 2 +- 14 files changed, 90 insertions(+), 57 deletions(-) create mode 100644 src/api/ShlinkApiClientBuilder.js diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index 6c35b84a..f2439bba 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -1,26 +1,18 @@ -import axios from 'axios'; import qs from 'qs'; import { isEmpty, isNil, reject } from 'ramda'; const API_VERSION = '1'; const STATUS_UNAUTHORIZED = 401; +const buildRestUrl = (url) => url ? `${url}/rest/v${API_VERSION}` : ''; -export class ShlinkApiClient { - constructor(axios) { +export default class ShlinkApiClient { + constructor(axios, baseUrl, apiKey) { this.axios = axios; - this._baseUrl = ''; - this._apiKey = ''; + this._baseUrl = buildRestUrl(baseUrl); + this._apiKey = apiKey || ''; this._token = ''; } - /** - * Sets the base URL to be used on any request - */ - setConfig = ({ url, apiKey }) => { - this._baseUrl = `${url}/rest/v${API_VERSION}`; - this._apiKey = apiKey; - }; - listShortUrls = (options = {}) => this._performRequest('/short-codes', 'GET', options) .then((resp) => resp.data.shortUrls) @@ -113,7 +105,3 @@ export class ShlinkApiClient { return Promise.reject(e); }; } - -const shlinkApiClient = new ShlinkApiClient(axios); - -export default shlinkApiClient; diff --git a/src/api/ShlinkApiClientBuilder.js b/src/api/ShlinkApiClientBuilder.js new file mode 100644 index 00000000..23b050c8 --- /dev/null +++ b/src/api/ShlinkApiClientBuilder.js @@ -0,0 +1,18 @@ +import * as axios from 'axios'; +import ShlinkApiClient from './ShlinkApiClient'; + +const apiClients = {}; + +const buildShlinkApiClient = (axios) => ({ url, apiKey }) => { + const clientKey = `${url}_${apiKey}`; + + if (!apiClients[clientKey]) { + apiClients[clientKey] = new ShlinkApiClient(axios, url, apiKey); + } + + return apiClients[clientKey]; +}; + +export default buildShlinkApiClient; + +export const buildShlinkApiClientWithAxios = buildShlinkApiClient(axios); diff --git a/src/container/index.js b/src/container/index.js index 88f8b6a3..88593418 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -25,7 +25,7 @@ import { ColorGenerator } from '../utils/ColorGenerator'; import { Storage } from '../utils/Storage'; import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu'; -import { ShlinkApiClient } from '../api/ShlinkApiClient'; +import ShlinkApiClient from '../api/ShlinkApiClient'; import DeleteServerModal from '../servers/DeleteServerModal'; import DeleteServerButton from '../servers/DeleteServerButton'; import AsideMenu from '../common/AsideMenu'; @@ -40,16 +40,17 @@ import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-urls/reducers/shortUrlDeletion'; import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; +import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; const bottle = new Bottle(); const { container } = bottle; -const mapActionService = (map, actionName) => { - // Wrap actual action service in a function so that it is lazily created the first time it is called - map[actionName] = (...args) => container[actionName](...args); +const mapActionService = (map, actionName) => ({ + ...map, - return map; -}; + // Wrap actual action service in a function so that it is lazily created the first time it is called + [actionName]: (...args) => container[actionName](...args), +}); const connectDecorator = (propsFromState, actionServiceNames) => connect( pick(propsFromState), @@ -144,8 +145,10 @@ bottle.decorator('EditTagsModal', connectDecorator( [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); -bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'ShlinkApiClient'); +bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); +bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); + export default container; diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index eca53474..d53f21c3 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,4 +1,3 @@ -import shlinkApiClient from '../../api/ShlinkApiClient'; import serversService from '../../servers/services/ServersService'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; @@ -22,17 +21,15 @@ export default function reducer(state = defaultState, action) { export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); -export const _selectServer = (shlinkApiClient, serversService) => (serverId) => (dispatch) => { +export const _selectServer = (serversService) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); const selectedServer = serversService.findServerById(serverId); - shlinkApiClient.setConfig(selectedServer); - dispatch({ type: SELECT_SERVER, selectedServer, }); }; -export const selectServer = _selectServer(shlinkApiClient, serversService); +export const selectServer = _selectServer(serversService); diff --git a/src/short-urls/reducers/shortUrlCreation.js b/src/short-urls/reducers/shortUrlCreation.js index 9f01a8ee..80150550 100644 --- a/src/short-urls/reducers/shortUrlCreation.js +++ b/src/short-urls/reducers/shortUrlCreation.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; @@ -50,9 +50,12 @@ export default function reducer(state = defaultState, action) { } } -export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => { +export const _createShortUrl = (buildShlinkApiClient, data) => async (dispatch, getState) => { dispatch({ type: CREATE_SHORT_URL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const result = await shlinkApiClient.createShortUrl(data); @@ -62,6 +65,6 @@ export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => { } }; -export const createShortUrl = curry(_createShortUrl)(shlinkApiClient); +export const createShortUrl = curry(_createShortUrl)(buildShlinkApiClient); export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL }); diff --git a/src/short-urls/reducers/shortUrlDeletion.js b/src/short-urls/reducers/shortUrlDeletion.js index 60cdedbe..20812079 100644 --- a/src/short-urls/reducers/shortUrlDeletion.js +++ b/src/short-urls/reducers/shortUrlDeletion.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START'; @@ -56,9 +56,12 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) => { +export const _deleteShortUrl = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { dispatch({ type: DELETE_SHORT_URL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.deleteShortUrl(shortCode); dispatch({ type: DELETE_SHORT_URL, shortCode }); @@ -69,7 +72,7 @@ export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) } }; -export const deleteShortUrl = curry(_deleteShortUrl)(shlinkApiClient); +export const deleteShortUrl = curry(_deleteShortUrl)(buildShlinkApiClient); export const resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL }); diff --git a/src/short-urls/reducers/shortUrlTags.js b/src/short-urls/reducers/shortUrlTags.js index c1f5646d..a0390a60 100644 --- a/src/short-urls/reducers/shortUrlTags.js +++ b/src/short-urls/reducers/shortUrlTags.js @@ -50,8 +50,10 @@ export default function reducer(state = defaultState, action) { } } -export const editShortUrlTags = (shlinkApiClient) => (shortCode, tags) => async (dispatch) => { +export const editShortUrlTags = (buildShlinkApiClient) => (shortCode, tags) => async (dispatch, getState) => { dispatch({ type: EDIT_SHORT_URL_TAGS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); try { const normalizedTags = await shlinkApiClient.updateShortUrlTags(shortCode, tags); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 95e4a7fc..de1df19e 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,6 +1,6 @@ import { assoc, assocPath, reject } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; @@ -55,9 +55,12 @@ export default function reducer(state = initialState, action) { } } -export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) => { +export const _listShortUrls = (buildShlinkApiClient, params = {}) => async (dispatch, getState) => { dispatch({ type: LIST_SHORT_URLS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const shortUrls = await shlinkApiClient.listShortUrls(params); @@ -67,4 +70,4 @@ export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) } }; -export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params); +export const listShortUrls = (params = {}) => _listShortUrls(buildShlinkApiClient, params); diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.js index dfd2bf4a..e4ea9410 100644 --- a/src/tags/reducers/tagDelete.js +++ b/src/tags/reducers/tagDelete.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; @@ -41,9 +41,12 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => { +export const _deleteTag = (buildShlinkApiClient, tag) => async (dispatch, getState) => { dispatch({ type: DELETE_TAG_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.deleteTags([ tag ]); dispatch({ type: DELETE_TAG }); @@ -54,6 +57,6 @@ export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => { } }; -export const deleteTag = curry(_deleteTag)(shlinkApiClient); +export const deleteTag = curry(_deleteTag)(buildShlinkApiClient); export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js index 650f0ba1..53bb25c4 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.js @@ -1,5 +1,5 @@ import { curry, pick } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import colorGenerator from '../../utils/ColorGenerator'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -42,9 +42,15 @@ export default function reducer(state = defaultState, action) { } } -export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, color) => async (dispatch) => { +export const _editTag = (buildShlinkApiClient, colorGenerator, oldName, newName, color) => async ( + dispatch, + getState +) => { dispatch({ type: EDIT_TAG_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { await shlinkApiClient.editTag(oldName, newName); colorGenerator.setColorForKey(newName, color); @@ -56,7 +62,7 @@ export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, colo } }; -export const editTag = curry(_editTag)(shlinkApiClient, colorGenerator); +export const editTag = curry(_editTag)(buildShlinkApiClient, colorGenerator); export const tagEdited = (oldName, newName, color) => ({ type: TAG_EDITED, diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index cb415902..94338266 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,5 +1,5 @@ import { isEmpty, reject } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { TAG_DELETED } from './tagDelete'; import { TAG_EDITED } from './tagEdit'; @@ -66,8 +66,8 @@ export default function reducer(state = defaultState, action) { } } -export const _listTags = (shlinkApiClient, force = false) => async (dispatch, getState) => { - const { tagsList } = getState(); +export const _listTags = (buildShlinkApiClient, force = false) => async (dispatch, getState) => { + const { tagsList, selectedServer } = getState(); if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) { return; @@ -76,6 +76,7 @@ export const _listTags = (shlinkApiClient, force = false) => async (dispatch, ge dispatch({ type: LIST_TAGS_START }); try { + const shlinkApiClient = buildShlinkApiClient(selectedServer); const tags = await shlinkApiClient.listTags(); dispatch({ tags, type: LIST_TAGS }); @@ -84,9 +85,9 @@ export const _listTags = (shlinkApiClient, force = false) => async (dispatch, ge } }; -export const listTags = () => _listTags(shlinkApiClient); +export const listTags = () => _listTags(buildShlinkApiClient); -export const forceListTags = () => _listTags(shlinkApiClient, true); +export const forceListTags = () => _listTags(buildShlinkApiClient, true); export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, diff --git a/src/visits/reducers/shortUrlDetail.js b/src/visits/reducers/shortUrlDetail.js index cb86e040..b62c99ce 100644 --- a/src/visits/reducers/shortUrlDetail.js +++ b/src/visits/reducers/shortUrlDetail.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { shortUrlType } from '../../short-urls/reducers/shortUrlsList'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -45,9 +45,12 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatch) => { +export const _getShortUrlDetail = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_DETAIL_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const shortUrl = await shlinkApiClient.getShortUrl(shortCode); @@ -57,4 +60,4 @@ export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatc } }; -export const getShortUrlDetail = curry(_getShortUrlDetail)(shlinkApiClient); +export const getShortUrlDetail = curry(_getShortUrlDetail)(buildShlinkApiClient); diff --git a/src/visits/reducers/shortUrlVisits.js b/src/visits/reducers/shortUrlVisits.js index 1bb724ab..4df1a09c 100644 --- a/src/visits/reducers/shortUrlVisits.js +++ b/src/visits/reducers/shortUrlVisits.js @@ -1,6 +1,6 @@ import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import shlinkApiClient from '../../api/ShlinkApiClient'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; @@ -44,9 +44,12 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => async (dispatch) => { +export const _getShortUrlVisits = (buildShlinkApiClient, shortCode, dates) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_VISITS_START }); + const { selectedServer } = getState(); + const shlinkApiClient = buildShlinkApiClient(selectedServer); + try { const visits = await shlinkApiClient.getShortUrlVisits(shortCode, dates); @@ -56,4 +59,4 @@ export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => async ( } }; -export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient); +export const getShortUrlVisits = curry(_getShortUrlVisits)(buildShlinkApiClient); diff --git a/test/api/ShlinkApiClient.test.js b/test/api/ShlinkApiClient.test.js index 68894fb0..02935f1d 100644 --- a/test/api/ShlinkApiClient.test.js +++ b/test/api/ShlinkApiClient.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; import { head, last } from 'ramda'; -import { ShlinkApiClient } from '../../src/api/ShlinkApiClient'; +import ShlinkApiClient from '../../src/api/ShlinkApiClient'; describe('ShlinkApiClient', () => { const createAxiosMock = (extraData) => () => From 79a0a5e4eae8b8d27e150f88fac53251c0cb673e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 10:23:09 +0100 Subject: [PATCH 10/18] Fixed tests --- test/servers/reducers/selectedServer.test.js | 9 ++------- test/short-urls/reducers/shortUrlCreation.test.js | 9 +++++---- test/tags/reducers/tagDelete.test.js | 9 +++++---- test/tags/reducers/tagEdit.test.js | 9 +++++---- test/visits/reducers/shortUrlDetail.test.js | 5 +++-- test/visits/reducers/shortUrlVisits.test.js | 5 +++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index 442172da..2b99d5d7 100644 --- a/test/servers/reducers/selectedServer.test.js +++ b/test/servers/reducers/selectedServer.test.js @@ -29,9 +29,6 @@ describe('selectedServerReducer', () => { }); describe('selectServer', () => { - const ShlinkApiClientMock = { - setConfig: sinon.spy(), - }; const serverId = 'abc123'; const selectedServer = { id: serverId, @@ -41,7 +38,6 @@ describe('selectedServerReducer', () => { }; afterEach(() => { - ShlinkApiClientMock.setConfig.resetHistory(); ServersServiceMock.findServerById.resetHistory(); }); @@ -49,7 +45,7 @@ describe('selectedServerReducer', () => { const dispatch = sinon.spy(); const expectedDispatchCalls = 2; - _selectServer(ShlinkApiClientMock, ServersServiceMock)(serverId)(dispatch); + _selectServer(ServersServiceMock)(serverId)(dispatch); expect(dispatch.callCount).toEqual(expectedDispatchCalls); expect(dispatch.firstCall.calledWith({ type: RESET_SHORT_URL_PARAMS })).toEqual(true); @@ -60,9 +56,8 @@ describe('selectedServerReducer', () => { }); it('invokes dependencies', () => { - _selectServer(ShlinkApiClientMock, ServersServiceMock)(serverId)(() => {}); + _selectServer(ServersServiceMock)(serverId)(() => {}); - expect(ShlinkApiClientMock.setConfig.callCount).toEqual(1); expect(ServersServiceMock.findServerById.callCount).toEqual(1); }); }); diff --git a/test/short-urls/reducers/shortUrlCreation.test.js b/test/short-urls/reducers/shortUrlCreation.test.js index ebf1dc04..28aac024 100644 --- a/test/short-urls/reducers/shortUrlCreation.test.js +++ b/test/short-urls/reducers/shortUrlCreation.test.js @@ -54,6 +54,7 @@ describe('shortUrlCreationReducer', () => { createShortUrl: sinon.fake.returns(result), }); const dispatch = sinon.spy(); + const getState = () => ({}); afterEach(() => dispatch.resetHistory()); @@ -61,9 +62,9 @@ describe('shortUrlCreationReducer', () => { const expectedDispatchCalls = 2; const result = 'foo'; const apiClientMock = createApiClientMock(Promise.resolve(result)); - const dispatchable = _createShortUrl(apiClientMock, {}); + const dispatchable = _createShortUrl(() => apiClientMock, {}); - await dispatchable(dispatch); + await dispatchable(dispatch, getState); expect(apiClientMock.createShortUrl.callCount).toEqual(1); @@ -76,10 +77,10 @@ describe('shortUrlCreationReducer', () => { const expectedDispatchCalls = 2; const error = 'Error'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _createShortUrl(apiClientMock, {}); + const dispatchable = _createShortUrl(() => apiClientMock, {}); try { - await dispatchable(dispatch); + await dispatchable(dispatch, getState); } catch (e) { expect(e).toEqual(error); } diff --git a/test/tags/reducers/tagDelete.test.js b/test/tags/reducers/tagDelete.test.js index ece3c92d..26450c9e 100644 --- a/test/tags/reducers/tagDelete.test.js +++ b/test/tags/reducers/tagDelete.test.js @@ -48,6 +48,7 @@ describe('tagDeleteReducer', () => { deleteTags: sinon.fake.returns(result), }); const dispatch = sinon.spy(); + const getState = () => ({}); afterEach(() => dispatch.resetHistory()); @@ -55,9 +56,9 @@ describe('tagDeleteReducer', () => { const expectedDispatchCalls = 2; const tag = 'foo'; const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = _deleteTag(apiClientMock, tag); + const dispatchable = _deleteTag(() => apiClientMock, tag); - await dispatchable(dispatch); + await dispatchable(dispatch, getState); expect(apiClientMock.deleteTags.callCount).toEqual(1); expect(apiClientMock.deleteTags.getCall(0).args).toEqual([[ tag ]]); @@ -72,10 +73,10 @@ describe('tagDeleteReducer', () => { const error = 'Error'; const tag = 'foo'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _deleteTag(apiClientMock, tag); + const dispatchable = _deleteTag(() => apiClientMock, tag); try { - await dispatchable(dispatch); + await dispatchable(dispatch, getState); } catch (e) { expect(e).toEqual(error); } diff --git a/test/tags/reducers/tagEdit.test.js b/test/tags/reducers/tagEdit.test.js index 8a1ca80c..ed97cf67 100644 --- a/test/tags/reducers/tagEdit.test.js +++ b/test/tags/reducers/tagEdit.test.js @@ -55,6 +55,7 @@ describe('tagEditReducer', () => { setColorForKey: sinon.spy(), }; const dispatch = sinon.spy(); + const getState = () => ({}); afterEach(() => { colorGenerator.setColorForKey.resetHistory(); @@ -67,9 +68,9 @@ describe('tagEditReducer', () => { const newName = 'bar'; const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = _editTag(apiClientMock, colorGenerator, oldName, newName, color); + const dispatchable = _editTag(() => apiClientMock, colorGenerator, oldName, newName, color); - await dispatchable(dispatch); + await dispatchable(dispatch, getState); expect(apiClientMock.editTag.callCount).toEqual(1); expect(apiClientMock.editTag.getCall(0).args).toEqual([ oldName, newName ]); @@ -89,10 +90,10 @@ describe('tagEditReducer', () => { const newName = 'bar'; const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _editTag(apiClientMock, colorGenerator, oldName, newName, color); + const dispatchable = _editTag(() => apiClientMock, colorGenerator, oldName, newName, color); try { - await dispatchable(dispatch); + await dispatchable(dispatch, getState); } catch (e) { expect(e).toEqual(error); } diff --git a/test/visits/reducers/shortUrlDetail.test.js b/test/visits/reducers/shortUrlDetail.test.js index 8fac8e71..2be3426f 100644 --- a/test/visits/reducers/shortUrlDetail.test.js +++ b/test/visits/reducers/shortUrlDetail.test.js @@ -50,6 +50,7 @@ describe('shortUrlDetailReducer', () => { getShortUrl: sinon.fake.returns(returned), }); const dispatchMock = sinon.spy(); + const getState = () => ({}); beforeEach(() => dispatchMock.resetHistory()); @@ -57,7 +58,7 @@ describe('shortUrlDetailReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.reject()); const expectedDispatchCalls = 2; - await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock); + await _getShortUrlDetail(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; @@ -76,7 +77,7 @@ describe('shortUrlDetailReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl)); const expectedDispatchCalls = 2; - await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock); + await _getShortUrlDetail(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; diff --git a/test/visits/reducers/shortUrlVisits.test.js b/test/visits/reducers/shortUrlVisits.test.js index 04888841..f3e47ff1 100644 --- a/test/visits/reducers/shortUrlVisits.test.js +++ b/test/visits/reducers/shortUrlVisits.test.js @@ -50,6 +50,7 @@ describe('shortUrlVisitsReducer', () => { getShortUrlVisits: sinon.fake.returns(returned), }); const dispatchMock = sinon.spy(); + const getState = () => ({}); beforeEach(() => dispatchMock.resetHistory()); @@ -57,7 +58,7 @@ describe('shortUrlVisitsReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.reject()); const expectedDispatchCalls = 2; - await _getShortUrlVisits(ShlinkApiClient, 'abc123')(dispatchMock); + await _getShortUrlVisits(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; @@ -76,7 +77,7 @@ describe('shortUrlVisitsReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedVisits)); const expectedDispatchCalls = 2; - await _getShortUrlVisits(ShlinkApiClient, 'abc123')(dispatchMock); + await _getShortUrlVisits(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; From 471322f4db5e7ff6c79a828d0cf135cf9aab6739 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 11:28:15 +0100 Subject: [PATCH 11/18] Implemented dependency injection in all tag related components --- src/container/index.js | 22 ++++++++---- src/short-urls/SearchBar.js | 4 ++- src/short-urls/helpers/ShortUrlsRow.js | 4 ++- src/tags/TagCard.js | 10 +++--- src/tags/TagsList.js | 7 ++-- src/tags/helpers/DeleteTagConfirmModal.js | 13 ++----- src/tags/helpers/EditTagModal.js | 18 +++------- src/tags/helpers/Tag.js | 41 +++++++++++------------ src/tags/helpers/TagBullet.js | 22 +++++------- src/tags/helpers/TagsSelector.js | 2 +- test/short-urls/SearchBar.test.js | 4 +-- test/tags/TagCard.test.js | 4 ++- test/tags/TagsList.test.js | 5 +-- 13 files changed, 75 insertions(+), 81 deletions(-) diff --git a/src/container/index.js b/src/container/index.js index 88593418..8bd9537e 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -20,7 +20,6 @@ import SearchBar from '../short-urls/SearchBar'; import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; import ShortUrlsList from '../short-urls/ShortUrlsList'; import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams'; -import Tag from '../tags/helpers/Tag'; import { ColorGenerator } from '../utils/ColorGenerator'; import { Storage } from '../utils/Storage'; import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; @@ -41,6 +40,11 @@ import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-u import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; +import TagCard from '../tags/TagCard'; +import DeleteTagConfirmModal from '../tags/helpers/DeleteTagConfirmModal'; +import { deleteTag, tagDeleted } from '../tags/reducers/tagDelete'; +import EditTagModal from '../tags/helpers/EditTagModal'; +import { editTag, tagEdited } from '../tags/reducers/tagEdit'; const bottle = new Bottle(); const { container } = bottle; @@ -78,7 +82,7 @@ bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateSer bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); bottle.decorator('ServersDropdown', connectDecorator([ 'servers', 'selectedServer' ], { listServers, selectServer })); -bottle.serviceFactory('TagsList', () => TagsList); +bottle.serviceFactory('TagsList', TagsList, 'TagCard'); bottle.decorator('TagsList', connectDecorator([ 'tagsList' ], { forceListTags, filterTags })); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); @@ -86,7 +90,7 @@ bottle.decorator('ShortUrls', connect( (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) )); -bottle.serviceFactory('SearchBar', SearchBar, 'Tag'); +bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); bottle.decorator('SearchBar', connectDecorator([ 'shortUrlsListParams' ], { listShortUrls })); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); @@ -95,13 +99,11 @@ bottle.decorator('ShortUrlsList', connectDecorator( { listShortUrls, resetShortUrlParams } )); -bottle.serviceFactory('Tag', Tag, 'ColorGenerator'); - bottle.constant('localStorage', global.localStorage); bottle.service('Storage', Storage, 'localStorage'); bottle.service('ColorGenerator', ColorGenerator, 'Storage'); -bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'Tag', 'ShortUrlsRowMenu'); +bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator'); bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); @@ -151,4 +153,12 @@ bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); +bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator'); + +bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal); +bottle.decorator('DeleteTagConfirmModal', connectDecorator([ 'tagDelete' ], { deleteTag, tagDeleted })); + +bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator'); +bottle.decorator('EditTagModal', connectDecorator([ 'tagEdit' ], { editTag, tagEdited })); + export default container; diff --git a/src/short-urls/SearchBar.js b/src/short-urls/SearchBar.js index 640682b3..6e343c70 100644 --- a/src/short-urls/SearchBar.js +++ b/src/short-urls/SearchBar.js @@ -4,6 +4,7 @@ import React from 'react'; import { isEmpty } from 'ramda'; import PropTypes from 'prop-types'; import SearchField from '../utils/SearchField'; +import Tag from '../tags/helpers/Tag'; import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; import './SearchBar.scss'; @@ -12,7 +13,7 @@ const propTypes = { shortUrlsListParams: shortUrlsListParamsType, }; -const SearchBar = (Tag) => { +const SearchBar = (colorGenerator) => { const SearchBar = ({ listShortUrls, shortUrlsListParams }) => { const selectedTags = shortUrlsListParams.tags || []; @@ -29,6 +30,7 @@ const SearchBar = (Tag) => {   {selectedTags.map((tag) => ( class ShortUrlsRow extends React.Component { +const ShortUrlsRow = (ShortUrlsRowMenu, colorGenerator) => class ShortUrlsRow extends React.Component { static propTypes = { refreshList: PropTypes.func, shortUrlsListParams: shortUrlsListParamsType, @@ -29,6 +30,7 @@ const ShortUrlsRow = (Tag, ShortUrlsRowMenu) => class ShortUrlsRow extends React return tags.map((tag) => ( refreshList({ tags: [ ...selectedTags, tag ] })} diff --git a/src/tags/TagCard.js b/src/tags/TagCard.js index 538a86ac..9e2033cd 100644 --- a/src/tags/TagCard.js +++ b/src/tags/TagCard.js @@ -7,10 +7,8 @@ import React from 'react'; import { Link } from 'react-router-dom'; import TagBullet from './helpers/TagBullet'; import './TagCard.scss'; -import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal'; -import EditTagModal from './helpers/EditTagModal'; -export default class TagCard extends React.Component { +const TagCard = (DeleteTagConfirmModal, EditTagModal, colorGenerator) => class TagCard extends React.Component { static propTypes = { tag: PropTypes.string, currentServerId: PropTypes.string, @@ -35,7 +33,7 @@ export default class TagCard extends React.Component {
- + {tag}
@@ -45,4 +43,6 @@ export default class TagCard extends React.Component { ); } -} +}; + +export default TagCard; diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index edf1f1c2..1982f6fc 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -3,12 +3,11 @@ import { splitEvery } from 'ramda'; import PropTypes from 'prop-types'; import MuttedMessage from '../utils/MuttedMessage'; import SearchField from '../utils/SearchField'; -import TagCard from './TagCard'; const { ceil } = Math; const TAGS_GROUPS_AMOUNT = 4; -export default class TagsList extends React.Component { +const TagsList = (TagCard) => class TagsList extends React.Component { static propTypes = { filterTags: PropTypes.func, forceListTags: PropTypes.func, @@ -80,4 +79,6 @@ export default class TagsList extends React.Component { ); } -} +}; + +export default TagsList; diff --git a/src/tags/helpers/DeleteTagConfirmModal.js b/src/tags/helpers/DeleteTagConfirmModal.js index e201c799..a7e62718 100644 --- a/src/tags/helpers/DeleteTagConfirmModal.js +++ b/src/tags/helpers/DeleteTagConfirmModal.js @@ -1,11 +1,9 @@ import React from 'react'; -import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import PropTypes from 'prop-types'; -import { pick } from 'ramda'; -import { deleteTag, tagDeleted, tagDeleteType } from '../reducers/tagDelete'; +import { tagDeleteType } from '../reducers/tagDelete'; -export class DeleteTagConfirmModalComponent extends React.Component { +export default class DeleteTagConfirmModal extends React.Component { static propTypes = { tag: PropTypes.string.isRequired, toggle: PropTypes.func.isRequired, @@ -67,10 +65,3 @@ export class DeleteTagConfirmModalComponent extends React.Component { ); } } - -const DeleteTagConfirmModal = connect( - pick([ 'tagDelete' ]), - { deleteTag, tagDeleted } -)(DeleteTagConfirmModalComponent); - -export default DeleteTagConfirmModal; diff --git a/src/tags/helpers/EditTagModal.js b/src/tags/helpers/EditTagModal.js index 91e58bd9..f017fc49 100644 --- a/src/tags/helpers/EditTagModal.js +++ b/src/tags/helpers/EditTagModal.js @@ -1,31 +1,23 @@ import React from 'react'; -import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap'; -import { pick } from 'ramda'; import { ChromePicker } from 'react-color'; import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import PropTypes from 'prop-types'; -import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; -import { editTag, tagEdited } from '../reducers/tagEdit'; import './EditTagModal.scss'; -export class EditTagModalComponent extends React.Component { +const EditTagModal = ({ getColorForKey }) => class EditTagModal extends React.Component { static propTypes = { tag: PropTypes.string, editTag: PropTypes.func, toggle: PropTypes.func, tagEdited: PropTypes.func, - colorGenerator: colorGeneratorType, isOpen: PropTypes.bool, tagEdit: PropTypes.shape({ error: PropTypes.bool, editing: PropTypes.bool, }), }; - static defaultProps = { - colorGenerator, - }; saveTag = (e) => { e.preventDefault(); @@ -53,12 +45,12 @@ export class EditTagModalComponent extends React.Component { constructor(props) { super(props); - const { colorGenerator, tag } = props; + const { tag } = props; this.state = { showColorPicker: false, tag, - color: colorGenerator.getColorForKey(tag), + color: getColorForKey(tag), }; } @@ -131,8 +123,6 @@ export class EditTagModalComponent extends React.Component { ); } -} - -const EditTagModal = connect(pick([ 'tagEdit' ]), { editTag, tagEdited })(EditTagModalComponent); +}; export default EditTagModal; diff --git a/src/tags/helpers/Tag.js b/src/tags/helpers/Tag.js index dad93103..2356e818 100644 --- a/src/tags/helpers/Tag.js +++ b/src/tags/helpers/Tag.js @@ -1,36 +1,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import './Tag.scss'; +import { colorGeneratorType } from '../../utils/ColorGenerator'; const propTypes = { text: PropTypes.string, children: PropTypes.node, clearable: PropTypes.bool, + colorGenerator: colorGeneratorType, onClick: PropTypes.func, onClose: PropTypes.func, }; -const Tag = (colorGenerator) => { - const Tag = ({ - text, - children, - clearable, - onClick = () => {}, - onClose = () => {}, - }) => ( - - {children || text} - {clearable && ×} - - ); +const Tag = ({ + text, + children, + clearable, + colorGenerator, + onClick = () => {}, + onClose = () => {}, +}) => ( + + {children || text} + {clearable && ×} + +); - Tag.propTypes = propTypes; - - return Tag; -}; +Tag.propTypes = propTypes; export default Tag; diff --git a/src/tags/helpers/TagBullet.js b/src/tags/helpers/TagBullet.js index 1427613a..6afcd79d 100644 --- a/src/tags/helpers/TagBullet.js +++ b/src/tags/helpers/TagBullet.js @@ -1,24 +1,20 @@ import React from 'react'; import * as PropTypes from 'prop-types'; -import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; +import { colorGeneratorType } from '../../utils/ColorGenerator'; import './TagBullet.scss'; const propTypes = { tag: PropTypes.string.isRequired, colorGenerator: colorGeneratorType, }; -const defaultProps = { - colorGenerator, -}; -export default function TagBullet({ tag, colorGenerator }) { - return ( -
- ); -} +const TagBullet = ({ tag, colorGenerator }) => ( +
+); TagBullet.propTypes = propTypes; -TagBullet.defaultProps = defaultProps; + +export default TagBullet; diff --git a/src/tags/helpers/TagsSelector.js b/src/tags/helpers/TagsSelector.js index fdbfeffd..161a9237 100644 --- a/src/tags/helpers/TagsSelector.js +++ b/src/tags/helpers/TagsSelector.js @@ -54,7 +54,7 @@ const TagsSelector = (colorGenerator) => class TagsSelector extends React.Compon getSuggestionValue={(suggestion) => suggestion} renderSuggestion={(suggestion) => ( - + {suggestion} )} diff --git a/test/short-urls/SearchBar.test.js b/test/short-urls/SearchBar.test.js index ae811817..d3a3869e 100644 --- a/test/short-urls/SearchBar.test.js +++ b/test/short-urls/SearchBar.test.js @@ -3,12 +3,12 @@ import { shallow } from 'enzyme'; import sinon from 'sinon'; import searchBarCreator from '../../src/short-urls/SearchBar'; import SearchField from '../../src/utils/SearchField'; +import Tag from '../../src/tags/helpers/Tag'; describe('', () => { let wrapper; const listShortUrlsMock = sinon.spy(); - const Tag = () => ''; - const SearchBar = searchBarCreator(Tag); + const SearchBar = searchBarCreator({}); afterEach(() => { listShortUrlsMock.resetHistory(); diff --git a/test/tags/TagCard.test.js b/test/tags/TagCard.test.js index a905b07e..b8049ba1 100644 --- a/test/tags/TagCard.test.js +++ b/test/tags/TagCard.test.js @@ -1,13 +1,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Link } from 'react-router-dom'; -import TagCard from '../../src/tags/TagCard'; +import createTagCard from '../../src/tags/TagCard'; import TagBullet from '../../src/tags/helpers/TagBullet'; describe('', () => { let wrapper; beforeEach(() => { + const TagCard = createTagCard(() => '', () => '', {}); + wrapper = shallow(); }); afterEach(() => wrapper.unmount()); diff --git a/test/tags/TagsList.test.js b/test/tags/TagsList.test.js index ade440d9..0098eee9 100644 --- a/test/tags/TagsList.test.js +++ b/test/tags/TagsList.test.js @@ -2,16 +2,17 @@ import React from 'react'; import { shallow } from 'enzyme'; import { identity, range } from 'ramda'; import * as sinon from 'sinon'; -import TagsList from '../../src/tags/TagsList'; +import createTagsList from '../../src/tags/TagsList'; import MuttedMessage from '../../src/utils/MuttedMessage'; -import TagCard from '../../src/tags/TagCard'; import SearchField from '../../src/utils/SearchField'; describe('', () => { let wrapper; const filterTags = sinon.spy(); + const TagCard = () => ''; const createWrapper = (tagsList) => { const params = { serverId: '1' }; + const TagsList = createTagsList(TagCard); wrapper = shallow( From fa3e1eba9340452c39b464fcc2ab8ca86893b3a4 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 14:32:02 +0100 Subject: [PATCH 12/18] Moved all visits-related services to its own service provide function inside visits --- src/common/MenuLayout.js | 176 ++++++++++---------- src/container/index.js | 13 +- src/visits/ShortUrlVisits.js | 48 ++---- src/visits/container/provideServices.js | 22 +++ src/visits/reducers/shortUrlDetail.js | 6 +- src/visits/reducers/shortUrlVisits.js | 6 +- test/visits/ShortUrlVisits.test.js | 15 +- test/visits/reducers/shortUrlDetail.test.js | 6 +- test/visits/reducers/shortUrlVisits.test.js | 6 +- 9 files changed, 151 insertions(+), 147 deletions(-) create mode 100644 src/visits/container/provideServices.js diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index fc18676d..70a8e164 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -5,96 +5,96 @@ import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import * as PropTypes from 'prop-types'; -import ShortUrlsVisits from '../visits/ShortUrlVisits'; -import './MenuLayout.scss'; import { serverType } from '../servers/prop-types'; +import './MenuLayout.scss'; -const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl) => class MenuLayout extends React.Component { - static propTypes = { - match: PropTypes.object, - selectServer: PropTypes.func, - location: PropTypes.object, - selectedServer: serverType, +const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) => + class MenuLayout extends React.Component { + static propTypes = { + match: PropTypes.object, + selectServer: PropTypes.func, + location: PropTypes.object, + selectedServer: serverType, + }; + + state = { showSideBar: false }; + + // FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered + /* eslint react/no-deprecated: "off" */ + componentWillMount() { + const { match, selectServer } = this.props; + const { params: { serverId } } = match; + + selectServer(serverId); + } + + componentDidUpdate(prevProps) { + const { location } = this.props; + + // Hide sidebar when location changes + if (location !== prevProps.location) { + this.setState({ showSideBar: false }); + } + } + + render() { + const { selectedServer } = this.props; + const burgerClasses = classnames('menu-layout__burger-icon', { + 'menu-layout__burger-icon--active': this.state.showSideBar, + }); + + return ( + + this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))} + /> + + this.setState({ showSideBar: false })} + onSwipedRight={() => this.setState({ showSideBar: true })} + > +
+ +
this.setState({ showSideBar: false })} + > + + + + + + +
+
+
+
+ ); + } }; - state = { showSideBar: false }; - - // FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered - /* eslint react/no-deprecated: "off" */ - componentWillMount() { - const { match, selectServer } = this.props; - const { params: { serverId } } = match; - - selectServer(serverId); - } - - componentDidUpdate(prevProps) { - const { location } = this.props; - - // Hide sidebar when location changes - if (location !== prevProps.location) { - this.setState({ showSideBar: false }); - } - } - - render() { - const { selectedServer } = this.props; - const burgerClasses = classnames('menu-layout__burger-icon', { - 'menu-layout__burger-icon--active': this.state.showSideBar, - }); - - return ( - - this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))} - /> - - this.setState({ showSideBar: false })} - onSwipedRight={() => this.setState({ showSideBar: true })} - > -
- -
this.setState({ showSideBar: false })} - > - - - - - - -
-
-
-
- ); - } -}; - export default MenuLayout; diff --git a/src/container/index.js b/src/container/index.js index 8bd9537e..4aba91ea 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -45,6 +45,7 @@ import DeleteTagConfirmModal from '../tags/helpers/DeleteTagConfirmModal'; import { deleteTag, tagDeleted } from '../tags/reducers/tagDelete'; import EditTagModal from '../tags/helpers/EditTagModal'; import { editTag, tagEdited } from '../tags/reducers/tagEdit'; +import provideVisitsServices from '../visits/container/provideServices'; const bottle = new Bottle(); const { container } = bottle; @@ -70,7 +71,15 @@ bottle.decorator('MainHeader', withRouter); bottle.serviceFactory('Home', () => Home); bottle.decorator('Home', connectDecorator([ 'servers' ], { resetSelectedServer })); -bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu', 'CreateShortUrl'); +bottle.serviceFactory( + 'MenuLayout', + MenuLayout, + 'TagsList', + 'ShortUrls', + 'AsideMenu', + 'CreateShortUrl', + 'ShortUrlVisits' +); bottle.decorator('MenuLayout', connectDecorator([ 'selectedServer', 'shortUrlsListParams' ], { selectServer })); bottle.decorator('MenuLayout', withRouter); @@ -161,4 +170,6 @@ bottle.decorator('DeleteTagConfirmModal', connectDecorator([ 'tagDelete' ], { de bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator'); bottle.decorator('EditTagModal', connectDecorator([ 'tagEdit' ], { editTag, tagEdited })); +provideVisitsServices(bottle, connectDecorator); + export default container; diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index 037157de..35446fd3 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -1,31 +1,25 @@ import preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; -import { isEmpty, mapObjIndexed, pick } from 'ramda'; +import { isEmpty, mapObjIndexed } from 'ramda'; import React from 'react'; -import { connect } from 'react-redux'; import { Card } from 'reactstrap'; import PropTypes from 'prop-types'; import DateInput from '../utils/DateInput'; import MutedMessage from '../utils/MuttedMessage'; import SortableBarGraph from './SortableBarGraph'; -import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits'; -import { - processBrowserStats, - processCountriesStats, - processOsStats, - processReferrersStats, -} from './services/VisitsParser'; +import { shortUrlVisitsType } from './reducers/shortUrlVisits'; import { VisitsHeader } from './VisitsHeader'; import GraphCard from './GraphCard'; -import { getShortUrlDetail, shortUrlDetailType } from './reducers/shortUrlDetail'; +import { shortUrlDetailType } from './reducers/shortUrlDetail'; import './ShortUrlVisits.scss'; -export class ShortUrlsVisitsComponent extends React.Component { +const ShortUrlVisits = ({ + processOsStats, + processBrowserStats, + processCountriesStats, + processReferrersStats, +}) => class ShortUrlVisits extends React.Component { static propTypes = { - processOsStats: PropTypes.func, - processBrowserStats: PropTypes.func, - processCountriesStats: PropTypes.func, - processReferrersStats: PropTypes.func, match: PropTypes.shape({ params: PropTypes.object, }), @@ -34,12 +28,6 @@ export class ShortUrlsVisitsComponent extends React.Component { getShortUrlDetail: PropTypes.func, shortUrlDetail: shortUrlDetailType, }; - static defaultProps = { - processOsStats, - processBrowserStats, - processCountriesStats, - processReferrersStats, - }; state = { startDate: undefined, endDate: undefined }; loadVisits = () => { @@ -59,14 +47,7 @@ export class ShortUrlsVisitsComponent extends React.Component { } render() { - const { - processOsStats, - processBrowserStats, - processCountriesStats, - processReferrersStats, - shortUrlVisits, - shortUrlDetail, - } = this.props; + const { shortUrlVisits, shortUrlDetail } = this.props; const renderVisitsContent = () => { const { visits, loading, error } = shortUrlVisits; @@ -153,11 +134,6 @@ export class ShortUrlsVisitsComponent extends React.Component {
); } -} +}; -const ShortUrlsVisits = connect( - pick([ 'shortUrlVisits', 'shortUrlDetail' ]), - { getShortUrlVisits, getShortUrlDetail } -)(ShortUrlsVisitsComponent); - -export default ShortUrlsVisits; +export default ShortUrlVisits; diff --git a/src/visits/container/provideServices.js b/src/visits/container/provideServices.js new file mode 100644 index 00000000..a7aec43f --- /dev/null +++ b/src/visits/container/provideServices.js @@ -0,0 +1,22 @@ +import ShortUrlVisits from '../ShortUrlVisits'; +import { getShortUrlVisits } from '../reducers/shortUrlVisits'; +import { getShortUrlDetail } from '../reducers/shortUrlDetail'; +import * as visitsParser from '../services/VisitsParser'; + +const provideServices = (bottle, connect) => { + // Components + bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsParser'); + bottle.decorator('ShortUrlVisits', connect( + [ 'shortUrlVisits', 'shortUrlDetail' ], + [ 'getShortUrlVisits', 'getShortUrlDetail' ] + )); + + // Services + bottle.serviceFactory('VisitsParser', () => visitsParser); + + // Actions + bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient'); + bottle.serviceFactory('getShortUrlDetail', getShortUrlDetail, 'buildShlinkApiClient'); +}; + +export default provideServices; diff --git a/src/visits/reducers/shortUrlDetail.js b/src/visits/reducers/shortUrlDetail.js index b62c99ce..385c8071 100644 --- a/src/visits/reducers/shortUrlDetail.js +++ b/src/visits/reducers/shortUrlDetail.js @@ -1,6 +1,4 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { shortUrlType } from '../../short-urls/reducers/shortUrlsList'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -45,7 +43,7 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlDetail = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { +export const getShortUrlDetail = (buildShlinkApiClient) => (shortCode) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_DETAIL_START }); const { selectedServer } = getState(); @@ -59,5 +57,3 @@ export const _getShortUrlDetail = (buildShlinkApiClient, shortCode) => async (di dispatch({ type: GET_SHORT_URL_DETAIL_ERROR }); } }; - -export const getShortUrlDetail = curry(_getShortUrlDetail)(buildShlinkApiClient); diff --git a/src/visits/reducers/shortUrlVisits.js b/src/visits/reducers/shortUrlVisits.js index 4df1a09c..8bdedc33 100644 --- a/src/visits/reducers/shortUrlVisits.js +++ b/src/visits/reducers/shortUrlVisits.js @@ -1,6 +1,4 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; @@ -44,7 +42,7 @@ export default function reducer(state = initialState, action) { } } -export const _getShortUrlVisits = (buildShlinkApiClient, shortCode, dates) => async (dispatch, getState) => { +export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) => async (dispatch, getState) => { dispatch({ type: GET_SHORT_URL_VISITS_START }); const { selectedServer } = getState(); @@ -58,5 +56,3 @@ export const _getShortUrlVisits = (buildShlinkApiClient, shortCode, dates) => as dispatch({ type: GET_SHORT_URL_VISITS_ERROR }); } }; - -export const getShortUrlVisits = curry(_getShortUrlVisits)(buildShlinkApiClient); diff --git a/test/visits/ShortUrlVisits.test.js b/test/visits/ShortUrlVisits.test.js index f103987d..788623cb 100644 --- a/test/visits/ShortUrlVisits.test.js +++ b/test/visits/ShortUrlVisits.test.js @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import { identity } from 'ramda'; import { Card } from 'reactstrap'; import * as sinon from 'sinon'; -import { ShortUrlsVisitsComponent as ShortUrlsVisits } from '../../src/visits/ShortUrlVisits'; +import createShortUrlVisits from '../../src/visits/ShortUrlVisits'; import MutedMessage from '../../src/utils/MuttedMessage'; import GraphCard from '../../src/visits/GraphCard'; import DateInput from '../../src/utils/DateInput'; @@ -18,14 +18,17 @@ describe('', () => { }; const createComponent = (shortUrlVisits) => { + const ShortUrlVisits = createShortUrlVisits({ + processBrowserStats: statsProcessor, + processCountriesStats: statsProcessor, + processOsStats: statsProcessor, + processReferrersStats: statsProcessor, + }); + wrapper = shallow( - { const ShlinkApiClient = buildApiClientMock(Promise.reject()); const expectedDispatchCalls = 2; - await _getShortUrlDetail(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); + await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; @@ -77,7 +77,7 @@ describe('shortUrlDetailReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl)); const expectedDispatchCalls = 2; - await _getShortUrlDetail(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); + await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; diff --git a/test/visits/reducers/shortUrlVisits.test.js b/test/visits/reducers/shortUrlVisits.test.js index f3e47ff1..48c8dc9d 100644 --- a/test/visits/reducers/shortUrlVisits.test.js +++ b/test/visits/reducers/shortUrlVisits.test.js @@ -1,6 +1,6 @@ import * as sinon from 'sinon'; import reducer, { - _getShortUrlVisits, + getShortUrlVisits, GET_SHORT_URL_VISITS_START, GET_SHORT_URL_VISITS_ERROR, GET_SHORT_URL_VISITS, @@ -58,7 +58,7 @@ describe('shortUrlVisitsReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.reject()); const expectedDispatchCalls = 2; - await _getShortUrlVisits(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); + await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; @@ -77,7 +77,7 @@ describe('shortUrlVisitsReducer', () => { const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedVisits)); const expectedDispatchCalls = 2; - await _getShortUrlVisits(() => ShlinkApiClient, 'abc123')(dispatchMock, getState); + await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState); const [ firstCallArg ] = dispatchMock.getCall(0).args; const { type: firstCallType } = firstCallArg; From 566322a8c531cf50a1f53e46d0786c3d17f2ee08 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 14:54:54 +0100 Subject: [PATCH 13/18] Extracted tag related services to its own service provider --- src/container/index.js | 62 +++++++------------ src/tags/reducers/tagDelete.js | 6 +- src/tags/reducers/tagEdit.js | 8 +-- src/tags/services/provideServices.js | 37 +++++++++++ .../provideServices.js | 2 +- test/tags/reducers/tagDelete.test.js | 6 +- test/tags/reducers/tagEdit.test.js | 6 +- 7 files changed, 68 insertions(+), 59 deletions(-) create mode 100644 src/tags/services/provideServices.js rename src/visits/{container => services}/provideServices.js (92%) diff --git a/src/container/index.js b/src/container/index.js index 4aba91ea..70ba82dc 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -1,6 +1,6 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; -import { connect } from 'react-redux'; +import { connect as reduxConnect } from 'react-redux'; import { assoc, pick } from 'ramda'; import csvjson from 'csvjson'; import axios from 'axios'; @@ -13,8 +13,6 @@ import MenuLayout from '../common/MenuLayout'; import { createServer, createServers, deleteServer, listServers } from '../servers/reducers/server'; import CreateServer from '../servers/CreateServer'; import ServersDropdown from '../servers/ServersDropdown'; -import TagsList from '../tags/TagsList'; -import { filterTags, forceListTags, listTags } from '../tags/reducers/tagsList'; import ShortUrls from '../short-urls/ShortUrls'; import SearchBar from '../short-urls/SearchBar'; import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; @@ -34,18 +32,13 @@ import { ServersExporter } from '../servers/services/ServersExporter'; import { ServersService } from '../servers/services/ServersService'; import CreateShortUrl from '../short-urls/CreateShortUrl'; import { createShortUrl, resetCreateShortUrl } from '../short-urls/reducers/shortUrlCreation'; -import TagsSelector from '../tags/helpers/TagsSelector'; import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-urls/reducers/shortUrlDeletion'; import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; -import TagCard from '../tags/TagCard'; -import DeleteTagConfirmModal from '../tags/helpers/DeleteTagConfirmModal'; -import { deleteTag, tagDeleted } from '../tags/reducers/tagDelete'; -import EditTagModal from '../tags/helpers/EditTagModal'; -import { editTag, tagEdited } from '../tags/reducers/tagEdit'; -import provideVisitsServices from '../visits/container/provideServices'; +import provideVisitsServices from '../visits/services/provideServices'; +import provideTagsServices from '../tags/services/provideServices'; const bottle = new Bottle(); const { container } = bottle; @@ -56,8 +49,8 @@ const mapActionService = (map, actionName) => ({ // Wrap actual action service in a function so that it is lazily created the first time it is called [actionName]: (...args) => container[actionName](...args), }); -const connectDecorator = (propsFromState, actionServiceNames) => - connect( +const connect = (propsFromState, actionServiceNames) => + reduxConnect( pick(propsFromState), Array.isArray(actionServiceNames) ? actionServiceNames.reduce(mapActionService, {}) : actionServiceNames ); @@ -69,7 +62,7 @@ bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.decorator('MainHeader', withRouter); bottle.serviceFactory('Home', () => Home); -bottle.decorator('Home', connectDecorator([ 'servers' ], { resetSelectedServer })); +bottle.decorator('Home', connect([ 'servers' ], { resetSelectedServer })); bottle.serviceFactory( 'MenuLayout', @@ -80,30 +73,27 @@ bottle.serviceFactory( 'CreateShortUrl', 'ShortUrlVisits' ); -bottle.decorator('MenuLayout', connectDecorator([ 'selectedServer', 'shortUrlsListParams' ], { selectServer })); +bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], { selectServer })); bottle.decorator('MenuLayout', withRouter); bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn'); -bottle.decorator('CreateServer', connectDecorator([ 'selectedServer' ], { createServer, resetSelectedServer })); +bottle.decorator('CreateServer', connect([ 'selectedServer' ], { createServer, resetSelectedServer })); bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); -bottle.decorator('ServersDropdown', connectDecorator([ 'servers', 'selectedServer' ], { listServers, selectServer })); - -bottle.serviceFactory('TagsList', TagsList, 'TagCard'); -bottle.decorator('TagsList', connectDecorator([ 'tagsList' ], { forceListTags, filterTags })); +bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], { listServers, selectServer })); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); -bottle.decorator('ShortUrls', connect( +bottle.decorator('ShortUrls', reduxConnect( (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) )); bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); -bottle.decorator('SearchBar', connectDecorator([ 'shortUrlsListParams' ], { listShortUrls })); +bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], { listShortUrls })); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); -bottle.decorator('ShortUrlsList', connectDecorator( +bottle.decorator('ShortUrlsList', connect( [ 'selectedServer', 'shortUrlsListParams' ], { listShortUrls, resetShortUrlParams } )); @@ -112,6 +102,8 @@ bottle.constant('localStorage', global.localStorage); bottle.service('Storage', Storage, 'localStorage'); bottle.service('ColorGenerator', ColorGenerator, 'Storage'); +bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); + bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator'); bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); @@ -121,13 +113,13 @@ bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios'); bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); bottle.decorator('DeleteServerModal', withRouter); -bottle.decorator('DeleteServerModal', connect(null, { deleteServer })); +bottle.decorator('DeleteServerModal', reduxConnect(null, { deleteServer })); bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); -bottle.decorator('ImportServersBtn', connect(null, { createServers })); +bottle.decorator('ImportServersBtn', reduxConnect(null, { createServers })); bottle.constant('csvjson', csvjson); bottle.constant('window', global.window); @@ -136,22 +128,19 @@ bottle.service('ServersService', ServersService, 'Storage'); bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); -bottle.decorator('CreateShortUrl', connectDecorator([ 'shortUrlCreationResult' ], { +bottle.decorator('CreateShortUrl', connect([ 'shortUrlCreationResult' ], { createShortUrl, resetCreateShortUrl, })); -bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator'); -bottle.decorator('TagsSelector', connectDecorator([ 'tagsList' ], { listTags })); - bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); -bottle.decorator('DeleteShortUrlModal', connectDecorator( +bottle.decorator('DeleteShortUrlModal', connect( [ 'shortUrlDeletion' ], { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } )); bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); -bottle.decorator('EditTagsModal', connectDecorator( +bottle.decorator('EditTagsModal', connect( [ 'shortUrlTags' ], [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] )); @@ -160,16 +149,7 @@ bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClien bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); -bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); - -bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator'); - -bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal); -bottle.decorator('DeleteTagConfirmModal', connectDecorator([ 'tagDelete' ], { deleteTag, tagDeleted })); - -bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator'); -bottle.decorator('EditTagModal', connectDecorator([ 'tagEdit' ], { editTag, tagEdited })); - -provideVisitsServices(bottle, connectDecorator); +provideTagsServices(bottle, connect); +provideVisitsServices(bottle, connect); export default container; diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.js index e4ea9410..03c42947 100644 --- a/src/tags/reducers/tagDelete.js +++ b/src/tags/reducers/tagDelete.js @@ -1,6 +1,4 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; @@ -41,7 +39,7 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteTag = (buildShlinkApiClient, tag) => async (dispatch, getState) => { +export const deleteTag = (buildShlinkApiClient) => (tag) => async (dispatch, getState) => { dispatch({ type: DELETE_TAG_START }); const { selectedServer } = getState(); @@ -57,6 +55,4 @@ export const _deleteTag = (buildShlinkApiClient, tag) => async (dispatch, getSta } }; -export const deleteTag = curry(_deleteTag)(buildShlinkApiClient); - export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js index 53bb25c4..950e95db 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.js @@ -1,6 +1,4 @@ -import { curry, pick } from 'ramda'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; -import colorGenerator from '../../utils/ColorGenerator'; +import { pick } from 'ramda'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START'; @@ -42,7 +40,7 @@ export default function reducer(state = defaultState, action) { } } -export const _editTag = (buildShlinkApiClient, colorGenerator, oldName, newName, color) => async ( +export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newName, color) => async ( dispatch, getState ) => { @@ -62,8 +60,6 @@ export const _editTag = (buildShlinkApiClient, colorGenerator, oldName, newName, } }; -export const editTag = curry(_editTag)(buildShlinkApiClient, colorGenerator); - export const tagEdited = (oldName, newName, color) => ({ type: TAG_EDITED, oldName, diff --git a/src/tags/services/provideServices.js b/src/tags/services/provideServices.js new file mode 100644 index 00000000..ba7c7be9 --- /dev/null +++ b/src/tags/services/provideServices.js @@ -0,0 +1,37 @@ +import TagsSelector from '../helpers/TagsSelector'; +import TagCard from '../TagCard'; +import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal'; +import EditTagModal from '../helpers/EditTagModal'; +import TagsList from '../TagsList'; +import { filterTags, forceListTags, listTags } from '../reducers/tagsList'; +import { deleteTag, tagDeleted } from '../reducers/tagDelete'; +import { editTag, tagEdited } from '../reducers/tagEdit'; + +const provideServices = (bottle, connect) => { + // Components + bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator'); + bottle.decorator('TagsSelector', connect([ 'tagsList' ], [ 'listTags' ])); + + bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator'); + + bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal); + bottle.decorator('DeleteTagConfirmModal', connect([ 'tagDelete' ], [ 'deleteTag', 'tagDeleted' ])); + + bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator'); + bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ])); + + bottle.serviceFactory('TagsList', TagsList, 'TagCard'); + bottle.decorator('TagsList', connect([ 'tagsList' ], [ 'forceListTags', 'filterTags' ])); + + // Actions + bottle.serviceFactory('filterTags', () => filterTags); + bottle.serviceFactory('forceListTags', () => forceListTags); + bottle.serviceFactory('listTags', () => listTags); + bottle.serviceFactory('tagDeleted', () => tagDeleted); + bottle.serviceFactory('tagEdited', () => tagEdited); + + bottle.serviceFactory('deleteTag', deleteTag, 'buildShlinkApiClient'); + bottle.serviceFactory('editTag', editTag, 'buildShlinkApiClient', 'ColorGenerator'); +}; + +export default provideServices; diff --git a/src/visits/container/provideServices.js b/src/visits/services/provideServices.js similarity index 92% rename from src/visits/container/provideServices.js rename to src/visits/services/provideServices.js index a7aec43f..0b754868 100644 --- a/src/visits/container/provideServices.js +++ b/src/visits/services/provideServices.js @@ -1,7 +1,7 @@ import ShortUrlVisits from '../ShortUrlVisits'; import { getShortUrlVisits } from '../reducers/shortUrlVisits'; import { getShortUrlDetail } from '../reducers/shortUrlDetail'; -import * as visitsParser from '../services/VisitsParser'; +import * as visitsParser from './VisitsParser'; const provideServices = (bottle, connect) => { // Components diff --git a/test/tags/reducers/tagDelete.test.js b/test/tags/reducers/tagDelete.test.js index 26450c9e..b3cc8c8e 100644 --- a/test/tags/reducers/tagDelete.test.js +++ b/test/tags/reducers/tagDelete.test.js @@ -5,7 +5,7 @@ import reducer, { DELETE_TAG, TAG_DELETED, tagDeleted, - _deleteTag, + deleteTag, } from '../../../src/tags/reducers/tagDelete'; describe('tagDeleteReducer', () => { @@ -56,7 +56,7 @@ describe('tagDeleteReducer', () => { const expectedDispatchCalls = 2; const tag = 'foo'; const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = _deleteTag(() => apiClientMock, tag); + const dispatchable = deleteTag(() => apiClientMock)(tag); await dispatchable(dispatch, getState); @@ -73,7 +73,7 @@ describe('tagDeleteReducer', () => { const error = 'Error'; const tag = 'foo'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _deleteTag(() => apiClientMock, tag); + const dispatchable = deleteTag(() => apiClientMock)(tag); try { await dispatchable(dispatch, getState); diff --git a/test/tags/reducers/tagEdit.test.js b/test/tags/reducers/tagEdit.test.js index ed97cf67..91642c06 100644 --- a/test/tags/reducers/tagEdit.test.js +++ b/test/tags/reducers/tagEdit.test.js @@ -5,7 +5,7 @@ import reducer, { EDIT_TAG, TAG_EDITED, tagEdited, - _editTag, + editTag, } from '../../../src/tags/reducers/tagEdit'; describe('tagEditReducer', () => { @@ -68,7 +68,7 @@ describe('tagEditReducer', () => { const newName = 'bar'; const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.resolve()); - const dispatchable = _editTag(() => apiClientMock, colorGenerator, oldName, newName, color); + const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); await dispatchable(dispatch, getState); @@ -90,7 +90,7 @@ describe('tagEditReducer', () => { const newName = 'bar'; const color = '#ff0000'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _editTag(() => apiClientMock, colorGenerator, oldName, newName, color); + const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color); try { await dispatchable(dispatch, getState); From cf1239cf6e2912cf1b36815d91459a7cb9c17841 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 19:45:09 +0100 Subject: [PATCH 14/18] Moved all server-related services to its own service provider --- src/container/index.js | 62 ++++++-------------- src/servers/reducers/selectedServer.js | 7 +-- src/servers/reducers/server.js | 25 +++----- src/servers/services/provideServices.js | 47 +++++++++++++++ src/short-urls/reducers/shortUrlsList.js | 7 +-- test/servers/reducers/selectedServer.test.js | 8 +-- test/servers/reducers/server.test.js | 16 ++--- 7 files changed, 88 insertions(+), 84 deletions(-) create mode 100644 src/servers/services/provideServices.js diff --git a/src/container/index.js b/src/container/index.js index 70ba82dc..a5e8ceda 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -2,17 +2,13 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect as reduxConnect } from 'react-redux'; import { assoc, pick } from 'ramda'; -import csvjson from 'csvjson'; import axios from 'axios'; import App from '../App'; import ScrollToTop from '../common/ScrollToTop'; import MainHeader from '../common/MainHeader'; -import { resetSelectedServer, selectServer } from '../servers/reducers/selectedServer'; +import { resetSelectedServer } from '../servers/reducers/selectedServer'; import Home from '../common/Home'; import MenuLayout from '../common/MenuLayout'; -import { createServer, createServers, deleteServer, listServers } from '../servers/reducers/server'; -import CreateServer from '../servers/CreateServer'; -import ServersDropdown from '../servers/ServersDropdown'; import ShortUrls from '../short-urls/ShortUrls'; import SearchBar from '../short-urls/SearchBar'; import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; @@ -22,14 +18,7 @@ import { ColorGenerator } from '../utils/ColorGenerator'; import { Storage } from '../utils/Storage'; import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu'; -import ShlinkApiClient from '../api/ShlinkApiClient'; -import DeleteServerModal from '../servers/DeleteServerModal'; -import DeleteServerButton from '../servers/DeleteServerButton'; import AsideMenu from '../common/AsideMenu'; -import ImportServersBtn from '../servers/helpers/ImportServersBtn'; -import { ServersImporter } from '../servers/services/ServersImporter'; -import { ServersExporter } from '../servers/services/ServersExporter'; -import { ServersService } from '../servers/services/ServersService'; import CreateShortUrl from '../short-urls/CreateShortUrl'; import { createShortUrl, resetCreateShortUrl } from '../short-urls/reducers/shortUrlCreation'; import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; @@ -37,6 +26,7 @@ import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-u import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; +import provideServersServices from '../servers/services/provideServices'; import provideVisitsServices from '../visits/services/provideServices'; import provideTagsServices from '../tags/services/provideServices'; @@ -51,13 +41,15 @@ const mapActionService = (map, actionName) => ({ }); const connect = (propsFromState, actionServiceNames) => reduxConnect( - pick(propsFromState), + propsFromState ? pick(propsFromState) : null, Array.isArray(actionServiceNames) ? actionServiceNames.reduce(mapActionService, {}) : actionServiceNames ); bottle.constant('ScrollToTop', ScrollToTop); bottle.decorator('ScrollToTop', withRouter); +bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); + bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.decorator('MainHeader', withRouter); @@ -73,16 +65,17 @@ bottle.serviceFactory( 'CreateShortUrl', 'ShortUrlVisits' ); -bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], { selectServer })); +bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); bottle.decorator('MenuLayout', withRouter); -bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn'); -bottle.decorator('CreateServer', connect([ 'selectedServer' ], { createServer, resetSelectedServer })); +bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); -bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); +bottle.constant('localStorage', global.localStorage); +bottle.service('Storage', Storage, 'localStorage'); +bottle.service('ColorGenerator', ColorGenerator, 'Storage'); -bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); -bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], { listServers, selectServer })); +bottle.constant('axios', axios); +bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); bottle.decorator('ShortUrls', reduxConnect( @@ -95,38 +88,13 @@ bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], { listShortUrls bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); bottle.decorator('ShortUrlsList', connect( [ 'selectedServer', 'shortUrlsListParams' ], - { listShortUrls, resetShortUrlParams } + [ 'listShortUrls', 'resetShortUrlParams' ] )); -bottle.constant('localStorage', global.localStorage); -bottle.service('Storage', Storage, 'localStorage'); -bottle.service('ColorGenerator', ColorGenerator, 'Storage'); - -bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); - bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator'); bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); -bottle.constant('axios', axios); -bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios'); - -bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); -bottle.decorator('DeleteServerModal', withRouter); -bottle.decorator('DeleteServerModal', reduxConnect(null, { deleteServer })); - -bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); -bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); - -bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); -bottle.decorator('ImportServersBtn', reduxConnect(null, { createServers })); - -bottle.constant('csvjson', csvjson); -bottle.constant('window', global.window); -bottle.service('ServersImporter', ServersImporter, 'csvjson'); -bottle.service('ServersService', ServersService, 'Storage'); -bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); - bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); bottle.decorator('CreateShortUrl', connect([ 'shortUrlCreationResult' ], { createShortUrl, @@ -149,6 +117,10 @@ bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClien bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); +bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); +bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams); + +provideServersServices(bottle, connect, withRouter); provideTagsServices(bottle, connect); provideVisitsServices(bottle, connect); diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index d53f21c3..dfee15ec 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,4 +1,3 @@ -import serversService from '../../servers/services/ServersService'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -21,9 +20,11 @@ export default function reducer(state = defaultState, action) { export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); -export const _selectServer = (serversService) => (serverId) => (dispatch) => { +export const selectServer = (serversService) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); + console.log('Setting server'); + const selectedServer = serversService.findServerById(serverId); dispatch({ @@ -31,5 +32,3 @@ export const _selectServer = (serversService) => (serverId) => (dispatch) => { selectedServer, }); }; - -export const selectServer = _selectServer(serversService); diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index cf2db109..503b303e 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -1,6 +1,3 @@ -import { curry } from 'ramda'; -import serversService from '../services/ServersService'; - export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS'; export default function reducer(state = {}, action) { @@ -12,33 +9,25 @@ export default function reducer(state = {}, action) { } } -export const _listServers = (serversService) => ({ +export const listServers = (serversService) => () => ({ type: FETCH_SERVERS, servers: serversService.listServers(), }); -export const listServers = () => _listServers(serversService); - -export const _createServer = (serversService, server) => { +export const createServer = (serversService) => (server) => { serversService.createServer(server); - return _listServers(serversService); + return listServers(serversService)(); }; -export const createServer = curry(_createServer)(serversService); - -export const _deleteServer = (serversService, server) => { +export const deleteServer = (serversService) => (server) => { serversService.deleteServer(server); - return _listServers(serversService); + return listServers(serversService)(); }; -export const deleteServer = curry(_deleteServer)(serversService); - -export const _createServers = (serversService, servers) => { +export const createServers = (serversService) => (servers) => { serversService.createServers(servers); - return _listServers(serversService); + return listServers(serversService)(); }; - -export const createServers = curry(_createServers)(serversService); diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js new file mode 100644 index 00000000..bbc026fc --- /dev/null +++ b/src/servers/services/provideServices.js @@ -0,0 +1,47 @@ +import csvjson from 'csvjson'; +import CreateServer from '../CreateServer'; +import ServersDropdown from '../ServersDropdown'; +import DeleteServerModal from '../DeleteServerModal'; +import DeleteServerButton from '../DeleteServerButton'; +import ImportServersBtn from '../helpers/ImportServersBtn'; +import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; +import { createServer, createServers, deleteServer, listServers } from '../reducers/server'; +import { ServersImporter } from './ServersImporter'; +import { ServersService } from './ServersService'; +import { ServersExporter } from './ServersExporter'; + +const provideServices = (bottle, connect, withRouter) => { + // Components + bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn'); + bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ])); + + bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); + bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers', 'selectServer' ])); + + bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal); + bottle.decorator('DeleteServerModal', withRouter); + bottle.decorator('DeleteServerModal', connect(null, [ 'deleteServer' ])); + + bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal'); + + bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter'); + bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ])); + + // Services + bottle.constant('csvjson', csvjson); + bottle.constant('window', global.window); + bottle.service('ServersImporter', ServersImporter, 'csvjson'); + bottle.service('ServersService', ServersService, 'Storage'); + bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson'); + + // Actions + bottle.serviceFactory('selectServer', selectServer, 'ServersService'); + bottle.serviceFactory('createServer', createServer, 'ServersService'); + bottle.serviceFactory('createServers', createServers, 'ServersService'); + bottle.serviceFactory('deleteServer', deleteServer, 'ServersService'); + bottle.serviceFactory('listServers', listServers, 'ServersService'); + + bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer); +}; + +export default provideServices; diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index de1df19e..eec4983f 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,6 +1,5 @@ import { assoc, assocPath, reject } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; @@ -55,10 +54,10 @@ export default function reducer(state = initialState, action) { } } -export const _listShortUrls = (buildShlinkApiClient, params = {}) => async (dispatch, getState) => { +export const listShortUrls = (buildShlinkApiClient) => (params = {}) => async (dispatch, getState) => { dispatch({ type: LIST_SHORT_URLS_START }); - const { selectedServer } = getState(); + const { selectedServer = {} } = getState(); const shlinkApiClient = buildShlinkApiClient(selectedServer); try { @@ -69,5 +68,3 @@ export const _listShortUrls = (buildShlinkApiClient, params = {}) => async (disp dispatch({ type: LIST_SHORT_URLS_ERROR, params }); } }; - -export const listShortUrls = (params = {}) => _listShortUrls(buildShlinkApiClient, params); diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index 2b99d5d7..2316e583 100644 --- a/test/servers/reducers/selectedServer.test.js +++ b/test/servers/reducers/selectedServer.test.js @@ -1,8 +1,8 @@ import * as sinon from 'sinon'; import reducer, { - _selectServer, - RESET_SELECTED_SERVER, + selectServer, resetSelectedServer, + RESET_SELECTED_SERVER, SELECT_SERVER, } from '../../../src/servers/reducers/selectedServer'; import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams'; @@ -45,7 +45,7 @@ describe('selectedServerReducer', () => { const dispatch = sinon.spy(); const expectedDispatchCalls = 2; - _selectServer(ServersServiceMock)(serverId)(dispatch); + selectServer(ServersServiceMock)(serverId)(dispatch); expect(dispatch.callCount).toEqual(expectedDispatchCalls); expect(dispatch.firstCall.calledWith({ type: RESET_SHORT_URL_PARAMS })).toEqual(true); @@ -56,7 +56,7 @@ describe('selectedServerReducer', () => { }); it('invokes dependencies', () => { - _selectServer(ServersServiceMock)(serverId)(() => {}); + selectServer(ServersServiceMock)(serverId)(() => {}); expect(ServersServiceMock.findServerById.callCount).toEqual(1); }); diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index 2449aba4..9e87236b 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -1,10 +1,10 @@ import * as sinon from 'sinon'; import { values } from 'ramda'; import reducer, { - _createServer, - _deleteServer, - _listServers, - _createServers, + createServer, + deleteServer, + listServers, + createServers, FETCH_SERVERS, } from '../../../src/servers/reducers/server'; @@ -38,7 +38,7 @@ describe('serverReducer', () => { describe('listServers', () => { it('fetches servers and returns them as part of the action', () => { - const result = _listServers(ServersServiceMock); + const result = listServers(ServersServiceMock)(); expect(result).toEqual({ type: FETCH_SERVERS, servers }); expect(ServersServiceMock.listServers.callCount).toEqual(1); @@ -51,7 +51,7 @@ describe('serverReducer', () => { describe('createServer', () => { it('adds new server and then fetches servers again', () => { const serverToCreate = { id: 'abc123' }; - const result = _createServer(ServersServiceMock, serverToCreate); + const result = createServer(ServersServiceMock)(serverToCreate); expect(result).toEqual({ type: FETCH_SERVERS, servers }); expect(ServersServiceMock.listServers.callCount).toEqual(1); @@ -65,7 +65,7 @@ describe('serverReducer', () => { describe('deleteServer', () => { it('deletes a server and then fetches servers again', () => { const serverToDelete = { id: 'abc123' }; - const result = _deleteServer(ServersServiceMock, serverToDelete); + const result = deleteServer(ServersServiceMock)(serverToDelete); expect(result).toEqual({ type: FETCH_SERVERS, servers }); expect(ServersServiceMock.listServers.callCount).toEqual(1); @@ -79,7 +79,7 @@ describe('serverReducer', () => { describe('createServer', () => { it('creates multiple servers and then fetches servers again', () => { const serversToCreate = values(servers); - const result = _createServers(ServersServiceMock, serversToCreate); + const result = createServers(ServersServiceMock)(serversToCreate); expect(result).toEqual({ type: FETCH_SERVERS, servers }); expect(ServersServiceMock.listServers.callCount).toEqual(1); From 4b1f5e9f4cf1b7f6aeda5fa25ed41ea94fcb312d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 19:59:50 +0100 Subject: [PATCH 15/18] Extracted short-url related services to its own service provider --- src/container/index.js | 67 ++--------------- src/servers/reducers/selectedServer.js | 2 - src/servers/services/ServersExporter.js | 8 +-- src/servers/services/ServersImporter.js | 7 +- src/servers/services/ServersService.js | 7 +- src/servers/services/provideServices.js | 6 +- src/short-urls/reducers/shortUrlCreation.js | 6 +- src/short-urls/reducers/shortUrlDeletion.js | 6 +- src/short-urls/services/provideServices.js | 71 +++++++++++++++++++ src/utils/ColorGenerator.js | 7 +- src/utils/Storage.js | 14 +--- test/servers/services/ServersExporter.test.js | 2 +- test/servers/services/ServersImporter.test.js | 2 +- test/servers/services/ServersService.test.js | 2 +- .../reducers/shortUrlCreation.test.js | 6 +- test/utils/ColorGenerator.test.js | 2 +- 16 files changed, 94 insertions(+), 121 deletions(-) create mode 100644 src/short-urls/services/provideServices.js diff --git a/src/container/index.js b/src/container/index.js index a5e8ceda..79a65834 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -1,31 +1,18 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect as reduxConnect } from 'react-redux'; -import { assoc, pick } from 'ramda'; +import { pick } from 'ramda'; import axios from 'axios'; import App from '../App'; import ScrollToTop from '../common/ScrollToTop'; import MainHeader from '../common/MainHeader'; -import { resetSelectedServer } from '../servers/reducers/selectedServer'; import Home from '../common/Home'; import MenuLayout from '../common/MenuLayout'; -import ShortUrls from '../short-urls/ShortUrls'; -import SearchBar from '../short-urls/SearchBar'; -import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; -import ShortUrlsList from '../short-urls/ShortUrlsList'; -import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams'; -import { ColorGenerator } from '../utils/ColorGenerator'; -import { Storage } from '../utils/Storage'; -import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; -import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu'; import AsideMenu from '../common/AsideMenu'; -import CreateShortUrl from '../short-urls/CreateShortUrl'; -import { createShortUrl, resetCreateShortUrl } from '../short-urls/reducers/shortUrlCreation'; -import DeleteShortUrlModal from '../short-urls/helpers/DeleteShortUrlModal'; -import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-urls/reducers/shortUrlDeletion'; -import EditTagsModal from '../short-urls/helpers/EditTagsModal'; -import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; +import ColorGenerator from '../utils/ColorGenerator'; +import Storage from '../utils/Storage'; import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; +import provideShortUrlsServices from '../short-urls/services/provideServices'; import provideServersServices from '../servers/services/provideServices'; import provideVisitsServices from '../visits/services/provideServices'; import provideTagsServices from '../tags/services/provideServices'; @@ -54,7 +41,7 @@ bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); bottle.decorator('MainHeader', withRouter); bottle.serviceFactory('Home', () => Home); -bottle.decorator('Home', connect([ 'servers' ], { resetSelectedServer })); +bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ])); bottle.serviceFactory( 'MenuLayout', @@ -77,49 +64,7 @@ bottle.service('ColorGenerator', ColorGenerator, 'Storage'); bottle.constant('axios', axios); bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); -bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); -bottle.decorator('ShortUrls', reduxConnect( - (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) -)); - -bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); -bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], { listShortUrls })); - -bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); -bottle.decorator('ShortUrlsList', connect( - [ 'selectedServer', 'shortUrlsListParams' ], - [ 'listShortUrls', 'resetShortUrlParams' ] -)); - -bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator'); - -bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); - -bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); -bottle.decorator('CreateShortUrl', connect([ 'shortUrlCreationResult' ], { - createShortUrl, - resetCreateShortUrl, -})); - -bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); -bottle.decorator('DeleteShortUrlModal', connect( - [ 'shortUrlDeletion' ], - { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } -)); - -bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); -bottle.decorator('EditTagsModal', connect( - [ 'shortUrlTags' ], - [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] -)); - -bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); -bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); -bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); - -bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); -bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams); - +provideShortUrlsServices(bottle, connect); provideServersServices(bottle, connect, withRouter); provideTagsServices(bottle, connect); provideVisitsServices(bottle, connect); diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index dfee15ec..51b53e86 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -23,8 +23,6 @@ export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); export const selectServer = (serversService) => (serverId) => (dispatch) => { dispatch(resetShortUrlParams()); - console.log('Setting server'); - const selectedServer = serversService.findServerById(serverId); dispatch({ diff --git a/src/servers/services/ServersExporter.js b/src/servers/services/ServersExporter.js index 7262ac0f..9caf5ab9 100644 --- a/src/servers/services/ServersExporter.js +++ b/src/servers/services/ServersExporter.js @@ -1,6 +1,4 @@ import { dissoc, head, keys, values } from 'ramda'; -import csvjson from 'csvjson'; -import serversService from './ServersService'; const saveCsv = (window, csv) => { const { navigator, document } = window; @@ -26,7 +24,7 @@ const saveCsv = (window, csv) => { document.body.removeChild(link); }; -export class ServersExporter { +export default class ServersExporter { constructor(serversService, window, csvjson) { this.serversService = serversService; this.window = window; @@ -49,7 +47,3 @@ export class ServersExporter { } }; } - -const serverExporter = new ServersExporter(serversService, global.window, csvjson); - -export default serverExporter; diff --git a/src/servers/services/ServersImporter.js b/src/servers/services/ServersImporter.js index 0734952c..af855a64 100644 --- a/src/servers/services/ServersImporter.js +++ b/src/servers/services/ServersImporter.js @@ -1,11 +1,10 @@ -import csvjson from 'csvjson'; import PropTypes from 'prop-types'; export const serversImporterType = PropTypes.shape({ importServersFromFile: PropTypes.func, }); -export class ServersImporter { +export default class ServersImporter { constructor(csvjson) { this.csvjson = csvjson; } @@ -28,7 +27,3 @@ export class ServersImporter { }); }; } - -const serversImporter = new ServersImporter(csvjson); - -export default serversImporter; diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js index 25568526..52bb8fed 100644 --- a/src/servers/services/ServersService.js +++ b/src/servers/services/ServersService.js @@ -1,9 +1,8 @@ import { assoc, dissoc, reduce } from 'ramda'; -import storage from '../../utils/Storage'; const SERVERS_STORAGE_KEY = 'servers'; -export class ServersService { +export default class ServersService { constructor(storage) { this.storage = storage; } @@ -30,7 +29,3 @@ export class ServersService { dissoc(server.id, this.listServers()) ); } - -const serversService = new ServersService(storage); - -export default serversService; diff --git a/src/servers/services/provideServices.js b/src/servers/services/provideServices.js index bbc026fc..a2464749 100644 --- a/src/servers/services/provideServices.js +++ b/src/servers/services/provideServices.js @@ -6,9 +6,9 @@ import DeleteServerButton from '../DeleteServerButton'; import ImportServersBtn from '../helpers/ImportServersBtn'; import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; import { createServer, createServers, deleteServer, listServers } from '../reducers/server'; -import { ServersImporter } from './ServersImporter'; -import { ServersService } from './ServersService'; -import { ServersExporter } from './ServersExporter'; +import ServersImporter from './ServersImporter'; +import ServersService from './ServersService'; +import ServersExporter from './ServersExporter'; const provideServices = (bottle, connect, withRouter) => { // Components diff --git a/src/short-urls/reducers/shortUrlCreation.js b/src/short-urls/reducers/shortUrlCreation.js index 80150550..8a6d772f 100644 --- a/src/short-urls/reducers/shortUrlCreation.js +++ b/src/short-urls/reducers/shortUrlCreation.js @@ -1,6 +1,4 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; @@ -50,7 +48,7 @@ export default function reducer(state = defaultState, action) { } } -export const _createShortUrl = (buildShlinkApiClient, data) => async (dispatch, getState) => { +export const createShortUrl = (buildShlinkApiClient) => (data) => async (dispatch, getState) => { dispatch({ type: CREATE_SHORT_URL_START }); const { selectedServer } = getState(); @@ -65,6 +63,4 @@ export const _createShortUrl = (buildShlinkApiClient, data) => async (dispatch, } }; -export const createShortUrl = curry(_createShortUrl)(buildShlinkApiClient); - export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL }); diff --git a/src/short-urls/reducers/shortUrlDeletion.js b/src/short-urls/reducers/shortUrlDeletion.js index 20812079..aeb46ed5 100644 --- a/src/short-urls/reducers/shortUrlDeletion.js +++ b/src/short-urls/reducers/shortUrlDeletion.js @@ -1,6 +1,4 @@ -import { curry } from 'ramda'; import PropTypes from 'prop-types'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements, newline-after-var */ const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START'; @@ -56,7 +54,7 @@ export default function reducer(state = defaultState, action) { } } -export const _deleteShortUrl = (buildShlinkApiClient, shortCode) => async (dispatch, getState) => { +export const deleteShortUrl = (buildShlinkApiClient) => (shortCode) => async (dispatch, getState) => { dispatch({ type: DELETE_SHORT_URL_START }); const { selectedServer } = getState(); @@ -72,8 +70,6 @@ export const _deleteShortUrl = (buildShlinkApiClient, shortCode) => async (dispa } }; -export const deleteShortUrl = curry(_deleteShortUrl)(buildShlinkApiClient); - export const resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL }); export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode }); diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js new file mode 100644 index 00000000..c86811f4 --- /dev/null +++ b/src/short-urls/services/provideServices.js @@ -0,0 +1,71 @@ +import { connect as reduxConnect } from 'react-redux'; +import { assoc } from 'ramda'; +import ShortUrls from '../ShortUrls'; +import SearchBar from '../SearchBar'; +import ShortUrlsList from '../ShortUrlsList'; +import ShortUrlsRow from '../helpers/ShortUrlsRow'; +import ShortUrlsRowMenu from '../helpers/ShortUrlsRowMenu'; +import CreateShortUrl from '../CreateShortUrl'; +import DeleteShortUrlModal from '../helpers/DeleteShortUrlModal'; +import EditTagsModal from '../helpers/EditTagsModal'; +import { listShortUrls } from '../reducers/shortUrlsList'; +import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation'; +import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion'; +import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../reducers/shortUrlTags'; +import { resetShortUrlParams } from '../reducers/shortUrlsListParams'; + +const provideServices = (bottle, connect) => { + // Components + bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); + bottle.decorator('ShortUrls', reduxConnect( + (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) + )); + + bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator'); + bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ])); + + bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); + bottle.decorator('ShortUrlsList', connect( + [ 'selectedServer', 'shortUrlsListParams' ], + [ 'listShortUrls', 'resetShortUrlParams' ] + )); + + bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator'); + + bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); + + bottle.serviceFactory('CreateShortUrl', CreateShortUrl, 'TagsSelector'); + bottle.decorator( + 'CreateShortUrl', + connect([ 'shortUrlCreationResult' ], [ 'createShortUrl', 'resetCreateShortUrl' ]) + ); + + bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); + bottle.decorator('DeleteShortUrlModal', connect( + [ 'shortUrlDeletion' ], + [ 'deleteShortUrl', 'resetDeleteShortUrl', 'shortUrlDeleted' ] + )); + + bottle.serviceFactory('EditTagsModal', EditTagsModal, 'TagsSelector'); + bottle.decorator('EditTagsModal', connect( + [ 'shortUrlTags' ], + [ 'editShortUrlTags', 'resetShortUrlsTags', 'shortUrlTagsEdited' ] + )); + + // Actions + bottle.serviceFactory('editShortUrlTags', editShortUrlTags, 'buildShlinkApiClient'); + bottle.serviceFactory('resetShortUrlsTags', () => resetShortUrlsTags); + bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited); + + bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient'); + bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams); + + bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient'); + bottle.serviceFactory('resetCreateShortUrl', () => resetCreateShortUrl); + + bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient'); + bottle.serviceFactory('resetDeleteShortUrl', () => resetDeleteShortUrl); + bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted); +}; + +export default provideServices; diff --git a/src/utils/ColorGenerator.js b/src/utils/ColorGenerator.js index 673eba3c..8f276835 100644 --- a/src/utils/ColorGenerator.js +++ b/src/utils/ColorGenerator.js @@ -1,6 +1,5 @@ import { range } from 'ramda'; import PropTypes from 'prop-types'; -import storage from './Storage'; const HEX_COLOR_LENGTH = 6; const { floor, random } = Math; @@ -13,7 +12,7 @@ const buildRandomColor = () => }`; const normalizeKey = (key) => key.toLowerCase().trim(); -export class ColorGenerator { +export default class ColorGenerator { constructor(storage) { this.storage = storage; this.colors = this.storage.get('colors') || {}; @@ -45,7 +44,3 @@ export const colorGeneratorType = PropTypes.shape({ getColorForKey: PropTypes.func, setColorForKey: PropTypes.func, }); - -const colorGenerator = new ColorGenerator(storage); - -export default colorGenerator; diff --git a/src/utils/Storage.js b/src/utils/Storage.js index 22a52632..35f9eb74 100644 --- a/src/utils/Storage.js +++ b/src/utils/Storage.js @@ -1,7 +1,7 @@ const PREFIX = 'shlink'; const buildPath = (path) => `${PREFIX}.${path}`; -export class Storage { +export default class Storage { constructor(localStorage) { this.localStorage = localStorage; } @@ -14,15 +14,3 @@ export class Storage { set = (key, value) => this.localStorage.setItem(buildPath(key), JSON.stringify(value)); } - -const browserStorage = global.localStorage || { - getItem() { - return ''; - }, - setItem() { - return ''; - }, -}; -const storage = new Storage(browserStorage); - -export default storage; diff --git a/test/servers/services/ServersExporter.test.js b/test/servers/services/ServersExporter.test.js index eea6e19a..173292e3 100644 --- a/test/servers/services/ServersExporter.test.js +++ b/test/servers/services/ServersExporter.test.js @@ -1,5 +1,5 @@ import sinon from 'sinon'; -import { ServersExporter } from '../../../src/servers/services/ServersExporter'; +import ServersExporter from '../../../src/servers/services/ServersExporter'; describe('ServersExporter', () => { const createLinkMock = () => ({ diff --git a/test/servers/services/ServersImporter.test.js b/test/servers/services/ServersImporter.test.js index af1b5e8c..ef79d790 100644 --- a/test/servers/services/ServersImporter.test.js +++ b/test/servers/services/ServersImporter.test.js @@ -1,5 +1,5 @@ import sinon from 'sinon'; -import { ServersImporter } from '../../../src/servers/services/ServersImporter'; +import ServersImporter from '../../../src/servers/services/ServersImporter'; describe('ServersImporter', () => { const servers = [{ name: 'foo' }, { name: 'bar' }]; diff --git a/test/servers/services/ServersService.test.js b/test/servers/services/ServersService.test.js index a8fb4281..b8c60d9e 100644 --- a/test/servers/services/ServersService.test.js +++ b/test/servers/services/ServersService.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; import { last } from 'ramda'; -import { ServersService } from '../../../src/servers/services/ServersService'; +import ServersService from '../../../src/servers/services/ServersService'; describe('ServersService', () => { const servers = { diff --git a/test/short-urls/reducers/shortUrlCreation.test.js b/test/short-urls/reducers/shortUrlCreation.test.js index 28aac024..371d257f 100644 --- a/test/short-urls/reducers/shortUrlCreation.test.js +++ b/test/short-urls/reducers/shortUrlCreation.test.js @@ -4,7 +4,7 @@ import reducer, { CREATE_SHORT_URL_ERROR, CREATE_SHORT_URL, RESET_CREATE_SHORT_URL, - _createShortUrl, + createShortUrl, resetCreateShortUrl, } from '../../../src/short-urls/reducers/shortUrlCreation'; @@ -62,7 +62,7 @@ describe('shortUrlCreationReducer', () => { const expectedDispatchCalls = 2; const result = 'foo'; const apiClientMock = createApiClientMock(Promise.resolve(result)); - const dispatchable = _createShortUrl(() => apiClientMock, {}); + const dispatchable = createShortUrl(() => apiClientMock)({}); await dispatchable(dispatch, getState); @@ -77,7 +77,7 @@ describe('shortUrlCreationReducer', () => { const expectedDispatchCalls = 2; const error = 'Error'; const apiClientMock = createApiClientMock(Promise.reject(error)); - const dispatchable = _createShortUrl(() => apiClientMock, {}); + const dispatchable = createShortUrl(() => apiClientMock)({}); try { await dispatchable(dispatch, getState); diff --git a/test/utils/ColorGenerator.test.js b/test/utils/ColorGenerator.test.js index e0a359c6..a70de95b 100644 --- a/test/utils/ColorGenerator.test.js +++ b/test/utils/ColorGenerator.test.js @@ -1,5 +1,5 @@ import * as sinon from 'sinon'; -import { ColorGenerator } from '../../src/utils/ColorGenerator'; +import ColorGenerator from '../../src/utils/ColorGenerator'; describe('ColorGenerator', () => { let colorGenerator; From eec79043cc2889ae7ffdbb034c11e8affa41bc55 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 20:19:22 +0100 Subject: [PATCH 16/18] Moved common and utils services to their own service providers --- src/common/services/provideServices.js | 32 +++++++++++++ src/container/index.js | 45 +++---------------- src/tags/helpers/Tag.js | 2 +- src/tags/helpers/TagBullet.js | 2 +- src/tags/reducers/tagsList.js | 2 +- src/utils/{ => services}/ColorGenerator.js | 0 .../services}/ShlinkApiClient.js | 0 .../services}/ShlinkApiClientBuilder.js | 0 src/utils/{ => services}/Storage.js | 0 src/utils/services/provideServices.js | 15 +++++++ .../{ => services}/ColorGenerator.test.js | 2 +- .../services}/ShlinkApiClient.test.js | 2 +- .../services/ShlinkApiClientBuilder.test.js | 26 +++++++++++ 13 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 src/common/services/provideServices.js rename src/utils/{ => services}/ColorGenerator.js (100%) rename src/{api => utils/services}/ShlinkApiClient.js (100%) rename src/{api => utils/services}/ShlinkApiClientBuilder.js (100%) rename src/utils/{ => services}/Storage.js (100%) create mode 100644 src/utils/services/provideServices.js rename test/utils/{ => services}/ColorGenerator.test.js (95%) rename test/{api => utils/services}/ShlinkApiClient.test.js (98%) create mode 100644 test/utils/services/ShlinkApiClientBuilder.test.js diff --git a/src/common/services/provideServices.js b/src/common/services/provideServices.js new file mode 100644 index 00000000..584ddcf9 --- /dev/null +++ b/src/common/services/provideServices.js @@ -0,0 +1,32 @@ +import ScrollToTop from '../ScrollToTop'; +import MainHeader from '../MainHeader'; +import Home from '../Home'; +import MenuLayout from '../MenuLayout'; +import AsideMenu from '../AsideMenu'; + +const provideServices = (bottle, connect, withRouter) => { + bottle.constant('ScrollToTop', ScrollToTop); + bottle.decorator('ScrollToTop', withRouter); + + bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); + bottle.decorator('MainHeader', withRouter); + + bottle.serviceFactory('Home', () => Home); + bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ])); + + bottle.serviceFactory( + 'MenuLayout', + MenuLayout, + 'TagsList', + 'ShortUrls', + 'AsideMenu', + 'CreateShortUrl', + 'ShortUrlVisits' + ); + bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); + bottle.decorator('MenuLayout', withRouter); + + bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); +}; + +export default provideServices; diff --git a/src/container/index.js b/src/container/index.js index 79a65834..ffe5de5d 100644 --- a/src/container/index.js +++ b/src/container/index.js @@ -2,20 +2,13 @@ import Bottle from 'bottlejs'; import { withRouter } from 'react-router-dom'; import { connect as reduxConnect } from 'react-redux'; import { pick } from 'ramda'; -import axios from 'axios'; import App from '../App'; -import ScrollToTop from '../common/ScrollToTop'; -import MainHeader from '../common/MainHeader'; -import Home from '../common/Home'; -import MenuLayout from '../common/MenuLayout'; -import AsideMenu from '../common/AsideMenu'; -import ColorGenerator from '../utils/ColorGenerator'; -import Storage from '../utils/Storage'; -import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; +import provideCommonServices from '../common/services/provideServices'; import provideShortUrlsServices from '../short-urls/services/provideServices'; import provideServersServices from '../servers/services/provideServices'; import provideVisitsServices from '../visits/services/provideServices'; import provideTagsServices from '../tags/services/provideServices'; +import provideUtilsServices from '../utils/services/provideServices'; const bottle = new Bottle(); const { container } = bottle; @@ -29,44 +22,16 @@ const mapActionService = (map, actionName) => ({ const connect = (propsFromState, actionServiceNames) => reduxConnect( propsFromState ? pick(propsFromState) : null, - Array.isArray(actionServiceNames) ? actionServiceNames.reduce(mapActionService, {}) : actionServiceNames + actionServiceNames.reduce(mapActionService, {}) ); -bottle.constant('ScrollToTop', ScrollToTop); -bottle.decorator('ScrollToTop', withRouter); - bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); -bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); -bottle.decorator('MainHeader', withRouter); - -bottle.serviceFactory('Home', () => Home); -bottle.decorator('Home', connect([ 'servers' ], [ 'resetSelectedServer' ])); - -bottle.serviceFactory( - 'MenuLayout', - MenuLayout, - 'TagsList', - 'ShortUrls', - 'AsideMenu', - 'CreateShortUrl', - 'ShortUrlVisits' -); -bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ])); -bottle.decorator('MenuLayout', withRouter); - -bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton'); - -bottle.constant('localStorage', global.localStorage); -bottle.service('Storage', Storage, 'localStorage'); -bottle.service('ColorGenerator', ColorGenerator, 'Storage'); - -bottle.constant('axios', axios); -bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); - +provideCommonServices(bottle, connect, withRouter); provideShortUrlsServices(bottle, connect); provideServersServices(bottle, connect, withRouter); provideTagsServices(bottle, connect); provideVisitsServices(bottle, connect); +provideUtilsServices(bottle); export default container; diff --git a/src/tags/helpers/Tag.js b/src/tags/helpers/Tag.js index 2356e818..29515af5 100644 --- a/src/tags/helpers/Tag.js +++ b/src/tags/helpers/Tag.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import './Tag.scss'; -import { colorGeneratorType } from '../../utils/ColorGenerator'; +import { colorGeneratorType } from '../../utils/services/ColorGenerator'; const propTypes = { text: PropTypes.string, diff --git a/src/tags/helpers/TagBullet.js b/src/tags/helpers/TagBullet.js index 6afcd79d..896eaf8f 100644 --- a/src/tags/helpers/TagBullet.js +++ b/src/tags/helpers/TagBullet.js @@ -1,6 +1,6 @@ import React from 'react'; import * as PropTypes from 'prop-types'; -import { colorGeneratorType } from '../../utils/ColorGenerator'; +import { colorGeneratorType } from '../../utils/services/ColorGenerator'; import './TagBullet.scss'; const propTypes = { diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index 94338266..9b4fe65e 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,5 +1,5 @@ import { isEmpty, reject } from 'ramda'; -import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../api/ShlinkApiClientBuilder'; +import { buildShlinkApiClientWithAxios as buildShlinkApiClient } from '../../utils/services/ShlinkApiClientBuilder'; import { TAG_DELETED } from './tagDelete'; import { TAG_EDITED } from './tagEdit'; diff --git a/src/utils/ColorGenerator.js b/src/utils/services/ColorGenerator.js similarity index 100% rename from src/utils/ColorGenerator.js rename to src/utils/services/ColorGenerator.js diff --git a/src/api/ShlinkApiClient.js b/src/utils/services/ShlinkApiClient.js similarity index 100% rename from src/api/ShlinkApiClient.js rename to src/utils/services/ShlinkApiClient.js diff --git a/src/api/ShlinkApiClientBuilder.js b/src/utils/services/ShlinkApiClientBuilder.js similarity index 100% rename from src/api/ShlinkApiClientBuilder.js rename to src/utils/services/ShlinkApiClientBuilder.js diff --git a/src/utils/Storage.js b/src/utils/services/Storage.js similarity index 100% rename from src/utils/Storage.js rename to src/utils/services/Storage.js diff --git a/src/utils/services/provideServices.js b/src/utils/services/provideServices.js new file mode 100644 index 00000000..fddb74bd --- /dev/null +++ b/src/utils/services/provideServices.js @@ -0,0 +1,15 @@ +import axios from 'axios'; +import Storage from './Storage'; +import ColorGenerator from './ColorGenerator'; +import buildShlinkApiClient from './ShlinkApiClientBuilder'; + +const provideServices = (bottle) => { + bottle.constant('localStorage', global.localStorage); + bottle.service('Storage', Storage, 'localStorage'); + bottle.service('ColorGenerator', ColorGenerator, 'Storage'); + + bottle.constant('axios', axios); + bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); +}; + +export default provideServices; diff --git a/test/utils/ColorGenerator.test.js b/test/utils/services/ColorGenerator.test.js similarity index 95% rename from test/utils/ColorGenerator.test.js rename to test/utils/services/ColorGenerator.test.js index a70de95b..937c3b2a 100644 --- a/test/utils/ColorGenerator.test.js +++ b/test/utils/services/ColorGenerator.test.js @@ -1,5 +1,5 @@ import * as sinon from 'sinon'; -import ColorGenerator from '../../src/utils/ColorGenerator'; +import ColorGenerator from '../../../src/utils/services/ColorGenerator'; describe('ColorGenerator', () => { let colorGenerator; diff --git a/test/api/ShlinkApiClient.test.js b/test/utils/services/ShlinkApiClient.test.js similarity index 98% rename from test/api/ShlinkApiClient.test.js rename to test/utils/services/ShlinkApiClient.test.js index 02935f1d..f92b00f7 100644 --- a/test/api/ShlinkApiClient.test.js +++ b/test/utils/services/ShlinkApiClient.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; import { head, last } from 'ramda'; -import ShlinkApiClient from '../../src/api/ShlinkApiClient'; +import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient'; describe('ShlinkApiClient', () => { const createAxiosMock = (extraData) => () => diff --git a/test/utils/services/ShlinkApiClientBuilder.test.js b/test/utils/services/ShlinkApiClientBuilder.test.js new file mode 100644 index 00000000..9316087f --- /dev/null +++ b/test/utils/services/ShlinkApiClientBuilder.test.js @@ -0,0 +1,26 @@ +import buildShlinkApiClient from '../../../src/utils/services/ShlinkApiClientBuilder'; + +describe('ShlinkApiClientBuilder', () => { + const builder = buildShlinkApiClient({}); + + it('creates new instances when provided params are different', () => { + const firstApiClient = builder({ url: 'foo', apiKey: 'bar' }); + const secondApiClient = builder({ url: 'bar', apiKey: 'bar' }); + const thirdApiClient = builder({ url: 'bar', apiKey: 'foo' }); + + expect(firstApiClient).not.toBe(secondApiClient); + expect(firstApiClient).not.toBe(thirdApiClient); + expect(secondApiClient).not.toBe(thirdApiClient); + }); + + it('returns existing instances when provided params are the same', () => { + const params = { url: 'foo', apiKey: 'bar' }; + const firstApiClient = builder(params); + const secondApiClient = builder(params); + const thirdApiClient = builder(params); + + expect(firstApiClient).toBe(secondApiClient); + expect(firstApiClient).toBe(thirdApiClient); + expect(secondApiClient).toBe(thirdApiClient); + }); +}); From cd11dd984817e59a6e19bb917235ced4c9749648 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 20:24:18 +0100 Subject: [PATCH 17/18] Updated tests config excluding config files form code coverage --- jest.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 55b1480b..a7bdef22 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,11 @@ module.exports = { coverageDirectory: '/coverage', collectCoverageFrom: [ - 'src/**/*.{js,jsx,mjs}', + 'src/**/*.js', '!src/registerServiceWorker.js', '!src/index.js', + '!src/**/provideServices.js', + '!src/container/*.js', ], setupFiles: [ '/config/polyfills.js', From 8bd3a15a1d2c5299cad07de7696b14ddefbc87ec Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Dec 2018 20:28:21 +0100 Subject: [PATCH 18/18] Updated changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4a0b8a..eb9826de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +#### Added + +* *Nothing* + +#### Changed + +* [#80](https://github.com/shlinkio/shlink-web-client/issues/80) Deeply refactored app to do true dependency injection with an IoC container. + +#### Deprecated + +* *Nothing* + +#### Removed + +* *Nothing* + +#### Fixed + +* *Nothing* + + ## 1.2.0 - 2018-11-01 #### Added