mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Merge pull request #81 from acelaya/feature/dependency-injection
Feature/dependency injection
This commit is contained in:
commit
a2b08277dc
86 changed files with 833 additions and 717 deletions
23
CHANGELOG.md
23
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
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
module.exports = {
|
||||
coverageDirectory: '<rootDir>/coverage',
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx,mjs}',
|
||||
'src/**/*.js',
|
||||
'!src/registerServiceWorker.js',
|
||||
'!src/index.js',
|
||||
'!src/**/provideServices.js',
|
||||
'!src/container/*.js',
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/config/polyfills.js',
|
||||
|
|
|
@ -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",
|
||||
|
|
30
src/App.js
30
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 (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
|
||||
<div className="container-fluid app-container">
|
||||
<MainHeader />
|
||||
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="app">
|
||||
<Switch>
|
||||
<Route exact path="/server/create" component={CreateServer} />
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route path="/server/:serverId" component={MenuLayout} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -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 (
|
||||
<aside className={asideClass}>
|
||||
<nav className="nav flex-column aside-menu__nav">
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/list-short-urls/1`}
|
||||
isActive={shortUrlsIsActive}
|
||||
>
|
||||
<FontAwesomeIcon icon={listIcon} />
|
||||
<span className="aside-menu__item-text">List short URLs</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/create-short-url`}
|
||||
>
|
||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||
<span className="aside-menu__item-text">Create short URL</span>
|
||||
</NavLink>
|
||||
return (
|
||||
<aside className={asideClass}>
|
||||
<nav className="nav flex-column aside-menu__nav">
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/list-short-urls/1`}
|
||||
isActive={shortUrlsIsActive}
|
||||
>
|
||||
<FontAwesomeIcon icon={listIcon} />
|
||||
<span className="aside-menu__item-text">List short URLs</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/create-short-url`}
|
||||
>
|
||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||
<span className="aside-menu__item-text">Create short URL</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/manage-tags`}
|
||||
>
|
||||
<FontAwesomeIcon icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="aside-menu__item"
|
||||
activeClassName="aside-menu__item--selected"
|
||||
to={`/server/${serverId}/manage-tags`}
|
||||
>
|
||||
<FontAwesomeIcon icon={tagsIcon} />
|
||||
<span className="aside-menu__item-text">Manage tags</span>
|
||||
</NavLink>
|
||||
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
server={selectedServer}
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
<DeleteServerButton
|
||||
className="aside-menu__item aside-menu__item--danger"
|
||||
server={selectedServer}
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
AsideMenu.defaultProps = defaultProps;
|
||||
AsideMenu.propTypes = propTypes;
|
||||
AsideMenu.defaultProps = defaultProps;
|
||||
AsideMenu.propTypes = propTypes;
|
||||
|
||||
return AsideMenu;
|
||||
};
|
||||
|
||||
export default AsideMenu;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
|||
</Navbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MainHeader = withRouter(MainHeaderComponent);
|
||||
};
|
||||
|
||||
export default MainHeader;
|
||||
|
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon
|
||||
icon={burgerIcon}
|
||||
className={burgerClasses}
|
||||
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
|
||||
/>
|
||||
|
||||
<Swipeable
|
||||
delta={40}
|
||||
className="menu-layout__swipeable"
|
||||
onSwipedLeft={() => this.setState({ showSideBar: false })}
|
||||
onSwipedRight={() => this.setState({ showSideBar: true })}
|
||||
>
|
||||
<div className="row menu-layout__swipeable-inner">
|
||||
<AsideMenu
|
||||
className="col-lg-2 col-md-3"
|
||||
selectedServer={selectedServer}
|
||||
showOnMobile={this.state.showSideBar}
|
||||
/>
|
||||
<div
|
||||
className="col-lg-10 offset-lg-2 col-md-9 offset-md-3"
|
||||
onClick={() => this.setState({ showSideBar: false })}
|
||||
>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/list-short-urls/:page"
|
||||
component={ShortUrls}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/create-short-url"
|
||||
component={CreateShortUrl}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/short-code/:shortCode/visits"
|
||||
component={ShortUrlVisits}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/manage-tags"
|
||||
component={TagsList}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<FontAwesomeIcon
|
||||
icon={burgerIcon}
|
||||
className={burgerClasses}
|
||||
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
|
||||
/>
|
||||
|
||||
<Swipeable
|
||||
delta={40}
|
||||
className="menu-layout__swipeable"
|
||||
onSwipedLeft={() => this.setState({ showSideBar: false })}
|
||||
onSwipedRight={() => this.setState({ showSideBar: true })}
|
||||
>
|
||||
<div className="row menu-layout__swipeable-inner">
|
||||
<AsideMenu
|
||||
className="col-lg-2 col-md-3"
|
||||
selectedServer={selectedServer}
|
||||
showOnMobile={this.state.showSideBar}
|
||||
/>
|
||||
<div
|
||||
className="col-lg-10 offset-lg-2 col-md-9 offset-md-3"
|
||||
onClick={() => this.setState({ showSideBar: false })}
|
||||
>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/list-short-urls/:page"
|
||||
component={ShortUrls}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/create-short-url"
|
||||
component={CreateShortUrl}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/short-code/:shortCode/visits"
|
||||
component={ShortUrlsVisits}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/server/:serverId/manage-tags"
|
||||
component={TagsList}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MenuLayout = compose(
|
||||
connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }),
|
||||
withRouter
|
||||
)(MenuLayoutComponent);
|
||||
|
||||
export default MenuLayout;
|
||||
|
|
|
@ -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;
|
||||
|
|
32
src/common/services/provideServices.js
Normal file
32
src/common/services/provideServices.js
Normal file
|
@ -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;
|
37
src/container/index.js
Normal file
37
src/container/index.js
Normal file
|
@ -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;
|
13
src/container/store.js
Normal file
13
src/container/store.js
Normal file
|
@ -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;
|
16
src/index.js
16
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(
|
||||
<Provider store={store}>
|
||||
|
|
|
@ -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 {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CreateServer = connect(
|
||||
pick([ 'selectedServer' ]),
|
||||
{ createServer, resetSelectedServer }
|
||||
)(CreateServerComponent);
|
||||
};
|
||||
|
||||
export default CreateServer;
|
||||
|
|
|
@ -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 {
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default DeleteServerButton;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
|
||||
|
@ -68,11 +58,6 @@ export class ServersDropdownComponent extends React.Component {
|
|||
</UncontrolledDropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ServersDropdown = connect(
|
||||
pick([ 'servers', 'selectedServer' ]),
|
||||
{ listServers, selectServer }
|
||||
)(ServersDropdownComponent);
|
||||
};
|
||||
|
||||
export default ServersDropdown;
|
||||
|
|
|
@ -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 {
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent);
|
||||
};
|
||||
|
||||
export default ImportServersBtn;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
47
src/servers/services/provideServices.js
Normal file
47
src/servers/services/provideServices.js
Normal file
|
@ -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;
|
|
@ -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 {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CreateShortUrl = connect(pick([ 'shortUrlCreationResult' ]), {
|
||||
createShortUrl,
|
||||
resetCreateShortUrl,
|
||||
})(CreateShortUrlComponent);
|
||||
};
|
||||
|
||||
export default CreateShortUrl;
|
||||
|
|
|
@ -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 (
|
||||
<div className="serach-bar-container">
|
||||
<SearchField onChange={
|
||||
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
|
||||
}
|
||||
/>
|
||||
return (
|
||||
<div className="serach-bar-container">
|
||||
<SearchField onChange={
|
||||
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
|
||||
}
|
||||
/>
|
||||
|
||||
{!isEmpty(selectedTags) && (
|
||||
<h4 className="search-bar__selected-tag mt-2">
|
||||
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
||||
|
||||
{selectedTags.map((tag) => (
|
||||
<Tag
|
||||
key={tag}
|
||||
text={tag}
|
||||
clearable
|
||||
onClose={() => listShortUrls(
|
||||
{
|
||||
...shortUrlsListParams,
|
||||
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||
}
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{!isEmpty(selectedTags) && (
|
||||
<h4 className="search-bar__selected-tag mt-2">
|
||||
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
||||
|
||||
{selectedTags.map((tag) => (
|
||||
<Tag
|
||||
colorGenerator={colorGenerator}
|
||||
key={tag}
|
||||
text={tag}
|
||||
clearable
|
||||
onClose={() => listShortUrls(
|
||||
{
|
||||
...shortUrlsListParams,
|
||||
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||
}
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SearchBarComponent.propTypes = propTypes;
|
||||
SearchBar.propTypes = propTypes;
|
||||
|
||||
const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent);
|
||||
return SearchBar;
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
|
|
|
@ -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 (
|
||||
<div className="shlink-container">
|
||||
<div className="form-group"><SearchBar /></div>
|
||||
<ShortUrlsList {...props} shortUrlsList={props.shortUrlsList.data || []} key={urlsListKey} />
|
||||
<Paginator paginator={props.shortUrlsList.pagination} serverId={props.match.params.serverId} />
|
||||
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
|
||||
<Paginator paginator={pagination} serverId={serverId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ShortUrls = connect(
|
||||
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
|
||||
)(ShortUrlsComponent);
|
||||
};
|
||||
|
||||
export default ShortUrls;
|
||||
|
|
|
@ -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 {
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ShortUrlsList = connect(
|
||||
pick([ 'selectedServer', 'shortUrlsListParams' ]),
|
||||
{ listShortUrls, resetShortUrlParams }
|
||||
)(ShortUrlsListComponent);
|
||||
};
|
||||
|
||||
export default ShortUrlsList;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
|||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EditTagsModal = connect(
|
||||
pick([ 'shortUrlTags' ]),
|
||||
{ editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited }
|
||||
)(EditTagsModalComponent);
|
||||
};
|
||||
|
||||
export default EditTagsModal;
|
||||
|
|
|
@ -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) => (
|
||||
<Tag
|
||||
colorGenerator={colorGenerator}
|
||||
key={tag}
|
||||
text={tag}
|
||||
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
|
||||
|
@ -72,4 +72,6 @@ export class ShortUrlsRow extends React.Component {
|
|||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ShortUrlsRow;
|
||||
|
|
|
@ -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 {
|
|||
</ButtonDropdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ShortUrlsRowMenu;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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);
|
||||
|
|
71
src/short-urls/services/provideServices.js
Normal file
71
src/short-urls/services/provideServices.js
Normal file
|
@ -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;
|
|
@ -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 {
|
|||
<FontAwesomeIcon icon={editIcon} />
|
||||
</button>
|
||||
<h5 className="tag-card__tag-title">
|
||||
<TagBullet tag={tag} />
|
||||
<TagBullet tag={tag} colorGenerator={colorGenerator} />
|
||||
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>{tag}</Link>
|
||||
</h5>
|
||||
</CardBody>
|
||||
|
@ -45,4 +43,6 @@ export default class TagCard extends React.Component {
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default TagCard;
|
||||
|
|
|
@ -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 {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TagsList = connect(pick([ 'tagsList' ]), { forceListTags, filterTags })(TagsListComponent);
|
||||
};
|
||||
|
||||
export default TagsList;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
|||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EditTagModal = connect(pick([ 'tagEdit' ]), { editTag, tagEdited })(EditTagModalComponent);
|
||||
};
|
||||
|
||||
export default EditTagModal;
|
||||
|
|
|
@ -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 = () => {},
|
||||
}) => (
|
||||
<span
|
||||
className="badge tag"
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children || text}
|
||||
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>×</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
export default function Tag(
|
||||
{
|
||||
colorGenerator,
|
||||
text,
|
||||
children,
|
||||
clearable,
|
||||
onClick = () => ({}),
|
||||
onClose = () => ({}),
|
||||
}
|
||||
) {
|
||||
return (
|
||||
<span
|
||||
className="badge tag"
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children || text}
|
||||
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>×</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Tag.defaultProps = defaultProps;
|
||||
Tag.propTypes = propTypes;
|
||||
|
||||
export default Tag;
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||
className="tag-bullet"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const TagBullet = ({ tag, colorGenerator }) => (
|
||||
<div
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||
className="tag-bullet"
|
||||
/>
|
||||
);
|
||||
|
||||
TagBullet.propTypes = propTypes;
|
||||
TagBullet.defaultProps = defaultProps;
|
||||
|
||||
export default TagBullet;
|
||||
|
|
|
@ -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 }) => (
|
||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||
{getTagDisplayValue(tag)}
|
||||
|
@ -59,7 +54,7 @@ export class TagsSelectorComponent extends React.Component {
|
|||
getSuggestionValue={(suggestion) => suggestion}
|
||||
renderSuggestion={(suggestion) => (
|
||||
<React.Fragment>
|
||||
<TagBullet tag={suggestion} />
|
||||
<TagBullet tag={suggestion} colorGenerator={colorGenerator} />
|
||||
{suggestion}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
@ -86,8 +81,6 @@ export class TagsSelectorComponent extends React.Component {
|
|||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent);
|
||||
};
|
||||
|
||||
export default TagsSelector;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
37
src/tags/services/provideServices.js
Normal file
37
src/tags/services/provideServices.js
Normal file
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
18
src/utils/services/ShlinkApiClientBuilder.js
Normal file
18
src/utils/services/ShlinkApiClientBuilder.js
Normal file
|
@ -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);
|
|
@ -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;
|
15
src/utils/services/provideServices.js
Normal file
15
src/utils/services/provideServices.js
Normal file
|
@ -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;
|
|
@ -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 {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ShortUrlsVisits = connect(
|
||||
pick([ 'shortUrlVisits', 'shortUrlDetail' ]),
|
||||
{ getShortUrlVisits, getShortUrlDetail }
|
||||
)(ShortUrlsVisitsComponent);
|
||||
|
||||
export default ShortUrlsVisits;
|
||||
export default ShortUrlVisits;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
22
src/visits/services/provideServices.js
Normal file
22
src/visits/services/provideServices.js
Normal file
|
@ -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;
|
|
@ -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('<App />', () => {
|
||||
let wrapper;
|
||||
const MainHeader = () => '';
|
||||
|
||||
beforeEach(() => {
|
||||
const App = appFactory(MainHeader, identity, identity, identity);
|
||||
|
||||
wrapper = shallow(<App />);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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('<AsideMenu />', () => {
|
||||
let wrapped;
|
||||
const DeleteServerButton = () => '';
|
||||
|
||||
beforeEach(() => {
|
||||
const AsideMenu = asideMenuCreator(DeleteServerButton);
|
||||
|
||||
wrapped = shallow(<AsideMenu selectedServer={{ id: 'abc123' }} />);
|
||||
});
|
||||
afterEach(() => wrapped.unmount());
|
||||
|
@ -20,6 +23,6 @@ describe('<AsideMenu />', () => {
|
|||
});
|
||||
|
||||
it('contains a button to delete server', () => {
|
||||
expect(wrapped.find('DeleteServerButton')).toHaveLength(1);
|
||||
expect(wrapped.find(DeleteServerButton)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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('<Home />', () => {
|
||||
let wrapped;
|
||||
|
@ -15,7 +15,7 @@ describe('<Home />', () => {
|
|||
const createComponent = (props) => {
|
||||
const actualProps = { ...defaultProps, ...props };
|
||||
|
||||
wrapped = shallow(<HomeComponent {...actualProps} />);
|
||||
wrapped = shallow(<Home {...actualProps} />);
|
||||
|
||||
return wrapped;
|
||||
};
|
||||
|
|
|
@ -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('<CreateServer />', () => {
|
||||
let wrapper;
|
||||
const ImportServersBtn = () => '';
|
||||
const createServerMock = sinon.fake();
|
||||
const historyMock = {
|
||||
push: sinon.fake(),
|
||||
|
@ -16,12 +16,10 @@ describe('<CreateServer />', () => {
|
|||
createServerMock.resetHistory();
|
||||
historyMock.push.resetHistory();
|
||||
|
||||
const CreateServer = createServerConstruct(ImportServersBtn);
|
||||
|
||||
wrapper = shallow(
|
||||
<CreateServerComponent
|
||||
createServer={createServerMock}
|
||||
resetSelectedServer={identity}
|
||||
history={historyMock}
|
||||
/>
|
||||
<CreateServer createServer={createServerMock} resetSelectedServer={identity} history={historyMock} />
|
||||
);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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('<DeleteServerButton />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const DeleteServerButton = deleteServerButtonConstruct(DeleteServerModal);
|
||||
|
||||
wrapper = shallow(<DeleteServerButton server={{}} className="button" />);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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('<DeleteServerModal />', () => {
|
||||
let wrapper;
|
||||
|
@ -17,7 +17,7 @@ describe('<DeleteServerModal />', () => {
|
|||
historyMock.push.resetHistory();
|
||||
|
||||
wrapper = shallow(
|
||||
<DeleteServerModalComponent
|
||||
<DeleteServerModal
|
||||
server={{ name: serverName }}
|
||||
toggle={toggleMock}
|
||||
isOpen={true}
|
||||
|
|
|
@ -2,10 +2,11 @@ import { identity, values } from 'ramda';
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
||||
import { ServersDropdownComponent } from '../../src/servers/ServersDropdown';
|
||||
import serversDropdownCreator from '../../src/servers/ServersDropdown';
|
||||
|
||||
describe('<ServersDropdown />', () => {
|
||||
let wrapped;
|
||||
let ServersDropdown;
|
||||
const servers = {
|
||||
'1a': { name: 'foo', id: 1 },
|
||||
'2b': { name: 'bar', id: 2 },
|
||||
|
@ -13,7 +14,8 @@ describe('<ServersDropdown />', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapped = shallow(<ServersDropdownComponent servers={servers} listServers={identity} />);
|
||||
ServersDropdown = serversDropdownCreator({});
|
||||
wrapped = shallow(<ServersDropdown servers={servers} listServers={identity} />);
|
||||
});
|
||||
afterEach(() => wrapped.unmount());
|
||||
|
||||
|
@ -31,7 +33,7 @@ describe('<ServersDropdown />', () => {
|
|||
});
|
||||
|
||||
it('contains a message when no servers exist yet', () => {
|
||||
wrapped = shallow(<ServersDropdownComponent servers={{}} listServers={identity} />);
|
||||
wrapped = shallow(<ServersDropdown servers={{}} listServers={identity} />);
|
||||
const item = wrapped.find(DropdownItem);
|
||||
|
||||
expect(item).toHaveLength(1);
|
||||
|
|
|
@ -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('<ImportServersBtn />', () => {
|
||||
let wrapper;
|
||||
|
@ -21,13 +21,10 @@ describe('<ImportServersBtn />', () => {
|
|||
serversImporterMock.importServersFromFile.resetHistory();
|
||||
fileRef.current.click.resetHistory();
|
||||
|
||||
const ImportServersBtn = importServersBtnConstruct(serversImporterMock);
|
||||
|
||||
wrapper = shallow(
|
||||
<ImportServersBtnComponent
|
||||
createServers={createServersMock}
|
||||
serversImporter={serversImporterMock}
|
||||
fileRef={fileRef}
|
||||
onImport={onImportMock}
|
||||
/>
|
||||
<ImportServersBtn createServers={createServersMock} fileRef={fileRef} onImport={onImportMock} />
|
||||
);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 = () => ({
|
||||
|
|
|
@ -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' }];
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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('<CreateShortUrl />', () => {
|
||||
let wrapper;
|
||||
const TagsSelector = () => '';
|
||||
const shortUrlCreationResult = {
|
||||
loading: false,
|
||||
};
|
||||
const createShortUrl = sinon.spy();
|
||||
|
||||
beforeEach(() => {
|
||||
const CreateShortUrl = createShortUrlsCreator(TagsSelector);
|
||||
|
||||
wrapper = shallow(
|
||||
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
||||
);
|
||||
|
|
|
@ -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('<SearchBar />', () => {
|
||||
let wrapper;
|
||||
const listShortUrlsMock = sinon.spy();
|
||||
const SearchBar = searchBarCreator({});
|
||||
|
||||
afterEach(() => {
|
||||
listShortUrlsMock.resetHistory();
|
||||
|
@ -18,13 +19,13 @@ describe('<SearchBar />', () => {
|
|||
});
|
||||
|
||||
it('renders a SearchField', () => {
|
||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{}} />);
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
|
||||
|
||||
expect(wrapper.find(SearchField)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders no tags when the list of tags is empty', () => {
|
||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{}} />);
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
|
||||
|
||||
expect(wrapper.find(Tag)).toHaveLength(0);
|
||||
});
|
||||
|
@ -32,13 +33,13 @@ describe('<SearchBar />', () => {
|
|||
it('renders the proper amount of tags', () => {
|
||||
const tags = [ 'foo', 'bar', 'baz' ];
|
||||
|
||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{ tags }} />);
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{ tags }} />);
|
||||
|
||||
expect(wrapper.find(Tag)).toHaveLength(tags.length);
|
||||
});
|
||||
|
||||
it('updates short URLs list when search field changes', () => {
|
||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
|
||||
const searchField = wrapper.find(SearchField);
|
||||
|
||||
expect(listShortUrlsMock.callCount).toEqual(0);
|
||||
|
@ -48,7 +49,7 @@ describe('<SearchBar />', () => {
|
|||
|
||||
it('updates short URLs list when a tag is removed', () => {
|
||||
wrapper = shallow(
|
||||
<SearchBarComponent shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />
|
||||
<SearchBar shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />
|
||||
);
|
||||
const tag = wrapper.find(Tag).first();
|
||||
|
||||
|
|
|
@ -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('<ShortUrlsList />', () => {
|
||||
describe('<ShortUrls />', () => {
|
||||
let wrapper;
|
||||
const SearchBar = () => '';
|
||||
const ShortUrlsList = () => '';
|
||||
|
||||
beforeEach(() => {
|
||||
const params = {
|
||||
|
@ -14,6 +14,8 @@ describe('<ShortUrlsList />', () => {
|
|||
page: '1',
|
||||
};
|
||||
|
||||
const ShortUrls = shortUrlsCreator(SearchBar, ShortUrlsList);
|
||||
|
||||
wrapper = shallow(<ShortUrls match={{ params }} shortUrlsList={{ data: [] }} />);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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('<DeleteShortUrlModal />', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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('<TagCard />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const TagCard = createTagCard(() => '', () => '', {});
|
||||
|
||||
wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />);
|
||||
});
|
||||
afterEach(() => wrapper.unmount());
|
||||
|
|
|
@ -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('<TagsList />', () => {
|
||||
let wrapper;
|
||||
const filterTags = sinon.spy();
|
||||
const TagCard = () => '';
|
||||
const createWrapper = (tagsList) => {
|
||||
const params = { serverId: '1' };
|
||||
const TagsList = createTagsList(TagCard);
|
||||
|
||||
wrapper = shallow(
|
||||
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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) => () =>
|
26
test/utils/services/ShlinkApiClientBuilder.test.js
Normal file
26
test/utils/services/ShlinkApiClientBuilder.test.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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('<ShortUrlVisits />', () => {
|
|||
};
|
||||
|
||||
const createComponent = (shortUrlVisits) => {
|
||||
const ShortUrlVisits = createShortUrlVisits({
|
||||
processBrowserStats: statsProcessor,
|
||||
processCountriesStats: statsProcessor,
|
||||
processOsStats: statsProcessor,
|
||||
processReferrersStats: statsProcessor,
|
||||
});
|
||||
|
||||
wrapper = shallow(
|
||||
<ShortUrlsVisits
|
||||
<ShortUrlVisits
|
||||
getShortUrlDetail={identity}
|
||||
getShortUrlVisits={getShortUrlVisitsMock}
|
||||
processBrowserStats={statsProcessor}
|
||||
processCountriesStats={statsProcessor}
|
||||
processOsStats={statsProcessor}
|
||||
processReferrersStats={statsProcessor}
|
||||
match={match}
|
||||
shortUrlVisits={shortUrlVisits}
|
||||
shortUrlDetail={{}}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as sinon from 'sinon';
|
||||
import reducer, {
|
||||
_getShortUrlDetail,
|
||||
getShortUrlDetail,
|
||||
GET_SHORT_URL_DETAIL_START,
|
||||
GET_SHORT_URL_DETAIL_ERROR,
|
||||
GET_SHORT_URL_DETAIL,
|
||||
|
@ -50,6 +50,7 @@ describe('shortUrlDetailReducer', () => {
|
|||
getShortUrl: sinon.fake.returns(returned),
|
||||
});
|
||||
const dispatchMock = sinon.spy();
|
||||
const getState = () => ({});
|
||||
|
||||
beforeEach(() => dispatchMock.resetHistory());
|
||||
|
||||
|
@ -57,7 +58,7 @@ describe('shortUrlDetailReducer', () => {
|
|||
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
||||
const expectedDispatchCalls = 2;
|
||||
|
||||
await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock);
|
||||
await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||
|
||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||
const { type: firstCallType } = firstCallArg;
|
||||
|
@ -76,7 +77,7 @@ describe('shortUrlDetailReducer', () => {
|
|||
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl));
|
||||
const expectedDispatchCalls = 2;
|
||||
|
||||
await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock);
|
||||
await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||
|
||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||
const { type: firstCallType } = firstCallArg;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue