mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +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).
|
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
|
## 1.2.0 - 2018-11-01
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
coverageDirectory: '<rootDir>/coverage',
|
coverageDirectory: '<rootDir>/coverage',
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
'src/**/*.{js,jsx,mjs}',
|
'src/**/*.js',
|
||||||
'!src/registerServiceWorker.js',
|
'!src/registerServiceWorker.js',
|
||||||
'!src/index.js',
|
'!src/index.js',
|
||||||
|
'!src/**/provideServices.js',
|
||||||
|
'!src/container/*.js',
|
||||||
],
|
],
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
'<rootDir>/config/polyfills.js',
|
'<rootDir>/config/polyfills.js',
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"@fortawesome/react-fontawesome": "0.0.19",
|
"@fortawesome/react-fontawesome": "0.0.19",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"bootstrap": "^4.1.1",
|
"bootstrap": "^4.1.1",
|
||||||
|
"bottlejs": "^1.7.1",
|
||||||
"chart.js": "^2.7.2",
|
"chart.js": "^2.7.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"csvjson": "^5.1.0",
|
"csvjson": "^5.1.0",
|
||||||
|
|
30
src/App.js
30
src/App.js
|
@ -1,23 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import './App.scss';
|
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() {
|
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
|
||||||
return (
|
<div className="container-fluid app-container">
|
||||||
<div className="container-fluid app-container">
|
<MainHeader />
|
||||||
<MainHeader />
|
|
||||||
|
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/server/create" component={CreateServer} />
|
<Route exact path="/server/create" component={CreateServer} />
|
||||||
<Route exact path="/" component={Home} />
|
<Route exact path="/" component={Home} />
|
||||||
<Route path="/server/:serverId" component={MenuLayout} />
|
<Route path="/server/:serverId" component={MenuLayout} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
|
@ -6,9 +6,8 @@ import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import DeleteServerButton from '../servers/DeleteServerButton';
|
|
||||||
import './AsideMenu.scss';
|
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import './AsideMenu.scss';
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: '',
|
className: '',
|
||||||
|
@ -20,51 +19,57 @@ const propTypes = {
|
||||||
showOnMobile: PropTypes.bool,
|
showOnMobile: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AsideMenu({ selectedServer, className, showOnMobile }) {
|
const AsideMenu = (DeleteServerButton) => {
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
|
||||||
const asideClass = classnames('aside-menu', className, {
|
const serverId = selectedServer ? selectedServer.id : '';
|
||||||
'aside-menu--hidden': !showOnMobile,
|
const asideClass = classnames('aside-menu', className, {
|
||||||
});
|
'aside-menu--hidden': !showOnMobile,
|
||||||
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
});
|
||||||
|
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={asideClass}>
|
<aside className={asideClass}>
|
||||||
<nav className="nav flex-column aside-menu__nav">
|
<nav className="nav flex-column aside-menu__nav">
|
||||||
<NavLink
|
<NavLink
|
||||||
className="aside-menu__item"
|
className="aside-menu__item"
|
||||||
activeClassName="aside-menu__item--selected"
|
activeClassName="aside-menu__item--selected"
|
||||||
to={`/server/${serverId}/list-short-urls/1`}
|
to={`/server/${serverId}/list-short-urls/1`}
|
||||||
isActive={shortUrlsIsActive}
|
isActive={shortUrlsIsActive}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={listIcon} />
|
<FontAwesomeIcon icon={listIcon} />
|
||||||
<span className="aside-menu__item-text">List short URLs</span>
|
<span className="aside-menu__item-text">List short URLs</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
className="aside-menu__item"
|
className="aside-menu__item"
|
||||||
activeClassName="aside-menu__item--selected"
|
activeClassName="aside-menu__item--selected"
|
||||||
to={`/server/${serverId}/create-short-url`}
|
to={`/server/${serverId}/create-short-url`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
|
||||||
<span className="aside-menu__item-text">Create short URL</span>
|
<span className="aside-menu__item-text">Create short URL</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavLink
|
<NavLink
|
||||||
className="aside-menu__item"
|
className="aside-menu__item"
|
||||||
activeClassName="aside-menu__item--selected"
|
activeClassName="aside-menu__item--selected"
|
||||||
to={`/server/${serverId}/manage-tags`}
|
to={`/server/${serverId}/manage-tags`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={tagsIcon} />
|
<FontAwesomeIcon icon={tagsIcon} />
|
||||||
<span className="aside-menu__item-text">Manage tags</span>
|
<span className="aside-menu__item-text">Manage tags</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<DeleteServerButton
|
<DeleteServerButton
|
||||||
className="aside-menu__item aside-menu__item--danger"
|
className="aside-menu__item aside-menu__item--danger"
|
||||||
server={selectedServer}
|
server={selectedServer}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
AsideMenu.defaultProps = defaultProps;
|
AsideMenu.defaultProps = defaultProps;
|
||||||
AsideMenu.propTypes = propTypes;
|
AsideMenu.propTypes = propTypes;
|
||||||
|
|
||||||
|
return AsideMenu;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AsideMenu;
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight';
|
import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, pick, values } from 'ramda';
|
import { isEmpty, values } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
import { ListGroup, ListGroupItem } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { resetSelectedServer } from '../servers/reducers/selectedServer';
|
|
||||||
import './Home.scss';
|
import './Home.scss';
|
||||||
|
|
||||||
export class HomeComponent extends React.Component {
|
export default class Home extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
resetSelectedServer: PropTypes.func,
|
resetSelectedServer: PropTypes.func,
|
||||||
servers: PropTypes.object,
|
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 arrowIcon from '@fortawesome/fontawesome-free-solid/faChevronDown';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
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 { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ServersDropdown from '../servers/ServersDropdown';
|
|
||||||
import './MainHeader.scss';
|
|
||||||
import shlinkLogo from './shlink-logo-white.png';
|
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 = {
|
static propTypes = {
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
@ -62,8 +61,6 @@ export class MainHeaderComponent extends React.Component {
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const MainHeader = withRouter(MainHeaderComponent);
|
|
||||||
|
|
||||||
export default MainHeader;
|
export default MainHeader;
|
||||||
|
|
|
@ -1,113 +1,100 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { compose } from 'redux';
|
|
||||||
import { pick } from 'ramda';
|
|
||||||
import Swipeable from 'react-swipeable';
|
import Swipeable from 'react-swipeable';
|
||||||
import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars';
|
import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import * as PropTypes from 'prop-types';
|
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 { serverType } from '../servers/prop-types';
|
||||||
import AsideMenu from './AsideMenu';
|
import './MenuLayout.scss';
|
||||||
|
|
||||||
export class MenuLayoutComponent extends React.Component {
|
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) =>
|
||||||
static propTypes = {
|
class MenuLayout extends React.Component {
|
||||||
match: PropTypes.object,
|
static propTypes = {
|
||||||
selectServer: PropTypes.func,
|
match: PropTypes.object,
|
||||||
location: PropTypes.object,
|
selectServer: PropTypes.func,
|
||||||
selectedServer: serverType,
|
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;
|
export default MenuLayout;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export class ScrollToTopComponent extends React.Component {
|
export default class ScrollToTop extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
window: PropTypes.shape({
|
window: PropTypes.shape({
|
||||||
|
@ -11,7 +10,7 @@ export class ScrollToTopComponent extends React.Component {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
window,
|
window: global.window,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -26,7 +25,3 @@ export class ScrollToTopComponent extends React.Component {
|
||||||
return this.props.children;
|
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 { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
|
||||||
import ReduxThunk from 'redux-thunk';
|
|
||||||
import { homepage } from '../package.json';
|
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 registerServiceWorker from './registerServiceWorker';
|
||||||
|
import container from './container';
|
||||||
|
import store from './container/store';
|
||||||
import '../node_modules/react-datepicker/dist/react-datepicker.css';
|
import '../node_modules/react-datepicker/dist/react-datepicker.css';
|
||||||
import './common/react-tagsinput.scss';
|
import './common/react-tagsinput.scss';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
const { App, ScrollToTop } = container;
|
||||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
|
||||||
: compose;
|
|
||||||
const store = createStore(reducers, composeEnhancers(
|
|
||||||
applyMiddleware(ReduxThunk)
|
|
||||||
));
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { stateFlagTimeout } from '../utils/utils';
|
import { stateFlagTimeout } from '../utils/utils';
|
||||||
import { resetSelectedServer } from './reducers/selectedServer';
|
|
||||||
import { createServer } from './reducers/server';
|
|
||||||
import './CreateServer.scss';
|
import './CreateServer.scss';
|
||||||
import ImportServersBtn from './helpers/ImportServersBtn';
|
|
||||||
|
|
||||||
const SHOW_IMPORT_MSG_TIME = 4000;
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
|
|
||||||
export class CreateServerComponent extends React.Component {
|
const CreateServer = (ImportServersBtn) => class CreateServer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
createServer: PropTypes.func,
|
createServer: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
|
@ -91,11 +87,6 @@ export class CreateServerComponent extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const CreateServer = connect(
|
|
||||||
pick([ 'selectedServer' ]),
|
|
||||||
{ createServer, resetSelectedServer }
|
|
||||||
)(CreateServerComponent);
|
|
||||||
|
|
||||||
export default CreateServer;
|
export default CreateServer;
|
||||||
|
|
|
@ -2,10 +2,9 @@ import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import DeleteServerModal from './DeleteServerModal';
|
|
||||||
import { serverType } from './prop-types';
|
import { serverType } from './prop-types';
|
||||||
|
|
||||||
export default class DeleteServerButton extends React.Component {
|
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
server: serverType,
|
server: serverType,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -36,4 +35,6 @@ export default class DeleteServerButton extends React.Component {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default DeleteServerButton;
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { compose } from 'redux';
|
|
||||||
import { deleteServer } from './reducers/server';
|
|
||||||
import { serverType } from './prop-types';
|
import { serverType } from './prop-types';
|
||||||
|
|
||||||
const propTypes = {
|
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 = () => {
|
const closeModal = () => {
|
||||||
deleteServer(server);
|
deleteServer(server);
|
||||||
toggle();
|
toggle();
|
||||||
|
@ -42,11 +38,6 @@ export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServe
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteServerModalComponent.propTypes = propTypes;
|
DeleteServerModal.propTypes = propTypes;
|
||||||
|
|
||||||
const DeleteServerModal = compose(
|
|
||||||
withRouter,
|
|
||||||
connect(null, { deleteServer })
|
|
||||||
)(DeleteServerModalComponent);
|
|
||||||
|
|
||||||
export default DeleteServerModal;
|
export default DeleteServerModal;
|
||||||
|
|
|
@ -1,30 +1,20 @@
|
||||||
import { isEmpty, pick, values } from 'ramda';
|
import { isEmpty, values } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
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';
|
import { serverType } from './prop-types';
|
||||||
|
|
||||||
export class ServersDropdownComponent extends React.Component {
|
const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component {
|
||||||
static defaultProps = {
|
|
||||||
serversExporter,
|
|
||||||
};
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
servers: PropTypes.object,
|
servers: PropTypes.object,
|
||||||
serversExporter: PropTypes.shape({
|
|
||||||
exportServers: PropTypes.func,
|
|
||||||
}),
|
|
||||||
selectedServer: serverType,
|
selectedServer: serverType,
|
||||||
selectServer: PropTypes.func,
|
selectServer: PropTypes.func,
|
||||||
listServers: PropTypes.func,
|
listServers: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
renderServers = () => {
|
renderServers = () => {
|
||||||
const { servers, selectedServer, selectServer, serversExporter } = this.props;
|
const { servers, selectedServer, selectServer } = this.props;
|
||||||
|
|
||||||
if (isEmpty(servers)) {
|
if (isEmpty(servers)) {
|
||||||
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
|
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
|
||||||
|
@ -68,11 +58,6 @@ export class ServersDropdownComponent extends React.Component {
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const ServersDropdown = connect(
|
|
||||||
pick([ 'servers', 'selectedServer' ]),
|
|
||||||
{ listServers, selectServer }
|
|
||||||
)(ServersDropdownComponent);
|
|
||||||
|
|
||||||
export default ServersDropdown;
|
export default ServersDropdown;
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { assoc } from 'ramda';
|
import { assoc } from 'ramda';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import PropTypes from 'prop-types';
|
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 = {
|
static defaultProps = {
|
||||||
serversImporter,
|
|
||||||
onImport: () => ({}),
|
onImport: () => ({}),
|
||||||
};
|
};
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onImport: PropTypes.func,
|
onImport: PropTypes.func,
|
||||||
serversImporter: serversImporterType,
|
|
||||||
createServers: PropTypes.func,
|
createServers: PropTypes.func,
|
||||||
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
|
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
|
||||||
};
|
};
|
||||||
|
@ -25,7 +20,8 @@ export class ImportServersBtnComponent extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { serversImporter: { importServersFromFile }, onImport, createServers } = this.props;
|
const { importServersFromFile } = serversImporter;
|
||||||
|
const { onImport, createServers } = this.props;
|
||||||
const onChange = (e) =>
|
const onChange = (e) =>
|
||||||
importServersFromFile(e.target.files[0])
|
importServersFromFile(e.target.files[0])
|
||||||
.then((servers) => servers.map((server) => assoc('id', uuid(), server)))
|
.then((servers) => servers.map((server) => assoc('id', uuid(), server)))
|
||||||
|
@ -56,8 +52,6 @@ export class ImportServersBtnComponent extends React.Component {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent);
|
|
||||||
|
|
||||||
export default ImportServersBtn;
|
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';
|
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* 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 resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER });
|
||||||
|
|
||||||
export const _selectServer = (shlinkApiClient, serversService, serverId) => (dispatch) => {
|
export const selectServer = (serversService) => (serverId) => (dispatch) => {
|
||||||
dispatch(resetShortUrlParams());
|
dispatch(resetShortUrlParams());
|
||||||
|
|
||||||
const selectedServer = serversService.findServerById(serverId);
|
const selectedServer = serversService.findServerById(serverId);
|
||||||
|
|
||||||
shlinkApiClient.setConfig(selectedServer);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SELECT_SERVER,
|
type: SELECT_SERVER,
|
||||||
selectedServer,
|
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 const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
|
||||||
|
|
||||||
export default function reducer(state = {}, action) {
|
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,
|
type: FETCH_SERVERS,
|
||||||
servers: serversService.listServers(),
|
servers: serversService.listServers(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listServers = () => _listServers(serversService);
|
export const createServer = (serversService) => (server) => {
|
||||||
|
|
||||||
export const _createServer = (serversService, server) => {
|
|
||||||
serversService.createServer(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);
|
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);
|
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 { dissoc, head, keys, values } from 'ramda';
|
||||||
import csvjson from 'csvjson';
|
|
||||||
import serversService from './ServersService';
|
|
||||||
|
|
||||||
const saveCsv = (window, csv) => {
|
const saveCsv = (window, csv) => {
|
||||||
const { navigator, document } = window;
|
const { navigator, document } = window;
|
||||||
|
@ -26,7 +24,7 @@ const saveCsv = (window, csv) => {
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ServersExporter {
|
export default class ServersExporter {
|
||||||
constructor(serversService, window, csvjson) {
|
constructor(serversService, window, csvjson) {
|
||||||
this.serversService = serversService;
|
this.serversService = serversService;
|
||||||
this.window = window;
|
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';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export const serversImporterType = PropTypes.shape({
|
export const serversImporterType = PropTypes.shape({
|
||||||
importServersFromFile: PropTypes.func,
|
importServersFromFile: PropTypes.func,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class ServersImporter {
|
export default class ServersImporter {
|
||||||
constructor(csvjson) {
|
constructor(csvjson) {
|
||||||
this.csvjson = 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 { assoc, dissoc, reduce } from 'ramda';
|
||||||
import storage from '../../utils/Storage';
|
|
||||||
|
|
||||||
const SERVERS_STORAGE_KEY = 'servers';
|
const SERVERS_STORAGE_KEY = 'servers';
|
||||||
|
|
||||||
export class ServersService {
|
export default class ServersService {
|
||||||
constructor(storage) {
|
constructor(storage) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +29,3 @@ export class ServersService {
|
||||||
dissoc(server.id, this.listServers())
|
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 downIcon from '@fortawesome/fontawesome-free-solid/faAngleDoubleDown';
|
||||||
import upIcon from '@fortawesome/fontawesome-free-solid/faAngleDoubleUp';
|
import upIcon from '@fortawesome/fontawesome-free-solid/faAngleDoubleUp';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Collapse } from 'reactstrap';
|
import { Collapse } from 'reactstrap';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import DateInput from '../utils/DateInput';
|
import DateInput from '../utils/DateInput';
|
||||||
import TagsSelector from '../tags/helpers/TagsSelector';
|
|
||||||
import CreateShortUrlResult from './helpers/CreateShortUrlResult';
|
import CreateShortUrlResult from './helpers/CreateShortUrlResult';
|
||||||
import { createShortUrl, createShortUrlResultType, resetCreateShortUrl } from './reducers/shortUrlCreation';
|
import { createShortUrlResultType } from './reducers/shortUrlCreation';
|
||||||
|
|
||||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
const formatDate = (date) => isNil(date) ? date : date.format();
|
const formatDate = (date) => isNil(date) ? date : date.format();
|
||||||
|
|
||||||
export class CreateShortUrlComponent extends React.Component {
|
const CreateShortUrl = (TagsSelector) => class CreateShortUrl extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
createShortUrl: PropTypes.func,
|
createShortUrl: PropTypes.func,
|
||||||
shortUrlCreationResult: createShortUrlResultType,
|
shortUrlCreationResult: createShortUrlResultType,
|
||||||
|
@ -122,11 +120,6 @@ export class CreateShortUrlComponent extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const CreateShortUrl = connect(pick([ 'shortUrlCreationResult' ]), {
|
|
||||||
createShortUrl,
|
|
||||||
resetCreateShortUrl,
|
|
||||||
})(CreateShortUrlComponent);
|
|
||||||
|
|
||||||
export default CreateShortUrl;
|
export default CreateShortUrl;
|
||||||
|
|
|
@ -1,55 +1,56 @@
|
||||||
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
|
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { isEmpty } from 'ramda';
|
||||||
import { isEmpty, pick } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Tag from '../tags/helpers/Tag';
|
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import { listShortUrls } from './reducers/shortUrlsList';
|
import Tag from '../tags/helpers/Tag';
|
||||||
import './SearchBar.scss';
|
|
||||||
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||||
|
import './SearchBar.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
listShortUrls: PropTypes.func,
|
listShortUrls: PropTypes.func,
|
||||||
shortUrlsListParams: shortUrlsListParamsType,
|
shortUrlsListParams: shortUrlsListParamsType,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) {
|
const SearchBar = (colorGenerator) => {
|
||||||
const selectedTags = shortUrlsListParams.tags || [];
|
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
|
||||||
|
const selectedTags = shortUrlsListParams.tags || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="serach-bar-container">
|
<div className="serach-bar-container">
|
||||||
<SearchField onChange={
|
<SearchField onChange={
|
||||||
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
|
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isEmpty(selectedTags) && (
|
{!isEmpty(selectedTags) && (
|
||||||
<h4 className="search-bar__selected-tag mt-2">
|
<h4 className="search-bar__selected-tag mt-2">
|
||||||
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
|
||||||
|
|
||||||
{selectedTags.map((tag) => (
|
{selectedTags.map((tag) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag}
|
colorGenerator={colorGenerator}
|
||||||
text={tag}
|
key={tag}
|
||||||
clearable
|
text={tag}
|
||||||
onClose={() => listShortUrls(
|
clearable
|
||||||
{
|
onClose={() => listShortUrls(
|
||||||
...shortUrlsListParams,
|
{
|
||||||
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
...shortUrlsListParams,
|
||||||
}
|
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||||
)}
|
}
|
||||||
/>
|
)}
|
||||||
))}
|
/>
|
||||||
</h4>
|
))}
|
||||||
)}
|
</h4>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
SearchBarComponent.propTypes = propTypes;
|
SearchBar.propTypes = propTypes;
|
||||||
|
|
||||||
const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent);
|
return SearchBar;
|
||||||
|
};
|
||||||
|
|
||||||
export default SearchBar;
|
export default SearchBar;
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { assoc } from 'ramda';
|
|
||||||
import Paginator from './Paginator';
|
import Paginator from './Paginator';
|
||||||
import SearchBar from './SearchBar';
|
|
||||||
import ShortUrlsList from './ShortUrlsList';
|
|
||||||
|
|
||||||
export function ShortUrlsComponent(props) {
|
const ShortUrls = (SearchBar, ShortUrlsList) => (props) => {
|
||||||
const { match: { params } } = 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
|
// 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 (
|
return (
|
||||||
<div className="shlink-container">
|
<div className="shlink-container">
|
||||||
<div className="form-group"><SearchBar /></div>
|
<div className="form-group"><SearchBar /></div>
|
||||||
<ShortUrlsList {...props} shortUrlsList={props.shortUrlsList.data || []} key={urlsListKey} />
|
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
|
||||||
<Paginator paginator={props.shortUrlsList.pagination} serverId={props.match.params.serverId} />
|
<Paginator paginator={pagination} serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ShortUrls = connect(
|
|
||||||
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
|
|
||||||
)(ShortUrlsComponent);
|
|
||||||
|
|
||||||
export default ShortUrls;
|
export default ShortUrls;
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown';
|
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown';
|
||||||
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp';
|
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
import SortingDropdown from '../utils/SortingDropdown';
|
import SortingDropdown from '../utils/SortingDropdown';
|
||||||
import { determineOrderDir } from '../utils/utils';
|
import { determineOrderDir } from '../utils/utils';
|
||||||
import { ShortUrlsRow } from './helpers/ShortUrlsRow';
|
import { shortUrlType } from './reducers/shortUrlsList';
|
||||||
import { listShortUrls, shortUrlType } from './reducers/shortUrlsList';
|
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||||
import { resetShortUrlParams, shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
|
||||||
import './ShortUrlsList.scss';
|
import './ShortUrlsList.scss';
|
||||||
|
|
||||||
const SORTABLE_FIELDS = {
|
const SORTABLE_FIELDS = {
|
||||||
|
@ -21,7 +19,7 @@ const SORTABLE_FIELDS = {
|
||||||
visits: 'Visits',
|
visits: 'Visits',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShortUrlsListComponent extends React.Component {
|
const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
listShortUrls: PropTypes.func,
|
listShortUrls: PropTypes.func,
|
||||||
resetShortUrlParams: PropTypes.func,
|
resetShortUrlParams: PropTypes.func,
|
||||||
|
@ -167,11 +165,6 @@ export class ShortUrlsListComponent extends React.Component {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const ShortUrlsList = connect(
|
|
||||||
pick([ 'selectedServer', 'shortUrlsListParams' ]),
|
|
||||||
{ listShortUrls, resetShortUrlParams }
|
|
||||||
)(ShortUrlsListComponent);
|
|
||||||
|
|
||||||
export default ShortUrlsList;
|
export default ShortUrlsList;
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { pick, identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import {
|
import { shortUrlDeletionType } from '../reducers/shortUrlDeletion';
|
||||||
deleteShortUrl,
|
|
||||||
resetDeleteShortUrl,
|
|
||||||
shortUrlDeleted,
|
|
||||||
shortUrlDeletionType,
|
|
||||||
} from '../reducers/shortUrlDeletion';
|
|
||||||
import './QrCodeModal.scss';
|
|
||||||
|
|
||||||
export class DeleteShortUrlModalComponent extends Component {
|
export default class DeleteShortUrlModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
shortUrl: shortUrlType,
|
shortUrl: shortUrlType,
|
||||||
toggle: PropTypes.func,
|
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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { pick } from 'ramda';
|
import { shortUrlTagsType } from '../reducers/shortUrlTags';
|
||||||
import TagsSelector from '../../tags/helpers/TagsSelector';
|
|
||||||
import {
|
|
||||||
editShortUrlTags,
|
|
||||||
resetShortUrlsTags,
|
|
||||||
shortUrlTagsType,
|
|
||||||
shortUrlTagsEdited,
|
|
||||||
} from '../reducers/shortUrlTags';
|
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
|
|
||||||
export class EditTagsModalComponent extends React.Component {
|
const EditTagsModal = (TagsSelector) => class EditTagsModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
toggle: PropTypes.func.isRequired,
|
toggle: PropTypes.func.isRequired,
|
||||||
|
@ -88,11 +80,6 @@ export class EditTagsModalComponent extends React.Component {
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const EditTagsModal = connect(
|
|
||||||
pick([ 'shortUrlTags' ]),
|
|
||||||
{ editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited }
|
|
||||||
)(EditTagsModalComponent);
|
|
||||||
|
|
||||||
export default EditTagsModal;
|
export default EditTagsModal;
|
||||||
|
|
|
@ -2,16 +2,15 @@ import { isEmpty } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Tag from '../../tags/helpers/Tag';
|
|
||||||
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
|
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
|
||||||
import { serverType } from '../../servers/prop-types';
|
import { serverType } from '../../servers/prop-types';
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import { stateFlagTimeout } from '../../utils/utils';
|
import { stateFlagTimeout } from '../../utils/utils';
|
||||||
import { ShortUrlsRowMenu } from './ShortUrlsRowMenu';
|
import Tag from '../../tags/helpers/Tag';
|
||||||
import './ShortUrlsRow.scss';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
export class ShortUrlsRow extends React.Component {
|
const ShortUrlsRow = (ShortUrlsRowMenu, colorGenerator) => class ShortUrlsRow extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
refreshList: PropTypes.func,
|
refreshList: PropTypes.func,
|
||||||
shortUrlsListParams: shortUrlsListParamsType,
|
shortUrlsListParams: shortUrlsListParamsType,
|
||||||
|
@ -31,6 +30,7 @@ export class ShortUrlsRow extends React.Component {
|
||||||
|
|
||||||
return tags.map((tag) => (
|
return tags.map((tag) => (
|
||||||
<Tag
|
<Tag
|
||||||
|
colorGenerator={colorGenerator}
|
||||||
key={tag}
|
key={tag}
|
||||||
text={tag}
|
text={tag}
|
||||||
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
|
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
|
||||||
|
@ -72,4 +72,6 @@ export class ShortUrlsRow extends React.Component {
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ShortUrlsRow;
|
||||||
|
|
|
@ -15,11 +15,9 @@ import { serverType } from '../../servers/prop-types';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import PreviewModal from './PreviewModal';
|
import PreviewModal from './PreviewModal';
|
||||||
import QrCodeModal from './QrCodeModal';
|
import QrCodeModal from './QrCodeModal';
|
||||||
import EditTagsModal from './EditTagsModal';
|
|
||||||
import DeleteShortUrlModal from './DeleteShortUrlModal';
|
|
||||||
import './ShortUrlsRowMenu.scss';
|
import './ShortUrlsRowMenu.scss';
|
||||||
|
|
||||||
export class ShortUrlsRowMenu extends React.Component {
|
const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal) => class ShortUrlsRowMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
completeShortUrl: PropTypes.string,
|
completeShortUrl: PropTypes.string,
|
||||||
onCopyToClipboard: PropTypes.func,
|
onCopyToClipboard: PropTypes.func,
|
||||||
|
@ -105,4 +103,6 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
</ButtonDropdown>
|
</ButtonDropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ShortUrlsRowMenu;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { curry } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
export const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
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 });
|
dispatch({ type: CREATE_SHORT_URL_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await shlinkApiClient.createShortUrl(data);
|
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 });
|
export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL });
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { curry } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
const DELETE_SHORT_URL_START = 'shlink/deleteShortUrl/DELETE_SHORT_URL_START';
|
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 });
|
dispatch({ type: DELETE_SHORT_URL_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shlinkApiClient.deleteShortUrl(shortCode);
|
await shlinkApiClient.deleteShortUrl(shortCode);
|
||||||
dispatch({ type: DELETE_SHORT_URL, 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 resetDeleteShortUrl = () => ({ type: RESET_DELETE_SHORT_URL });
|
||||||
|
|
||||||
export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode });
|
export const shortUrlDeleted = (shortCode) => ({ type: SHORT_URL_DELETED, shortCode });
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { curry } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
import { pick } from 'ramda';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
export const EDIT_SHORT_URL_TAGS_START = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_START';
|
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:
|
case EDIT_SHORT_URL_TAGS:
|
||||||
return {
|
return {
|
||||||
shortCode: action.shortCode,
|
...pick([ 'shortCode', 'tags' ], action),
|
||||||
tags: action.tags,
|
|
||||||
saving: false,
|
saving: false,
|
||||||
error: 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 });
|
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const normalizedTags = await shlinkApiClient.updateShortUrlTags(shortCode, tags);
|
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 resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS });
|
||||||
|
|
||||||
export const shortUrlTagsEdited = (shortCode, tags) => ({
|
export const shortUrlTagsEdited = (shortCode, tags) => ({
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { assoc, assocPath, reject } from 'ramda';
|
import { assoc, assocPath, reject } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||||
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
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 });
|
dispatch({ type: LIST_SHORT_URLS_START });
|
||||||
|
|
||||||
|
const { selectedServer = {} } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shortUrls = await shlinkApiClient.listShortUrls(params);
|
const shortUrls = await shlinkApiClient.listShortUrls(params);
|
||||||
|
|
||||||
|
@ -66,5 +68,3 @@ export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch)
|
||||||
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
|
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 { Link } from 'react-router-dom';
|
||||||
import TagBullet from './helpers/TagBullet';
|
import TagBullet from './helpers/TagBullet';
|
||||||
import './TagCard.scss';
|
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 = {
|
static propTypes = {
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
currentServerId: PropTypes.string,
|
currentServerId: PropTypes.string,
|
||||||
|
@ -35,7 +33,7 @@ export default class TagCard extends React.Component {
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</button>
|
</button>
|
||||||
<h5 className="tag-card__tag-title">
|
<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>
|
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>{tag}</Link>
|
||||||
</h5>
|
</h5>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
@ -45,4 +43,6 @@ export default class TagCard extends React.Component {
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default TagCard;
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { splitEvery } from 'ramda';
|
||||||
import { pick, splitEvery } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import MuttedMessage from '../utils/MuttedMessage';
|
import MuttedMessage from '../utils/MuttedMessage';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import { filterTags, forceListTags } from './reducers/tagsList';
|
|
||||||
import TagCard from './TagCard';
|
|
||||||
|
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
const TAGS_GROUPS_AMOUNT = 4;
|
const TAGS_GROUPS_AMOUNT = 4;
|
||||||
|
|
||||||
export class TagsListComponent extends React.Component {
|
const TagsList = (TagCard) => class TagsList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
filterTags: PropTypes.func,
|
filterTags: PropTypes.func,
|
||||||
forceListTags: PropTypes.func,
|
forceListTags: PropTypes.func,
|
||||||
|
@ -82,8 +79,6 @@ export class TagsListComponent extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const TagsList = connect(pick([ 'tagsList' ]), { forceListTags, filterTags })(TagsListComponent);
|
|
||||||
|
|
||||||
export default TagsList;
|
export default TagsList;
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { pick } from 'ramda';
|
import { tagDeleteType } from '../reducers/tagDelete';
|
||||||
import { deleteTag, tagDeleted, tagDeleteType } from '../reducers/tagDelete';
|
|
||||||
|
|
||||||
export class DeleteTagConfirmModalComponent extends React.Component {
|
export default class DeleteTagConfirmModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tag: PropTypes.string.isRequired,
|
tag: PropTypes.string.isRequired,
|
||||||
toggle: PropTypes.func.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 React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
|
||||||
import { pick } from 'ramda';
|
|
||||||
import { ChromePicker } from 'react-color';
|
import { ChromePicker } from 'react-color';
|
||||||
import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette';
|
import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
|
|
||||||
import { editTag, tagEdited } from '../reducers/tagEdit';
|
|
||||||
import './EditTagModal.scss';
|
import './EditTagModal.scss';
|
||||||
|
|
||||||
export class EditTagModalComponent extends React.Component {
|
const EditTagModal = ({ getColorForKey }) => class EditTagModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
editTag: PropTypes.func,
|
editTag: PropTypes.func,
|
||||||
toggle: PropTypes.func,
|
toggle: PropTypes.func,
|
||||||
tagEdited: PropTypes.func,
|
tagEdited: PropTypes.func,
|
||||||
colorGenerator: colorGeneratorType,
|
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
tagEdit: PropTypes.shape({
|
tagEdit: PropTypes.shape({
|
||||||
error: PropTypes.bool,
|
error: PropTypes.bool,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
static defaultProps = {
|
|
||||||
colorGenerator,
|
|
||||||
};
|
|
||||||
|
|
||||||
saveTag = (e) => {
|
saveTag = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -53,12 +45,12 @@ export class EditTagModalComponent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { colorGenerator, tag } = props;
|
const { tag } = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showColorPicker: false,
|
showColorPicker: false,
|
||||||
tag,
|
tag,
|
||||||
color: colorGenerator.getColorForKey(tag),
|
color: getColorForKey(tag),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +123,6 @@ export class EditTagModalComponent extends React.Component {
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const EditTagModal = connect(pick([ 'tagEdit' ]), { editTag, tagEdited })(EditTagModalComponent);
|
|
||||||
|
|
||||||
export default EditTagModal;
|
export default EditTagModal;
|
||||||
|
|
|
@ -1,41 +1,35 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
|
|
||||||
import './Tag.scss';
|
import './Tag.scss';
|
||||||
|
import { colorGeneratorType } from '../../utils/services/ColorGenerator';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
colorGenerator: colorGeneratorType,
|
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
clearable: PropTypes.bool,
|
clearable: PropTypes.bool,
|
||||||
|
colorGenerator: colorGeneratorType,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
const defaultProps = {
|
|
||||||
|
const Tag = ({
|
||||||
|
text,
|
||||||
|
children,
|
||||||
|
clearable,
|
||||||
colorGenerator,
|
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;
|
Tag.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Tag;
|
||||||
|
|
|
@ -1,24 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
|
import { colorGeneratorType } from '../../utils/services/ColorGenerator';
|
||||||
import './TagBullet.scss';
|
import './TagBullet.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
tag: PropTypes.string.isRequired,
|
tag: PropTypes.string.isRequired,
|
||||||
colorGenerator: colorGeneratorType,
|
colorGenerator: colorGeneratorType,
|
||||||
};
|
};
|
||||||
const defaultProps = {
|
|
||||||
colorGenerator,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TagBullet({ tag, colorGenerator }) {
|
const TagBullet = ({ tag, colorGenerator }) => (
|
||||||
return (
|
<div
|
||||||
<div
|
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||||
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
className="tag-bullet"
|
||||||
className="tag-bullet"
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TagBullet.propTypes = propTypes;
|
TagBullet.propTypes = propTypes;
|
||||||
TagBullet.defaultProps = defaultProps;
|
|
||||||
|
export default TagBullet;
|
||||||
|
|
|
@ -1,26 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import TagsInput from 'react-tagsinput';
|
import TagsInput from 'react-tagsinput';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Autosuggest from 'react-autosuggest';
|
import Autosuggest from 'react-autosuggest';
|
||||||
import { pick, identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { listTags } from '../reducers/tagsList';
|
|
||||||
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
|
|
||||||
import './TagsSelector.scss';
|
|
||||||
import TagBullet from './TagBullet';
|
import TagBullet from './TagBullet';
|
||||||
|
import './TagsSelector.scss';
|
||||||
|
|
||||||
export class TagsSelectorComponent extends React.Component {
|
const TagsSelector = (colorGenerator) => class TagsSelector extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
colorGenerator: colorGeneratorType,
|
|
||||||
tagsList: PropTypes.shape({
|
tagsList: PropTypes.shape({
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
colorGenerator,
|
|
||||||
placeholder: 'Add tags to the URL',
|
placeholder: 'Add tags to the URL',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,7 +26,7 @@ export class TagsSelectorComponent extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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 }) => (
|
const renderTag = ({ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }) => (
|
||||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||||
{getTagDisplayValue(tag)}
|
{getTagDisplayValue(tag)}
|
||||||
|
@ -59,7 +54,7 @@ export class TagsSelectorComponent extends React.Component {
|
||||||
getSuggestionValue={(suggestion) => suggestion}
|
getSuggestionValue={(suggestion) => suggestion}
|
||||||
renderSuggestion={(suggestion) => (
|
renderSuggestion={(suggestion) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TagBullet tag={suggestion} />
|
<TagBullet tag={suggestion} colorGenerator={colorGenerator} />
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
@ -86,8 +81,6 @@ export class TagsSelectorComponent extends React.Component {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const TagsSelector = connect(pick([ 'tagsList' ]), { listTags })(TagsSelectorComponent);
|
|
||||||
|
|
||||||
export default TagsSelector;
|
export default TagsSelector;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { curry } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
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 });
|
dispatch({ type: DELETE_TAG_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shlinkApiClient.deleteTags([ tag ]);
|
await shlinkApiClient.deleteTags([ tag ]);
|
||||||
dispatch({ type: DELETE_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 });
|
export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag });
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { curry, pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import colorGenerator from '../../utils/ColorGenerator';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
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 });
|
dispatch({ type: EDIT_TAG_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shlinkApiClient.editTag(oldName, newName);
|
await shlinkApiClient.editTag(oldName, newName);
|
||||||
colorGenerator.setColorForKey(newName, color);
|
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) => ({
|
export const tagEdited = (oldName, newName, color) => ({
|
||||||
type: TAG_EDITED,
|
type: TAG_EDITED,
|
||||||
oldName,
|
oldName,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { isEmpty, reject } from 'ramda';
|
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_DELETED } from './tagDelete';
|
||||||
import { TAG_EDITED } from './tagEdit';
|
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) => {
|
export const _listTags = (buildShlinkApiClient, force = false) => async (dispatch, getState) => {
|
||||||
const { tagsList } = getState();
|
const { tagsList, selectedServer } = getState();
|
||||||
|
|
||||||
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
|
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
|
||||||
return;
|
return;
|
||||||
|
@ -76,6 +76,7 @@ export const _listTags = (shlinkApiClient, force = false) => async (dispatch, ge
|
||||||
dispatch({ type: LIST_TAGS_START });
|
dispatch({ type: LIST_TAGS_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
const tags = await shlinkApiClient.listTags();
|
const tags = await shlinkApiClient.listTags();
|
||||||
|
|
||||||
dispatch({ tags, type: LIST_TAGS });
|
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) => ({
|
export const filterTags = (searchTerm) => ({
|
||||||
type: FILTER_TAGS,
|
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 { range } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import storage from './Storage';
|
|
||||||
|
|
||||||
const HEX_COLOR_LENGTH = 6;
|
const HEX_COLOR_LENGTH = 6;
|
||||||
const { floor, random } = Math;
|
const { floor, random } = Math;
|
||||||
|
@ -13,7 +12,7 @@ const buildRandomColor = () =>
|
||||||
}`;
|
}`;
|
||||||
const normalizeKey = (key) => key.toLowerCase().trim();
|
const normalizeKey = (key) => key.toLowerCase().trim();
|
||||||
|
|
||||||
export class ColorGenerator {
|
export default class ColorGenerator {
|
||||||
constructor(storage) {
|
constructor(storage) {
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.colors = this.storage.get('colors') || {};
|
this.colors = this.storage.get('colors') || {};
|
||||||
|
@ -45,7 +44,3 @@ export const colorGeneratorType = PropTypes.shape({
|
||||||
getColorForKey: PropTypes.func,
|
getColorForKey: PropTypes.func,
|
||||||
setColorForKey: 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 qs from 'qs';
|
||||||
import { isEmpty, isNil, reject } from 'ramda';
|
import { isEmpty, isNil, reject } from 'ramda';
|
||||||
|
|
||||||
const API_VERSION = '1';
|
const API_VERSION = '1';
|
||||||
const STATUS_UNAUTHORIZED = 401;
|
const STATUS_UNAUTHORIZED = 401;
|
||||||
|
const buildRestUrl = (url) => url ? `${url}/rest/v${API_VERSION}` : '';
|
||||||
|
|
||||||
export class ShlinkApiClient {
|
export default class ShlinkApiClient {
|
||||||
constructor(axios) {
|
constructor(axios, baseUrl, apiKey) {
|
||||||
this.axios = axios;
|
this.axios = axios;
|
||||||
this._baseUrl = '';
|
this._baseUrl = buildRestUrl(baseUrl);
|
||||||
this._apiKey = '';
|
this._apiKey = apiKey || '';
|
||||||
this._token = '';
|
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 = {}) =>
|
listShortUrls = (options = {}) =>
|
||||||
this._performRequest('/short-codes', 'GET', options)
|
this._performRequest('/short-codes', 'GET', options)
|
||||||
.then((resp) => resp.data.shortUrls)
|
.then((resp) => resp.data.shortUrls)
|
||||||
|
@ -113,7 +105,3 @@ export class ShlinkApiClient {
|
||||||
return Promise.reject(e);
|
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 PREFIX = 'shlink';
|
||||||
const buildPath = (path) => `${PREFIX}.${path}`;
|
const buildPath = (path) => `${PREFIX}.${path}`;
|
||||||
|
|
||||||
export class Storage {
|
export default class Storage {
|
||||||
constructor(localStorage) {
|
constructor(localStorage) {
|
||||||
this.localStorage = localStorage;
|
this.localStorage = localStorage;
|
||||||
}
|
}
|
||||||
|
@ -14,15 +14,3 @@ export class Storage {
|
||||||
|
|
||||||
set = (key, value) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
|
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 preloader from '@fortawesome/fontawesome-free-solid/faCircleNotch';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import { isEmpty, mapObjIndexed, pick } from 'ramda';
|
import { isEmpty, mapObjIndexed } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Card } from 'reactstrap';
|
import { Card } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import DateInput from '../utils/DateInput';
|
import DateInput from '../utils/DateInput';
|
||||||
import MutedMessage from '../utils/MuttedMessage';
|
import MutedMessage from '../utils/MuttedMessage';
|
||||||
import SortableBarGraph from './SortableBarGraph';
|
import SortableBarGraph from './SortableBarGraph';
|
||||||
import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits';
|
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||||
import {
|
|
||||||
processBrowserStats,
|
|
||||||
processCountriesStats,
|
|
||||||
processOsStats,
|
|
||||||
processReferrersStats,
|
|
||||||
} from './services/VisitsParser';
|
|
||||||
import { VisitsHeader } from './VisitsHeader';
|
import { VisitsHeader } from './VisitsHeader';
|
||||||
import GraphCard from './GraphCard';
|
import GraphCard from './GraphCard';
|
||||||
import { getShortUrlDetail, shortUrlDetailType } from './reducers/shortUrlDetail';
|
import { shortUrlDetailType } from './reducers/shortUrlDetail';
|
||||||
import './ShortUrlVisits.scss';
|
import './ShortUrlVisits.scss';
|
||||||
|
|
||||||
export class ShortUrlsVisitsComponent extends React.Component {
|
const ShortUrlVisits = ({
|
||||||
|
processOsStats,
|
||||||
|
processBrowserStats,
|
||||||
|
processCountriesStats,
|
||||||
|
processReferrersStats,
|
||||||
|
}) => class ShortUrlVisits extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
processOsStats: PropTypes.func,
|
|
||||||
processBrowserStats: PropTypes.func,
|
|
||||||
processCountriesStats: PropTypes.func,
|
|
||||||
processReferrersStats: PropTypes.func,
|
|
||||||
match: PropTypes.shape({
|
match: PropTypes.shape({
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
}),
|
}),
|
||||||
|
@ -34,12 +28,6 @@ export class ShortUrlsVisitsComponent extends React.Component {
|
||||||
getShortUrlDetail: PropTypes.func,
|
getShortUrlDetail: PropTypes.func,
|
||||||
shortUrlDetail: shortUrlDetailType,
|
shortUrlDetail: shortUrlDetailType,
|
||||||
};
|
};
|
||||||
static defaultProps = {
|
|
||||||
processOsStats,
|
|
||||||
processBrowserStats,
|
|
||||||
processCountriesStats,
|
|
||||||
processReferrersStats,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = { startDate: undefined, endDate: undefined };
|
state = { startDate: undefined, endDate: undefined };
|
||||||
loadVisits = () => {
|
loadVisits = () => {
|
||||||
|
@ -59,14 +47,7 @@ export class ShortUrlsVisitsComponent extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { shortUrlVisits, shortUrlDetail } = this.props;
|
||||||
processOsStats,
|
|
||||||
processBrowserStats,
|
|
||||||
processCountriesStats,
|
|
||||||
processReferrersStats,
|
|
||||||
shortUrlVisits,
|
|
||||||
shortUrlDetail,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const renderVisitsContent = () => {
|
const renderVisitsContent = () => {
|
||||||
const { visits, loading, error } = shortUrlVisits;
|
const { visits, loading, error } = shortUrlVisits;
|
||||||
|
@ -153,11 +134,6 @@ export class ShortUrlsVisitsComponent extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const ShortUrlsVisits = connect(
|
export default ShortUrlVisits;
|
||||||
pick([ 'shortUrlVisits', 'shortUrlDetail' ]),
|
|
||||||
{ getShortUrlVisits, getShortUrlDetail }
|
|
||||||
)(ShortUrlsVisitsComponent);
|
|
||||||
|
|
||||||
export default ShortUrlsVisits;
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { curry } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { shortUrlType } from '../../short-urls/reducers/shortUrlsList';
|
import { shortUrlType } from '../../short-urls/reducers/shortUrlsList';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* 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 });
|
dispatch({ type: GET_SHORT_URL_DETAIL_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shortUrl = await shlinkApiClient.getShortUrl(shortCode);
|
const shortUrl = await shlinkApiClient.getShortUrl(shortCode);
|
||||||
|
|
||||||
|
@ -56,5 +57,3 @@ export const _getShortUrlDetail = (shlinkApiClient, shortCode) => async (dispatc
|
||||||
dispatch({ type: GET_SHORT_URL_DETAIL_ERROR });
|
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 PropTypes from 'prop-types';
|
||||||
import shlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements, newline-after-var */
|
/* eslint-disable padding-line-between-statements, newline-after-var */
|
||||||
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
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 });
|
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||||
|
|
||||||
|
const { selectedServer } = getState();
|
||||||
|
const shlinkApiClient = buildShlinkApiClient(selectedServer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const visits = await shlinkApiClient.getShortUrlVisits(shortCode, dates);
|
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 });
|
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 React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
import App from '../src/App';
|
import { identity } from 'ramda';
|
||||||
import MainHeader from '../src/common/MainHeader';
|
import appFactory from '../src/App';
|
||||||
|
|
||||||
describe('<App />', () => {
|
describe('<App />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
const MainHeader = () => '';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const App = appFactory(MainHeader, identity, identity, identity);
|
||||||
|
|
||||||
wrapper = shallow(<App />);
|
wrapper = shallow(<App />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import AsideMenu from '../../src/common/AsideMenu';
|
import asideMenuCreator from '../../src/common/AsideMenu';
|
||||||
|
|
||||||
describe('<AsideMenu />', () => {
|
describe('<AsideMenu />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
|
const DeleteServerButton = () => '';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const AsideMenu = asideMenuCreator(DeleteServerButton);
|
||||||
|
|
||||||
wrapped = shallow(<AsideMenu selectedServer={{ id: 'abc123' }} />);
|
wrapped = shallow(<AsideMenu selectedServer={{ id: 'abc123' }} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapped.unmount());
|
afterEach(() => wrapped.unmount());
|
||||||
|
@ -20,6 +23,6 @@ describe('<AsideMenu />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a button to delete server', () => {
|
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 { values } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { HomeComponent } from '../../src/common/Home';
|
import Home from '../../src/common/Home';
|
||||||
|
|
||||||
describe('<Home />', () => {
|
describe('<Home />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
|
@ -15,7 +15,7 @@ describe('<Home />', () => {
|
||||||
const createComponent = (props) => {
|
const createComponent = (props) => {
|
||||||
const actualProps = { ...defaultProps, ...props };
|
const actualProps = { ...defaultProps, ...props };
|
||||||
|
|
||||||
wrapped = shallow(<HomeComponent {...actualProps} />);
|
wrapped = shallow(<Home {...actualProps} />);
|
||||||
|
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,11 +2,11 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { CreateServerComponent } from '../../src/servers/CreateServer';
|
import createServerConstruct from '../../src/servers/CreateServer';
|
||||||
import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn';
|
|
||||||
|
|
||||||
describe('<CreateServer />', () => {
|
describe('<CreateServer />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
const ImportServersBtn = () => '';
|
||||||
const createServerMock = sinon.fake();
|
const createServerMock = sinon.fake();
|
||||||
const historyMock = {
|
const historyMock = {
|
||||||
push: sinon.fake(),
|
push: sinon.fake(),
|
||||||
|
@ -16,12 +16,10 @@ describe('<CreateServer />', () => {
|
||||||
createServerMock.resetHistory();
|
createServerMock.resetHistory();
|
||||||
historyMock.push.resetHistory();
|
historyMock.push.resetHistory();
|
||||||
|
|
||||||
|
const CreateServer = createServerConstruct(ImportServersBtn);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<CreateServerComponent
|
<CreateServer createServer={createServerMock} resetSelectedServer={identity} history={historyMock} />
|
||||||
createServer={createServerMock}
|
|
||||||
resetSelectedServer={identity}
|
|
||||||
history={historyMock}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import DeleteServerButton from '../../src/servers/DeleteServerButton';
|
import deleteServerButtonConstruct from '../../src/servers/DeleteServerButton';
|
||||||
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
||||||
|
|
||||||
describe('<DeleteServerButton />', () => {
|
describe('<DeleteServerButton />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const DeleteServerButton = deleteServerButtonConstruct(DeleteServerModal);
|
||||||
|
|
||||||
wrapper = shallow(<DeleteServerButton server={{}} className="button" />);
|
wrapper = shallow(<DeleteServerButton server={{}} className="button" />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import { DeleteServerModalComponent } from '../../src/servers/DeleteServerModal';
|
import DeleteServerModal from '../../src/servers/DeleteServerModal';
|
||||||
|
|
||||||
describe('<DeleteServerModal />', () => {
|
describe('<DeleteServerModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -17,7 +17,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
historyMock.push.resetHistory();
|
historyMock.push.resetHistory();
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<DeleteServerModalComponent
|
<DeleteServerModal
|
||||||
server={{ name: serverName }}
|
server={{ name: serverName }}
|
||||||
toggle={toggleMock}
|
toggle={toggleMock}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { identity, values } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
||||||
import { ServersDropdownComponent } from '../../src/servers/ServersDropdown';
|
import serversDropdownCreator from '../../src/servers/ServersDropdown';
|
||||||
|
|
||||||
describe('<ServersDropdown />', () => {
|
describe('<ServersDropdown />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
|
let ServersDropdown;
|
||||||
const servers = {
|
const servers = {
|
||||||
'1a': { name: 'foo', id: 1 },
|
'1a': { name: 'foo', id: 1 },
|
||||||
'2b': { name: 'bar', id: 2 },
|
'2b': { name: 'bar', id: 2 },
|
||||||
|
@ -13,7 +14,8 @@ describe('<ServersDropdown />', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapped = shallow(<ServersDropdownComponent servers={servers} listServers={identity} />);
|
ServersDropdown = serversDropdownCreator({});
|
||||||
|
wrapped = shallow(<ServersDropdown servers={servers} listServers={identity} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapped.unmount());
|
afterEach(() => wrapped.unmount());
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ describe('<ServersDropdown />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a message when no servers exist yet', () => {
|
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);
|
const item = wrapped.find(DropdownItem);
|
||||||
|
|
||||||
expect(item).toHaveLength(1);
|
expect(item).toHaveLength(1);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { ImportServersBtnComponent } from '../../../src/servers/helpers/ImportServersBtn';
|
import importServersBtnConstruct from '../../../src/servers/helpers/ImportServersBtn';
|
||||||
|
|
||||||
describe('<ImportServersBtn />', () => {
|
describe('<ImportServersBtn />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -21,13 +21,10 @@ describe('<ImportServersBtn />', () => {
|
||||||
serversImporterMock.importServersFromFile.resetHistory();
|
serversImporterMock.importServersFromFile.resetHistory();
|
||||||
fileRef.current.click.resetHistory();
|
fileRef.current.click.resetHistory();
|
||||||
|
|
||||||
|
const ImportServersBtn = importServersBtnConstruct(serversImporterMock);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ImportServersBtnComponent
|
<ImportServersBtn createServers={createServersMock} fileRef={fileRef} onImport={onImportMock} />
|
||||||
createServers={createServersMock}
|
|
||||||
serversImporter={serversImporterMock}
|
|
||||||
fileRef={fileRef}
|
|
||||||
onImport={onImportMock}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
_selectServer,
|
selectServer,
|
||||||
RESET_SELECTED_SERVER,
|
|
||||||
resetSelectedServer,
|
resetSelectedServer,
|
||||||
|
RESET_SELECTED_SERVER,
|
||||||
SELECT_SERVER,
|
SELECT_SERVER,
|
||||||
} from '../../../src/servers/reducers/selectedServer';
|
} from '../../../src/servers/reducers/selectedServer';
|
||||||
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
|
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
|
||||||
|
@ -29,9 +29,6 @@ describe('selectedServerReducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('selectServer', () => {
|
describe('selectServer', () => {
|
||||||
const ShlinkApiClientMock = {
|
|
||||||
setConfig: sinon.spy(),
|
|
||||||
};
|
|
||||||
const serverId = 'abc123';
|
const serverId = 'abc123';
|
||||||
const selectedServer = {
|
const selectedServer = {
|
||||||
id: serverId,
|
id: serverId,
|
||||||
|
@ -41,7 +38,6 @@ describe('selectedServerReducer', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
ShlinkApiClientMock.setConfig.resetHistory();
|
|
||||||
ServersServiceMock.findServerById.resetHistory();
|
ServersServiceMock.findServerById.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +45,7 @@ describe('selectedServerReducer', () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
_selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(dispatch);
|
selectServer(ServersServiceMock)(serverId)(dispatch);
|
||||||
|
|
||||||
expect(dispatch.callCount).toEqual(expectedDispatchCalls);
|
expect(dispatch.callCount).toEqual(expectedDispatchCalls);
|
||||||
expect(dispatch.firstCall.calledWith({ type: RESET_SHORT_URL_PARAMS })).toEqual(true);
|
expect(dispatch.firstCall.calledWith({ type: RESET_SHORT_URL_PARAMS })).toEqual(true);
|
||||||
|
@ -60,9 +56,8 @@ describe('selectedServerReducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes dependencies', () => {
|
it('invokes dependencies', () => {
|
||||||
_selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(() => {});
|
selectServer(ServersServiceMock)(serverId)(() => {});
|
||||||
|
|
||||||
expect(ShlinkApiClientMock.setConfig.callCount).toEqual(1);
|
|
||||||
expect(ServersServiceMock.findServerById.callCount).toEqual(1);
|
expect(ServersServiceMock.findServerById.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { values } from 'ramda';
|
import { values } from 'ramda';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
_createServer,
|
createServer,
|
||||||
_deleteServer,
|
deleteServer,
|
||||||
_listServers,
|
listServers,
|
||||||
_createServers,
|
createServers,
|
||||||
FETCH_SERVERS,
|
FETCH_SERVERS,
|
||||||
} from '../../../src/servers/reducers/server';
|
} from '../../../src/servers/reducers/server';
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ describe('serverReducer', () => {
|
||||||
|
|
||||||
describe('listServers', () => {
|
describe('listServers', () => {
|
||||||
it('fetches servers and returns them as part of the action', () => {
|
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(result).toEqual({ type: FETCH_SERVERS, servers });
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
||||||
|
@ -51,7 +51,7 @@ describe('serverReducer', () => {
|
||||||
describe('createServer', () => {
|
describe('createServer', () => {
|
||||||
it('adds new server and then fetches servers again', () => {
|
it('adds new server and then fetches servers again', () => {
|
||||||
const serverToCreate = { id: 'abc123' };
|
const serverToCreate = { id: 'abc123' };
|
||||||
const result = _createServer(ServersServiceMock, serverToCreate);
|
const result = createServer(ServersServiceMock)(serverToCreate);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
||||||
|
@ -65,7 +65,7 @@ describe('serverReducer', () => {
|
||||||
describe('deleteServer', () => {
|
describe('deleteServer', () => {
|
||||||
it('deletes a server and then fetches servers again', () => {
|
it('deletes a server and then fetches servers again', () => {
|
||||||
const serverToDelete = { id: 'abc123' };
|
const serverToDelete = { id: 'abc123' };
|
||||||
const result = _deleteServer(ServersServiceMock, serverToDelete);
|
const result = deleteServer(ServersServiceMock)(serverToDelete);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
||||||
|
@ -79,7 +79,7 @@ describe('serverReducer', () => {
|
||||||
describe('createServer', () => {
|
describe('createServer', () => {
|
||||||
it('creates multiple servers and then fetches servers again', () => {
|
it('creates multiple servers and then fetches servers again', () => {
|
||||||
const serversToCreate = values(servers);
|
const serversToCreate = values(servers);
|
||||||
const result = _createServers(ServersServiceMock, serversToCreate);
|
const result = createServers(ServersServiceMock)(serversToCreate);
|
||||||
|
|
||||||
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
expect(result).toEqual({ type: FETCH_SERVERS, servers });
|
||||||
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
expect(ServersServiceMock.listServers.callCount).toEqual(1);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { ServersExporter } from '../../../src/servers/services/ServersExporter';
|
import ServersExporter from '../../../src/servers/services/ServersExporter';
|
||||||
|
|
||||||
describe('ServersExporter', () => {
|
describe('ServersExporter', () => {
|
||||||
const createLinkMock = () => ({
|
const createLinkMock = () => ({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
import ServersImporter from '../../../src/servers/services/ServersImporter';
|
||||||
|
|
||||||
describe('ServersImporter', () => {
|
describe('ServersImporter', () => {
|
||||||
const servers = [{ name: 'foo' }, { name: 'bar' }];
|
const servers = [{ name: 'foo' }, { name: 'bar' }];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { last } from 'ramda';
|
import { last } from 'ramda';
|
||||||
import { ServersService } from '../../../src/servers/services/ServersService';
|
import ServersService from '../../../src/servers/services/ServersService';
|
||||||
|
|
||||||
describe('ServersService', () => {
|
describe('ServersService', () => {
|
||||||
const servers = {
|
const servers = {
|
||||||
|
|
|
@ -3,18 +3,20 @@ import { shallow } from 'enzyme';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { CreateShortUrlComponent as CreateShortUrl } from '../../src/short-urls/CreateShortUrl';
|
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
|
||||||
import TagsSelector from '../../src/tags/helpers/TagsSelector';
|
|
||||||
import DateInput from '../../src/utils/DateInput';
|
import DateInput from '../../src/utils/DateInput';
|
||||||
|
|
||||||
describe('<CreateShortUrl />', () => {
|
describe('<CreateShortUrl />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
const TagsSelector = () => '';
|
||||||
const shortUrlCreationResult = {
|
const shortUrlCreationResult = {
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
const createShortUrl = sinon.spy();
|
const createShortUrl = sinon.spy();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const CreateShortUrl = createShortUrlsCreator(TagsSelector);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
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 SearchField from '../../src/utils/SearchField';
|
||||||
import Tag from '../../src/tags/helpers/Tag';
|
import Tag from '../../src/tags/helpers/Tag';
|
||||||
|
|
||||||
describe('<SearchBar />', () => {
|
describe('<SearchBar />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const listShortUrlsMock = sinon.spy();
|
const listShortUrlsMock = sinon.spy();
|
||||||
|
const SearchBar = searchBarCreator({});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
listShortUrlsMock.resetHistory();
|
listShortUrlsMock.resetHistory();
|
||||||
|
@ -18,13 +19,13 @@ describe('<SearchBar />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a SearchField', () => {
|
it('renders a SearchField', () => {
|
||||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{}} />);
|
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
|
||||||
|
|
||||||
expect(wrapper.find(SearchField)).toHaveLength(1);
|
expect(wrapper.find(SearchField)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders no tags when the list of tags is empty', () => {
|
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);
|
expect(wrapper.find(Tag)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
@ -32,13 +33,13 @@ describe('<SearchBar />', () => {
|
||||||
it('renders the proper amount of tags', () => {
|
it('renders the proper amount of tags', () => {
|
||||||
const tags = [ 'foo', 'bar', 'baz' ];
|
const tags = [ 'foo', 'bar', 'baz' ];
|
||||||
|
|
||||||
wrapper = shallow(<SearchBarComponent shortUrlsListParams={{ tags }} />);
|
wrapper = shallow(<SearchBar shortUrlsListParams={{ tags }} />);
|
||||||
|
|
||||||
expect(wrapper.find(Tag)).toHaveLength(tags.length);
|
expect(wrapper.find(Tag)).toHaveLength(tags.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates short URLs list when search field changes', () => {
|
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);
|
const searchField = wrapper.find(SearchField);
|
||||||
|
|
||||||
expect(listShortUrlsMock.callCount).toEqual(0);
|
expect(listShortUrlsMock.callCount).toEqual(0);
|
||||||
|
@ -48,7 +49,7 @@ describe('<SearchBar />', () => {
|
||||||
|
|
||||||
it('updates short URLs list when a tag is removed', () => {
|
it('updates short URLs list when a tag is removed', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<SearchBarComponent shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />
|
<SearchBar shortUrlsListParams={{ tags: [ 'foo' ] }} listShortUrls={listShortUrlsMock} />
|
||||||
);
|
);
|
||||||
const tag = wrapper.find(Tag).first();
|
const tag = wrapper.find(Tag).first();
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
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 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;
|
let wrapper;
|
||||||
|
const SearchBar = () => '';
|
||||||
|
const ShortUrlsList = () => '';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const params = {
|
const params = {
|
||||||
|
@ -14,6 +14,8 @@ describe('<ShortUrlsList />', () => {
|
||||||
page: '1',
|
page: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ShortUrls = shortUrlsCreator(SearchBar, ShortUrlsList);
|
||||||
|
|
||||||
wrapper = shallow(<ShortUrls match={{ params }} shortUrlsList={{ data: [] }} />);
|
wrapper = shallow(<ShortUrls match={{ params }} shortUrlsList={{ data: [] }} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import * as sinon from 'sinon';
|
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 />', () => {
|
describe('<DeleteShortUrlModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import reducer, {
|
||||||
CREATE_SHORT_URL_ERROR,
|
CREATE_SHORT_URL_ERROR,
|
||||||
CREATE_SHORT_URL,
|
CREATE_SHORT_URL,
|
||||||
RESET_CREATE_SHORT_URL,
|
RESET_CREATE_SHORT_URL,
|
||||||
_createShortUrl,
|
createShortUrl,
|
||||||
resetCreateShortUrl,
|
resetCreateShortUrl,
|
||||||
} from '../../../src/short-urls/reducers/shortUrlCreation';
|
} from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ describe('shortUrlCreationReducer', () => {
|
||||||
createShortUrl: sinon.fake.returns(result),
|
createShortUrl: sinon.fake.returns(result),
|
||||||
});
|
});
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
const getState = () => ({});
|
||||||
|
|
||||||
afterEach(() => dispatch.resetHistory());
|
afterEach(() => dispatch.resetHistory());
|
||||||
|
|
||||||
|
@ -61,9 +62,9 @@ describe('shortUrlCreationReducer', () => {
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
const result = 'foo';
|
const result = 'foo';
|
||||||
const apiClientMock = createApiClientMock(Promise.resolve(result));
|
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);
|
expect(apiClientMock.createShortUrl.callCount).toEqual(1);
|
||||||
|
|
||||||
|
@ -76,10 +77,10 @@ describe('shortUrlCreationReducer', () => {
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
const error = 'Error';
|
const error = 'Error';
|
||||||
const apiClientMock = createApiClientMock(Promise.reject(error));
|
const apiClientMock = createApiClientMock(Promise.reject(error));
|
||||||
const dispatchable = _createShortUrl(apiClientMock, {});
|
const dispatchable = createShortUrl(() => apiClientMock)({});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatchable(dispatch);
|
await dispatchable(dispatch, getState);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Link } from 'react-router-dom';
|
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';
|
import TagBullet from '../../src/tags/helpers/TagBullet';
|
||||||
|
|
||||||
describe('<TagCard />', () => {
|
describe('<TagCard />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const TagCard = createTagCard(() => '', () => '', {});
|
||||||
|
|
||||||
wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />);
|
wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
|
@ -2,16 +2,17 @@ import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { identity, range } from 'ramda';
|
import { identity, range } from 'ramda';
|
||||||
import * as sinon from 'sinon';
|
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 MuttedMessage from '../../src/utils/MuttedMessage';
|
||||||
import TagCard from '../../src/tags/TagCard';
|
|
||||||
import SearchField from '../../src/utils/SearchField';
|
import SearchField from '../../src/utils/SearchField';
|
||||||
|
|
||||||
describe('<TagsList />', () => {
|
describe('<TagsList />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const filterTags = sinon.spy();
|
const filterTags = sinon.spy();
|
||||||
|
const TagCard = () => '';
|
||||||
const createWrapper = (tagsList) => {
|
const createWrapper = (tagsList) => {
|
||||||
const params = { serverId: '1' };
|
const params = { serverId: '1' };
|
||||||
|
const TagsList = createTagsList(TagCard);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} />
|
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} />
|
||||||
|
|
|
@ -5,7 +5,7 @@ import reducer, {
|
||||||
DELETE_TAG,
|
DELETE_TAG,
|
||||||
TAG_DELETED,
|
TAG_DELETED,
|
||||||
tagDeleted,
|
tagDeleted,
|
||||||
_deleteTag,
|
deleteTag,
|
||||||
} from '../../../src/tags/reducers/tagDelete';
|
} from '../../../src/tags/reducers/tagDelete';
|
||||||
|
|
||||||
describe('tagDeleteReducer', () => {
|
describe('tagDeleteReducer', () => {
|
||||||
|
@ -48,6 +48,7 @@ describe('tagDeleteReducer', () => {
|
||||||
deleteTags: sinon.fake.returns(result),
|
deleteTags: sinon.fake.returns(result),
|
||||||
});
|
});
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
const getState = () => ({});
|
||||||
|
|
||||||
afterEach(() => dispatch.resetHistory());
|
afterEach(() => dispatch.resetHistory());
|
||||||
|
|
||||||
|
@ -55,9 +56,9 @@ describe('tagDeleteReducer', () => {
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
const tag = 'foo';
|
const tag = 'foo';
|
||||||
const apiClientMock = createApiClientMock(Promise.resolve());
|
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.callCount).toEqual(1);
|
||||||
expect(apiClientMock.deleteTags.getCall(0).args).toEqual([[ tag ]]);
|
expect(apiClientMock.deleteTags.getCall(0).args).toEqual([[ tag ]]);
|
||||||
|
@ -72,10 +73,10 @@ describe('tagDeleteReducer', () => {
|
||||||
const error = 'Error';
|
const error = 'Error';
|
||||||
const tag = 'foo';
|
const tag = 'foo';
|
||||||
const apiClientMock = createApiClientMock(Promise.reject(error));
|
const apiClientMock = createApiClientMock(Promise.reject(error));
|
||||||
const dispatchable = _deleteTag(apiClientMock, tag);
|
const dispatchable = deleteTag(() => apiClientMock)(tag);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatchable(dispatch);
|
await dispatchable(dispatch, getState);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import reducer, {
|
||||||
EDIT_TAG,
|
EDIT_TAG,
|
||||||
TAG_EDITED,
|
TAG_EDITED,
|
||||||
tagEdited,
|
tagEdited,
|
||||||
_editTag,
|
editTag,
|
||||||
} from '../../../src/tags/reducers/tagEdit';
|
} from '../../../src/tags/reducers/tagEdit';
|
||||||
|
|
||||||
describe('tagEditReducer', () => {
|
describe('tagEditReducer', () => {
|
||||||
|
@ -55,6 +55,7 @@ describe('tagEditReducer', () => {
|
||||||
setColorForKey: sinon.spy(),
|
setColorForKey: sinon.spy(),
|
||||||
};
|
};
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
const getState = () => ({});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
colorGenerator.setColorForKey.resetHistory();
|
colorGenerator.setColorForKey.resetHistory();
|
||||||
|
@ -67,9 +68,9 @@ describe('tagEditReducer', () => {
|
||||||
const newName = 'bar';
|
const newName = 'bar';
|
||||||
const color = '#ff0000';
|
const color = '#ff0000';
|
||||||
const apiClientMock = createApiClientMock(Promise.resolve());
|
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.callCount).toEqual(1);
|
||||||
expect(apiClientMock.editTag.getCall(0).args).toEqual([ oldName, newName ]);
|
expect(apiClientMock.editTag.getCall(0).args).toEqual([ oldName, newName ]);
|
||||||
|
@ -89,10 +90,10 @@ describe('tagEditReducer', () => {
|
||||||
const newName = 'bar';
|
const newName = 'bar';
|
||||||
const color = '#ff0000';
|
const color = '#ff0000';
|
||||||
const apiClientMock = createApiClientMock(Promise.reject(error));
|
const apiClientMock = createApiClientMock(Promise.reject(error));
|
||||||
const dispatchable = _editTag(apiClientMock, colorGenerator, oldName, newName, color);
|
const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await dispatchable(dispatch);
|
await dispatchable(dispatch, getState);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e).toEqual(error);
|
expect(e).toEqual(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { ColorGenerator } from '../../src/utils/ColorGenerator';
|
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
|
||||||
|
|
||||||
describe('ColorGenerator', () => {
|
describe('ColorGenerator', () => {
|
||||||
let colorGenerator;
|
let colorGenerator;
|
|
@ -1,6 +1,6 @@
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { head, last } from 'ramda';
|
import { head, last } from 'ramda';
|
||||||
import { ShlinkApiClient } from '../../src/api/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||||
|
|
||||||
describe('ShlinkApiClient', () => {
|
describe('ShlinkApiClient', () => {
|
||||||
const createAxiosMock = (extraData) => () =>
|
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 { identity } from 'ramda';
|
||||||
import { Card } from 'reactstrap';
|
import { Card } from 'reactstrap';
|
||||||
import * as sinon from 'sinon';
|
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 MutedMessage from '../../src/utils/MuttedMessage';
|
||||||
import GraphCard from '../../src/visits/GraphCard';
|
import GraphCard from '../../src/visits/GraphCard';
|
||||||
import DateInput from '../../src/utils/DateInput';
|
import DateInput from '../../src/utils/DateInput';
|
||||||
|
@ -18,14 +18,17 @@ describe('<ShortUrlVisits />', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = (shortUrlVisits) => {
|
const createComponent = (shortUrlVisits) => {
|
||||||
|
const ShortUrlVisits = createShortUrlVisits({
|
||||||
|
processBrowserStats: statsProcessor,
|
||||||
|
processCountriesStats: statsProcessor,
|
||||||
|
processOsStats: statsProcessor,
|
||||||
|
processReferrersStats: statsProcessor,
|
||||||
|
});
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ShortUrlsVisits
|
<ShortUrlVisits
|
||||||
getShortUrlDetail={identity}
|
getShortUrlDetail={identity}
|
||||||
getShortUrlVisits={getShortUrlVisitsMock}
|
getShortUrlVisits={getShortUrlVisitsMock}
|
||||||
processBrowserStats={statsProcessor}
|
|
||||||
processCountriesStats={statsProcessor}
|
|
||||||
processOsStats={statsProcessor}
|
|
||||||
processReferrersStats={statsProcessor}
|
|
||||||
match={match}
|
match={match}
|
||||||
shortUrlVisits={shortUrlVisits}
|
shortUrlVisits={shortUrlVisits}
|
||||||
shortUrlDetail={{}}
|
shortUrlDetail={{}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
_getShortUrlDetail,
|
getShortUrlDetail,
|
||||||
GET_SHORT_URL_DETAIL_START,
|
GET_SHORT_URL_DETAIL_START,
|
||||||
GET_SHORT_URL_DETAIL_ERROR,
|
GET_SHORT_URL_DETAIL_ERROR,
|
||||||
GET_SHORT_URL_DETAIL,
|
GET_SHORT_URL_DETAIL,
|
||||||
|
@ -50,6 +50,7 @@ describe('shortUrlDetailReducer', () => {
|
||||||
getShortUrl: sinon.fake.returns(returned),
|
getShortUrl: sinon.fake.returns(returned),
|
||||||
});
|
});
|
||||||
const dispatchMock = sinon.spy();
|
const dispatchMock = sinon.spy();
|
||||||
|
const getState = () => ({});
|
||||||
|
|
||||||
beforeEach(() => dispatchMock.resetHistory());
|
beforeEach(() => dispatchMock.resetHistory());
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ describe('shortUrlDetailReducer', () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock);
|
await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||||
|
|
||||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||||
const { type: firstCallType } = firstCallArg;
|
const { type: firstCallType } = firstCallArg;
|
||||||
|
@ -76,7 +77,7 @@ describe('shortUrlDetailReducer', () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl));
|
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedShortUrl));
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
await _getShortUrlDetail(ShlinkApiClient, 'abc123')(dispatchMock);
|
await getShortUrlDetail(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||||
|
|
||||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||||
const { type: firstCallType } = firstCallArg;
|
const { type: firstCallType } = firstCallArg;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
_getShortUrlVisits,
|
getShortUrlVisits,
|
||||||
GET_SHORT_URL_VISITS_START,
|
GET_SHORT_URL_VISITS_START,
|
||||||
GET_SHORT_URL_VISITS_ERROR,
|
GET_SHORT_URL_VISITS_ERROR,
|
||||||
GET_SHORT_URL_VISITS,
|
GET_SHORT_URL_VISITS,
|
||||||
|
@ -50,6 +50,7 @@ describe('shortUrlVisitsReducer', () => {
|
||||||
getShortUrlVisits: sinon.fake.returns(returned),
|
getShortUrlVisits: sinon.fake.returns(returned),
|
||||||
});
|
});
|
||||||
const dispatchMock = sinon.spy();
|
const dispatchMock = sinon.spy();
|
||||||
|
const getState = () => ({});
|
||||||
|
|
||||||
beforeEach(() => dispatchMock.resetHistory());
|
beforeEach(() => dispatchMock.resetHistory());
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ describe('shortUrlVisitsReducer', () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
const ShlinkApiClient = buildApiClientMock(Promise.reject());
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
await _getShortUrlVisits(ShlinkApiClient, 'abc123')(dispatchMock);
|
await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||||
|
|
||||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||||
const { type: firstCallType } = firstCallArg;
|
const { type: firstCallType } = firstCallArg;
|
||||||
|
@ -76,7 +77,7 @@ describe('shortUrlVisitsReducer', () => {
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedVisits));
|
const ShlinkApiClient = buildApiClientMock(Promise.resolve(resolvedVisits));
|
||||||
const expectedDispatchCalls = 2;
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
await _getShortUrlVisits(ShlinkApiClient, 'abc123')(dispatchMock);
|
await getShortUrlVisits(() => ShlinkApiClient)('abc123')(dispatchMock, getState);
|
||||||
|
|
||||||
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
const [ firstCallArg ] = dispatchMock.getCall(0).args;
|
||||||
const { type: firstCallType } = firstCallArg;
|
const { type: firstCallType } = firstCallArg;
|
||||||
|
|
|
@ -1316,6 +1316,10 @@ bootstrap@^4.1.1:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
|
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:
|
boxen@1.3.0, boxen@^1.2.1:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||||
|
|
Loading…
Reference in a new issue