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 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', 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..b351422c 100644 --- a/src/App.js +++ b/src/App.js @@ -1,23 +1,19 @@ 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, MenuLayout, CreateServer) => () => ( +
+ -
- - - - - -
+
+ + + + +
- ); -} +
+); + +export default App; 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/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..075ae7be 100644 --- a/src/common/MainHeader.js +++ b/src/common/MainHeader.js @@ -2,15 +2,14 @@ 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'; -import ServersDropdown from '../servers/ServersDropdown'; -import './MainHeader.scss'; import shlinkLogo from './shlink-logo-white.png'; +import './MainHeader.scss'; -export class MainHeaderComponent extends React.Component { +const MainHeader = (ServersDropdown) => class MainHeader extends React.Component { static propTypes = { location: PropTypes.object, }; @@ -62,8 +61,6 @@ export class MainHeaderComponent extends React.Component { ); } -} - -const MainHeader = withRouter(MainHeaderComponent); +}; export default MainHeader; diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 60e33a31..70a8e164 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -1,113 +1,100 @@ 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'; -import TagsList from '../tags/TagsList'; import { serverType } from '../servers/prop-types'; -import AsideMenu from './AsideMenu'; +import './MenuLayout.scss'; -export class MenuLayoutComponent 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 })} - > - - - - - - -
-
-
-
- ); - } -} - -const MenuLayout = compose( - connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }), - withRouter -)(MenuLayoutComponent); - export default MenuLayout; 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/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 new file mode 100644 index 00000000..ffe5de5d --- /dev/null +++ b/src/container/index.js @@ -0,0 +1,37 @@ +import Bottle from 'bottlejs'; +import { withRouter } from 'react-router-dom'; +import { connect as reduxConnect } from 'react-redux'; +import { pick } from 'ramda'; +import App from '../App'; +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; + +const mapActionService = (map, actionName) => ({ + ...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 connect = (propsFromState, actionServiceNames) => + reduxConnect( + propsFromState ? pick(propsFromState) : null, + actionServiceNames.reduce(mapActionService, {}) + ); + +bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer'); + +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/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..8f30306f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,23 +3,15 @@ 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 store from './container/store'; 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 } = container; render( diff --git a/src/servers/CreateServer.js b/src/servers/CreateServer.js index 3875ba66..fbea083e 100644 --- a/src/servers/CreateServer.js +++ b/src/servers/CreateServer.js @@ -1,17 +1,13 @@ -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 { +const CreateServer = (ImportServersBtn) => class CreateServer extends React.Component { static propTypes = { createServer: PropTypes.func, history: PropTypes.shape({ @@ -91,11 +87,6 @@ export class CreateServerComponent extends React.Component {
); } -} - -const CreateServer = connect( - pick([ 'selectedServer' ]), - { createServer, resetSelectedServer } -)(CreateServerComponent); +}; 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/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 891f5ce2..51b53e86 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -1,6 +1,3 @@ -import { curry } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; -import serversService from '../../servers/services/ServersService'; import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -23,17 +20,13 @@ 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 = curry(_selectServer)(shlinkApiClient, 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/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 new file mode 100644 index 00000000..a2464749 --- /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/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/SearchBar.js b/src/short-urls/SearchBar.js index 6cd5cf3d..6e343c70 100644 --- a/src/short-urls/SearchBar.js +++ b/src/short-urls/SearchBar.js @@ -1,55 +1,56 @@ 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 Tag from '../tags/helpers/Tag'; 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 = (colorGenerator) => { + 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/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/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js index f9d64420..9d9e874d 100644 --- a/src/short-urls/helpers/ShortUrlsRow.js +++ b/src/short-urls/helpers/ShortUrlsRow.js @@ -2,16 +2,15 @@ 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 Tag from '../../tags/helpers/Tag'; import './ShortUrlsRow.scss'; -export class ShortUrlsRow extends React.Component { +const ShortUrlsRow = (ShortUrlsRowMenu, colorGenerator) => class ShortUrlsRow extends React.Component { static propTypes = { refreshList: PropTypes.func, shortUrlsListParams: shortUrlsListParamsType, @@ -31,6 +30,7 @@ export class ShortUrlsRow extends React.Component { return tags.map((tag) => ( refreshList({ tags: [ ...selectedTags, tag ] })} @@ -72,4 +72,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..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 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 class ShortUrlsRowMenu extends React.Component { ); } -} +}; + +export default ShortUrlsRowMenu; diff --git a/src/short-urls/reducers/shortUrlCreation.js b/src/short-urls/reducers/shortUrlCreation.js index 9f01a8ee..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 shlinkApiClient from '../../api/ShlinkApiClient'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START'; @@ -50,9 +48,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 +63,4 @@ export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => { } }; -export const createShortUrl = curry(_createShortUrl)(shlinkApiClient); - 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..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 shlinkApiClient from '../../api/ShlinkApiClient'; /* eslint-disable padding-line-between-statements, newline-after-var */ const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START'; @@ -56,9 +54,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,8 +70,6 @@ export const _deleteShortUrl = (shlinkApiClient, shortCode) => async (dispatch) } }; -export const deleteShortUrl = curry(_deleteShortUrl)(shlinkApiClient); - export const resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL }); export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode }); diff --git a/src/short-urls/reducers/shortUrlTags.js b/src/short-urls/reducers/shortUrlTags.js index 597c5a89..a0390a60 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,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); @@ -66,8 +66,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) => ({ diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 95e4a7fc..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 shlinkApiClient from '../../api/ShlinkApiClient'; import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; @@ -55,9 +54,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); @@ -66,5 +68,3 @@ export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) dispatch({ type: LIST_SHORT_URLS_ERROR, params }); } }; - -export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params); 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/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 1971817e..1982f6fc 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -1,16 +1,13 @@ 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 { +const TagsList = (TagCard) => class TagsList extends React.Component { static propTypes = { filterTags: PropTypes.func, forceListTags: PropTypes.func, @@ -82,8 +79,6 @@ export class TagsListComponent extends React.Component { ); } -} - -const TagsList = connect(pick([ 'tagsList' ]), { forceListTags, filterTags })(TagsListComponent); +}; 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 a08085a9..29515af5 100644 --- a/src/tags/helpers/Tag.js +++ b/src/tags/helpers/Tag.js @@ -1,41 +1,35 @@ import React from 'react'; import PropTypes from 'prop-types'; -import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; import './Tag.scss'; +import { colorGeneratorType } from '../../utils/services/ColorGenerator'; const propTypes = { - colorGenerator: colorGeneratorType, text: PropTypes.string, children: PropTypes.node, clearable: PropTypes.bool, + colorGenerator: colorGeneratorType, onClick: PropTypes.func, onClose: PropTypes.func, }; -const defaultProps = { + +const Tag = ({ + text, + children, + clearable, colorGenerator, -}; + onClick = () => {}, + onClose = () => {}, +}) => ( + + {children || text} + {clearable && ×} + +); -export default function Tag( - { - colorGenerator, - text, - children, - clearable, - onClick = () => ({}), - onClose = () => ({}), - } -) { - return ( - - {children || text} - {clearable && ×} - - ); -} - -Tag.defaultProps = defaultProps; Tag.propTypes = propTypes; + +export default Tag; diff --git a/src/tags/helpers/TagBullet.js b/src/tags/helpers/TagBullet.js index 1427613a..896eaf8f 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/services/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 4a218448..161a9237 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)} @@ -59,7 +54,7 @@ export class TagsSelectorComponent extends React.Component { getSuggestionValue={(suggestion) => suggestion} renderSuggestion={(suggestion) => ( - + {suggestion} )} @@ -86,8 +81,6 @@ export class TagsSelectorComponent extends React.Component { /> ); } -} - -const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent); +}; export default TagsSelector; diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.js index dfd2bf4a..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 shlinkApiClient from '../../api/ShlinkApiClient'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; @@ -41,9 +39,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 +55,4 @@ export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => { } }; -export const deleteTag = curry(_deleteTag)(shlinkApiClient); - export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js index 650f0ba1..950e95db 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.js @@ -1,6 +1,4 @@ -import { curry, pick } from 'ramda'; -import shlinkApiClient from '../../api/ShlinkApiClient'; -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,9 +40,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,8 +60,6 @@ export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, colo } }; -export const editTag = curry(_editTag)(shlinkApiClient, colorGenerator); - export const tagEdited = (oldName, newName, color) => ({ type: TAG_EDITED, oldName, diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index cb415902..9b4fe65e 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 '../../utils/services/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/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/utils/ColorGenerator.js b/src/utils/services/ColorGenerator.js similarity index 88% rename from src/utils/ColorGenerator.js rename to src/utils/services/ColorGenerator.js index 673eba3c..8f276835 100644 --- a/src/utils/ColorGenerator.js +++ b/src/utils/services/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/api/ShlinkApiClient.js b/src/utils/services/ShlinkApiClient.js similarity index 89% rename from src/api/ShlinkApiClient.js rename to src/utils/services/ShlinkApiClient.js index 6c35b84a..f2439bba 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/utils/services/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/utils/services/ShlinkApiClientBuilder.js b/src/utils/services/ShlinkApiClientBuilder.js new file mode 100644 index 00000000..23b050c8 --- /dev/null +++ b/src/utils/services/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/utils/Storage.js b/src/utils/services/Storage.js similarity index 63% rename from src/utils/Storage.js rename to src/utils/services/Storage.js index 22a52632..35f9eb74 100644 --- a/src/utils/Storage.js +++ b/src/utils/services/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/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/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/reducers/shortUrlDetail.js b/src/visits/reducers/shortUrlDetail.js index cb86e040..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 shlinkApiClient from '../../api/ShlinkApiClient'; import { shortUrlType } from '../../short-urls/reducers/shortUrlsList'; /* eslint-disable padding-line-between-statements, newline-after-var */ @@ -45,9 +43,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); @@ -56,5 +57,3 @@ export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatc dispatch({ type: GET_SHORT_URL_DETAIL_ERROR }); } }; - -export const getShortUrlDetail = curry(_getShortUrlDetail)(shlinkApiClient); diff --git a/src/visits/reducers/shortUrlVisits.js b/src/visits/reducers/shortUrlVisits.js index 1bb724ab..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 shlinkApiClient from '../../api/ShlinkApiClient'; /* 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 +42,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); @@ -55,5 +56,3 @@ export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => async ( dispatch({ type: GET_SHORT_URL_VISITS_ERROR }); } }; - -export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient); diff --git a/src/visits/services/provideServices.js b/src/visits/services/provideServices.js new file mode 100644 index 00000000..0b754868 --- /dev/null +++ b/src/visits/services/provideServices.js @@ -0,0 +1,22 @@ +import ShortUrlVisits from '../ShortUrlVisits'; +import { getShortUrlVisits } from '../reducers/shortUrlVisits'; +import { getShortUrlDetail } from '../reducers/shortUrlDetail'; +import * as visitsParser from './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/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/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/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..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 { CreateServerComponent } 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,12 +16,10 @@ describe('', () => { createServerMock.resetHistory(); historyMock.push.resetHistory(); + const CreateServer = createServerConstruct(ImportServersBtn); + wrapper = shallow( - + ); }); afterEach(() => wrapper.unmount()); 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,7 +14,8 @@ describe('', () => { }; beforeEach(() => { - wrapped = shallow(); + ServersDropdown = serversDropdownCreator({}); + wrapped = shallow(); }); afterEach(() => wrapped.unmount()); @@ -31,7 +33,7 @@ describe('', () => { }); it('contains a message when no servers exist yet', () => { - wrapped = shallow(); + wrapped = shallow(); const item = wrapped.find(DropdownItem); expect(item).toHaveLength(1); 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/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index e8cf174f..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'; @@ -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/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); 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/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/SearchBar.test.js b/test/short-urls/SearchBar.test.js index 75bc122e..d3a3869e 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 SearchBar = searchBarCreator({}); 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/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; diff --git a/test/short-urls/reducers/shortUrlCreation.test.js b/test/short-urls/reducers/shortUrlCreation.test.js index ebf1dc04..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'; @@ -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/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 bf37bd4f..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 { TagsListComponent as 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( diff --git a/test/tags/reducers/tagDelete.test.js b/test/tags/reducers/tagDelete.test.js index ece3c92d..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', () => { @@ -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..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', () => { @@ -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/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 e0a359c6..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 68894fb0..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); + }); +}); 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( - { 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..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, @@ -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; 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"