mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 23:07:26 +03:00
Updated styles in javascript to fulfill adidas rules
This commit is contained in:
parent
ed0aa68452
commit
6a016d8e6f
70 changed files with 1250 additions and 759 deletions
53
package.json
53
package.json
|
@ -5,7 +5,8 @@
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "yarn lint:js && yarn lint:css",
|
"lint": "yarn lint:js && yarn lint:css",
|
||||||
"lint:js": "eslint src test scripts config",
|
"lint:js": "eslint src test",
|
||||||
|
"lint:js:fix": "yarn lint:js --fix",
|
||||||
"lint:css": "stylelint src/**/*.scss",
|
"lint:css": "stylelint src/**/*.scss",
|
||||||
"lint:css:fix": "yarn lint:css --fix",
|
"lint:css:fix": "yarn lint:css --fix",
|
||||||
"start": "node scripts/start.js",
|
"start": "node scripts/start.js",
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
"babel-core": "6.26.0",
|
"babel-core": "6.26.0",
|
||||||
"babel-eslint": "7.2.3",
|
"babel-eslint": "7.2.3",
|
||||||
"babel-jest": "20.0.3",
|
"babel-jest": "20.0.3",
|
||||||
"babel-loader": "7.1.2",
|
"babel-loader": "^7.1.2",
|
||||||
"babel-preset-react-app": "^3.1.1",
|
"babel-preset-react-app": "^3.1.1",
|
||||||
"babel-runtime": "6.26.0",
|
"babel-runtime": "6.26.0",
|
||||||
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
||||||
|
@ -61,13 +62,16 @@
|
||||||
"dotenv-expand": "4.2.0",
|
"dotenv-expand": "4.2.0",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
"enzyme-adapter-react-16": "^1.1.1",
|
"enzyme-adapter-react-16": "^1.1.1",
|
||||||
"eslint": "4.10.0",
|
"eslint": "^5.4.0",
|
||||||
"eslint-config-react-app": "^2.1.0",
|
"eslint-config-adidas-babel": "^1.0.1",
|
||||||
|
"eslint-config-adidas-env": "^1.0.1",
|
||||||
|
"eslint-config-adidas-es6": "^1.0.1",
|
||||||
|
"eslint-config-adidas-react": "^1.0.1",
|
||||||
"eslint-loader": "1.9.0",
|
"eslint-loader": "1.9.0",
|
||||||
"eslint-plugin-flowtype": "2.39.1",
|
"eslint-plugin-import": "^2.8.0",
|
||||||
"eslint-plugin-import": "2.8.0",
|
"eslint-plugin-jest": "^21.22.0",
|
||||||
"eslint-plugin-jsx-a11y": "5.1.1",
|
"eslint-plugin-promise": "^3.0.0",
|
||||||
"eslint-plugin-react": "7.4.0",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"file-loader": "1.1.5",
|
"file-loader": "1.1.5",
|
||||||
"fs-extra": "3.0.1",
|
"fs-extra": "3.0.1",
|
||||||
|
@ -138,6 +142,37 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": [
|
||||||
|
"adidas-env/browser",
|
||||||
|
"adidas-env/module",
|
||||||
|
"adidas-es6",
|
||||||
|
"adidas-babel",
|
||||||
|
"adidas-react"
|
||||||
|
],
|
||||||
|
"plugins": ["jest"],
|
||||||
|
"env": {
|
||||||
|
"jest/globals": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"process": true,
|
||||||
|
"setImmediate": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
"template-curly-spacing": ["error", "never"],
|
||||||
|
"no-warning-comments": "off",
|
||||||
|
"no-undefined": "off",
|
||||||
|
"indent": ["error", 2, {
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-curly-spacing": ["error", "never"],
|
||||||
|
"react/jsx-indent-props": ["error", 2],
|
||||||
|
"react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
|
||||||
|
"react/jsx-closing-bracket-location": ["error", "tag-aligned"],
|
||||||
|
"react/no-array-index-key": "off",
|
||||||
|
"react/no-did-update-set-state": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
src/App.js
26
src/App.js
|
@ -6,20 +6,18 @@ import MainHeader from './common/MainHeader';
|
||||||
import MenuLayout from './common/MenuLayout';
|
import MenuLayout from './common/MenuLayout';
|
||||||
import CreateServer from './servers/CreateServer';
|
import CreateServer from './servers/CreateServer';
|
||||||
|
|
||||||
export default class App extends React.Component {
|
export default function App() {
|
||||||
render() {
|
return (
|
||||||
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>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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;
|
||||||
|
|
||||||
export class ShlinkApiClient {
|
export class ShlinkApiClient {
|
||||||
constructor(axios) {
|
constructor(axios) {
|
||||||
|
@ -14,8 +15,6 @@ export class ShlinkApiClient {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the base URL to be used on any request
|
* Sets the base URL to be used on any request
|
||||||
* @param {String} baseUrl
|
|
||||||
* @param {String} apiKey
|
|
||||||
*/
|
*/
|
||||||
setConfig = ({ url, apiKey }) => {
|
setConfig = ({ url, apiKey }) => {
|
||||||
this._baseUrl = `${url}/rest/v${API_VERSION}`;
|
this._baseUrl = `${url}/rest/v${API_VERSION}`;
|
||||||
|
@ -24,45 +23,46 @@ export class ShlinkApiClient {
|
||||||
|
|
||||||
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)
|
||||||
.catch(e => this._handleAuthError(e, this.listShortUrls, [options]));
|
.catch((e) => this._handleAuthError(e, this.listShortUrls, [ options ]));
|
||||||
|
|
||||||
|
createShortUrl = (options) => {
|
||||||
|
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options);
|
||||||
|
|
||||||
createShortUrl = options => {
|
|
||||||
const filteredOptions = reject(value => isEmpty(value) || isNil(value), options);
|
|
||||||
return this._performRequest('/short-codes', 'POST', {}, filteredOptions)
|
return this._performRequest('/short-codes', 'POST', {}, filteredOptions)
|
||||||
.then(resp => resp.data)
|
.then((resp) => resp.data)
|
||||||
.catch(e => this._handleAuthError(e, this.createShortUrl, [filteredOptions]));
|
.catch((e) => this._handleAuthError(e, this.createShortUrl, [ filteredOptions ]));
|
||||||
};
|
};
|
||||||
|
|
||||||
getShortUrlVisits = (shortCode, dates) =>
|
getShortUrlVisits = (shortCode, dates) =>
|
||||||
this._performRequest(`/short-codes/${shortCode}/visits`, 'GET', dates)
|
this._performRequest(`/short-codes/${shortCode}/visits`, 'GET', dates)
|
||||||
.then(resp => resp.data.visits.data)
|
.then((resp) => resp.data.visits.data)
|
||||||
.catch(e => this._handleAuthError(e, this.getShortUrlVisits, [shortCode, dates]));
|
.catch((e) => this._handleAuthError(e, this.getShortUrlVisits, [ shortCode, dates ]));
|
||||||
|
|
||||||
getShortUrl = shortCode =>
|
getShortUrl = (shortCode) =>
|
||||||
this._performRequest(`/short-codes/${shortCode}`, 'GET')
|
this._performRequest(`/short-codes/${shortCode}`, 'GET')
|
||||||
.then(resp => resp.data)
|
.then((resp) => resp.data)
|
||||||
.catch(e => this._handleAuthError(e, this.getShortUrl, [shortCode]));
|
.catch((e) => this._handleAuthError(e, this.getShortUrl, [ shortCode ]));
|
||||||
|
|
||||||
updateShortUrlTags = (shortCode, tags) =>
|
updateShortUrlTags = (shortCode, tags) =>
|
||||||
this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags })
|
this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags })
|
||||||
.then(resp => resp.data.tags)
|
.then((resp) => resp.data.tags)
|
||||||
.catch(e => this._handleAuthError(e, this.updateShortUrlTags, [shortCode, tags]));
|
.catch((e) => this._handleAuthError(e, this.updateShortUrlTags, [ shortCode, tags ]));
|
||||||
|
|
||||||
listTags = () =>
|
listTags = () =>
|
||||||
this._performRequest('/tags', 'GET')
|
this._performRequest('/tags', 'GET')
|
||||||
.then(resp => resp.data.tags.data)
|
.then((resp) => resp.data.tags.data)
|
||||||
.catch(e => this._handleAuthError(e, this.listTags, []));
|
.catch((e) => this._handleAuthError(e, this.listTags, []));
|
||||||
|
|
||||||
deleteTags = tags =>
|
deleteTags = (tags) =>
|
||||||
this._performRequest('/tags', 'DELETE', { tags })
|
this._performRequest('/tags', 'DELETE', { tags })
|
||||||
.then(() => ({ tags }))
|
.then(() => ({ tags }))
|
||||||
.catch(e => this._handleAuthError(e, this.deleteTags, [tags]));
|
.catch((e) => this._handleAuthError(e, this.deleteTags, [ tags ]));
|
||||||
|
|
||||||
editTag = (oldName, newName) =>
|
editTag = (oldName, newName) =>
|
||||||
this._performRequest('/tags', 'PUT', {}, { oldName, newName })
|
this._performRequest('/tags', 'PUT', {}, { oldName, newName })
|
||||||
.then(() => ({ oldName, newName }))
|
.then(() => ({ oldName, newName }))
|
||||||
.catch(e => this._handleAuthError(e, this.editTag, [oldName, newName]));
|
.catch((e) => this._handleAuthError(e, this.editTag, [ oldName, newName ]));
|
||||||
|
|
||||||
_performRequest = async (url, method = 'GET', query = {}, body = {}) => {
|
_performRequest = async (url, method = 'GET', query = {}, body = {}) => {
|
||||||
if (isEmpty(this._token)) {
|
if (isEmpty(this._token)) {
|
||||||
|
@ -72,14 +72,16 @@ export class ShlinkApiClient {
|
||||||
return await this.axios({
|
return await this.axios({
|
||||||
method,
|
method,
|
||||||
url: `${this._baseUrl}${url}`,
|
url: `${this._baseUrl}${url}`,
|
||||||
headers: { 'Authorization': `Bearer ${this._token}` },
|
headers: { Authorization: `Bearer ${this._token}` },
|
||||||
params: query,
|
params: query,
|
||||||
data: body,
|
data: body,
|
||||||
paramsSerializer: params => qs.stringify(params, { arrayFormat: 'brackets' })
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
|
||||||
}).then(resp => {
|
}).then((resp) => {
|
||||||
// Save new token
|
// Save new token
|
||||||
const { authorization = '' } = resp.headers;
|
const { authorization = '' } = resp.headers;
|
||||||
|
|
||||||
this._token = authorization.substr('Bearer '.length);
|
this._token = authorization.substr('Bearer '.length);
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -88,15 +90,17 @@ export class ShlinkApiClient {
|
||||||
const resp = await this.axios({
|
const resp = await this.axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `${this._baseUrl}/authenticate`,
|
url: `${this._baseUrl}/authenticate`,
|
||||||
data: { apiKey: this._apiKey }
|
data: { apiKey: this._apiKey },
|
||||||
});
|
});
|
||||||
|
|
||||||
return resp.data.token;
|
return resp.data.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
_handleAuthError = (e, method, args) => {
|
_handleAuthError = (e, method, args) => {
|
||||||
// If auth failed, reset token to force it to be regenerated, and perform a new request
|
// If auth failed, reset token to force it to be regenerated, and perform a new request
|
||||||
if (e.response.status === 401) {
|
if (e.response.status === STATUS_UNAUTHORIZED) {
|
||||||
this._token = '';
|
this._token = '';
|
||||||
|
|
||||||
return method(...args);
|
return method(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,4 +109,6 @@ export class ShlinkApiClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ShlinkApiClient(axios);
|
const shlinkApiClient = new ShlinkApiClient(axios);
|
||||||
|
|
||||||
|
export default shlinkApiClient;
|
||||||
|
|
|
@ -4,11 +4,11 @@ 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 { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
import DeleteServerButton from '../servers/DeleteServerButton';
|
import DeleteServerButton from '../servers/DeleteServerButton';
|
||||||
import './AsideMenu.scss';
|
import './AsideMenu.scss';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { serverType } from '../servers/prop-types';
|
import { serverType } from '../servers/prop-types';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: '',
|
className: '',
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
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, pick, values } from 'ramda';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { connect } from 'react-redux'
|
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 { resetSelectedServer } from '../servers/reducers/selectedServer'
|
import PropTypes from 'prop-types';
|
||||||
import './Home.scss'
|
import { resetSelectedServer } from '../servers/reducers/selectedServer';
|
||||||
|
import './Home.scss';
|
||||||
|
|
||||||
export class Home extends React.Component {
|
const propTypes = {
|
||||||
|
resetSelectedServer: PropTypes.func,
|
||||||
|
servers: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class HomeComponent extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.resetSelectedServer();
|
this.props.resetSelectedServer();
|
||||||
}
|
}
|
||||||
|
@ -45,4 +51,8 @@ export class Home extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(pick(['servers']), { resetSelectedServer })(Home);
|
HomeComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
const Home = connect(pick([ 'servers' ]), { resetSelectedServer })(HomeComponent);
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
|
|
@ -2,18 +2,23 @@ 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, withRouter } 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 PropTypes from 'prop-types';
|
||||||
import ServersDropdown from '../servers/ServersDropdown';
|
import ServersDropdown from '../servers/ServersDropdown';
|
||||||
import './MainHeader.scss';
|
import './MainHeader.scss';
|
||||||
import shlinkLogo from './shlink-logo-white.png';
|
import shlinkLogo from './shlink-logo-white.png';
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
export class MainHeader extends React.Component {
|
const propTypes = {
|
||||||
|
location: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MainHeaderComponent extends React.Component {
|
||||||
state = { isOpen: false };
|
state = { isOpen: false };
|
||||||
toggle = () => {
|
handleToggle = () => {
|
||||||
this.setState(({ isOpen }) => ({
|
this.setState(({ isOpen }) => ({
|
||||||
isOpen: !isOpen
|
isOpen: !isOpen,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,10 +38,10 @@ export class MainHeader extends React.Component {
|
||||||
return (
|
return (
|
||||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
||||||
<NavbarBrand tag={Link} to="/">
|
<NavbarBrand tag={Link} to="/">
|
||||||
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo"/> Shlink
|
<img src={shlinkLogo} alt="Shlink" className="main-header__brand-logo" /> Shlink
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
|
||||||
<NavbarToggler onClick={this.toggle}>
|
<NavbarToggler onClick={this.handleToggle}>
|
||||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
||||||
</NavbarToggler>
|
</NavbarToggler>
|
||||||
|
|
||||||
|
@ -48,7 +53,7 @@ export class MainHeader extends React.Component {
|
||||||
to={createServerPath}
|
to={createServerPath}
|
||||||
active={location.pathname === createServerPath}
|
active={location.pathname === createServerPath}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={plusIcon}/> Add server
|
<FontAwesomeIcon icon={plusIcon} /> Add server
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<ServersDropdown />
|
<ServersDropdown />
|
||||||
|
@ -59,4 +64,8 @@ export class MainHeader extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(MainHeader);
|
MainHeaderComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
const MainHeader = withRouter(MainHeaderComponent);
|
||||||
|
|
||||||
|
export default MainHeader;
|
||||||
|
|
|
@ -2,26 +2,38 @@ import React from 'react';
|
||||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
import { selectServer } from '../servers/reducers/selectedServer';
|
|
||||||
import CreateShortUrl from '../short-urls/CreateShortUrl';
|
|
||||||
import ShortUrls from '../short-urls/ShortUrls';
|
|
||||||
import ShortUrlsVisits from '../short-urls/ShortUrlVisits';
|
|
||||||
import AsideMenu from './AsideMenu';
|
|
||||||
import { pick } from 'ramda';
|
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 ShortUrlsVisits from '../short-urls/ShortUrlVisits';
|
||||||
|
import { selectServer } from '../servers/reducers/selectedServer';
|
||||||
|
import CreateShortUrl from '../short-urls/CreateShortUrl';
|
||||||
|
import ShortUrls from '../short-urls/ShortUrls';
|
||||||
import './MenuLayout.scss';
|
import './MenuLayout.scss';
|
||||||
import TagsList from '../tags/TagsList';
|
import TagsList from '../tags/TagsList';
|
||||||
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import AsideMenu from './AsideMenu';
|
||||||
|
|
||||||
export class MenuLayout extends React.Component {
|
const propTypes = {
|
||||||
|
match: PropTypes.object,
|
||||||
|
selectServer: PropTypes.func,
|
||||||
|
location: PropTypes.object,
|
||||||
|
selectedServer: serverType,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MenuLayoutComponent extends React.Component {
|
||||||
state = { showSideBar: false };
|
state = { showSideBar: false };
|
||||||
|
|
||||||
// FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered
|
// FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered
|
||||||
|
/* eslint react/no-deprecated: "off" */
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { serverId } = this.props.match.params;
|
const { match, selectServer } = this.props;
|
||||||
this.props.selectServer(serverId);
|
const { params: { serverId } } = match;
|
||||||
|
|
||||||
|
selectServer(serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -44,14 +56,14 @@ export class MenuLayout extends React.Component {
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={burgerIcon}
|
icon={burgerIcon}
|
||||||
className={burgerClasses}
|
className={burgerClasses}
|
||||||
onClick={() => this.setState({ showSideBar: !this.state.showSideBar })}
|
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Swipeable
|
<Swipeable
|
||||||
delta={40}
|
delta={40}
|
||||||
|
className="menu-layout__swipeable"
|
||||||
onSwipedLeft={() => this.setState({ showSideBar: false })}
|
onSwipedLeft={() => this.setState({ showSideBar: false })}
|
||||||
onSwipedRight={() => this.setState({ showSideBar: true })}
|
onSwipedRight={() => this.setState({ showSideBar: true })}
|
||||||
className="menu-layout__swipeable"
|
|
||||||
>
|
>
|
||||||
<div className="row menu-layout__swipeable-inner">
|
<div className="row menu-layout__swipeable-inner">
|
||||||
<AsideMenu
|
<AsideMenu
|
||||||
|
@ -93,7 +105,11 @@ export class MenuLayout extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
MenuLayoutComponent.propTypes = propTypes;
|
||||||
connect(pick(['selectedServer', 'shortUrlsListParams']), { selectServer }),
|
|
||||||
|
const MenuLayout = compose(
|
||||||
|
connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }),
|
||||||
withRouter
|
withRouter
|
||||||
)(MenuLayout);
|
)(MenuLayoutComponent);
|
||||||
|
|
||||||
|
export default MenuLayout;
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router-dom'
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export class ScrollToTop extends React.Component {
|
const propTypes = {
|
||||||
|
location: PropTypes.object,
|
||||||
|
window: PropTypes.shape({
|
||||||
|
scrollTo: PropTypes.func,
|
||||||
|
}),
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
const defaultProps = {
|
||||||
|
window,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ScrollToTopComponent extends React.Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { location, window } = this.props;
|
const { location, window } = this.props;
|
||||||
|
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +27,9 @@ export class ScrollToTop extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollToTop.defaultProps = {
|
ScrollToTopComponent.defaultProps = defaultProps;
|
||||||
window
|
ScrollToTopComponent.propTypes = propTypes;
|
||||||
};
|
|
||||||
|
|
||||||
export default withRouter(ScrollToTop);
|
const ScrollToTop = withRouter(ScrollToTopComponent);
|
||||||
|
|
||||||
|
export default ScrollToTop;
|
||||||
|
|
|
@ -5,15 +5,13 @@ import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
import { applyMiddleware, compose, createStore } from 'redux';
|
||||||
import ReduxThunk from 'redux-thunk';
|
import ReduxThunk from 'redux-thunk';
|
||||||
|
|
||||||
import '../node_modules/react-datepicker/dist/react-datepicker.css';
|
|
||||||
import './common/react-tagsinput.scss';
|
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import ScrollToTop from './common/ScrollToTop'
|
import ScrollToTop from './common/ScrollToTop';
|
||||||
import reducers from './reducers';
|
import reducers from './reducers';
|
||||||
import registerServiceWorker from './registerServiceWorker';
|
import registerServiceWorker from './registerServiceWorker';
|
||||||
|
import '../node_modules/react-datepicker/dist/react-datepicker.css';
|
||||||
|
import './common/react-tagsinput.scss';
|
||||||
|
|
||||||
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
const composeEnhancers = process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||||
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import serversReducer from '../servers/reducers/server';
|
import serversReducer from '../servers/reducers/server';
|
||||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
||||||
|
|
|
@ -8,10 +8,14 @@
|
||||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||||
// This link also includes instructions on opting out of this behavior.
|
// This link also includes instructions on opting out of this behavior.
|
||||||
|
|
||||||
|
/* eslint no-console: "off" */
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
const isLocalhost = Boolean(
|
||||||
window.location.hostname === 'localhost' ||
|
window.location.hostname === 'localhost' ||
|
||||||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
// [::1] is the IPv6 localhost address.
|
||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === '[::1]' ||
|
||||||
|
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
window.location.hostname.match(
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
@ -22,6 +26,7 @@ export default function register() {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||||
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
@ -55,9 +60,10 @@ export default function register() {
|
||||||
function registerValidSW(swUrl) {
|
function registerValidSW(swUrl) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing;
|
||||||
|
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === 'installed') {
|
if (installingWorker.state === 'installed') {
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
|
@ -76,7 +82,7 @@ function registerValidSW(swUrl) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error('Error during service worker registration:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -84,14 +90,16 @@ function registerValidSW(swUrl) {
|
||||||
function checkValidServiceWorker(swUrl) {
|
function checkValidServiceWorker(swUrl) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl)
|
fetch(swUrl)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
|
const NOT_FOUND_STATUS = 404;
|
||||||
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
if (
|
if (
|
||||||
response.status === 404 ||
|
response.status === NOT_FOUND_STATUS ||
|
||||||
response.headers.get('content-type').indexOf('javascript') === -1
|
response.headers.get('content-type').indexOf('javascript') === -1
|
||||||
) {
|
) {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
@ -110,7 +118,7 @@ function checkValidServiceWorker(swUrl) {
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { assoc, dissoc, pick, pipe } from 'ramda';
|
import { assoc, dissoc, pick, pipe } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createServer } from './reducers/server';
|
|
||||||
import { resetSelectedServer } from './reducers/selectedServer';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { resetSelectedServer } from './reducers/selectedServer';
|
||||||
|
import { createServer } from './reducers/server';
|
||||||
import './CreateServer.scss';
|
import './CreateServer.scss';
|
||||||
import ImportServersBtn from './helpers/ImportServersBtn';
|
import ImportServersBtn from './helpers/ImportServersBtn';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
|
const SHOW_IMPORT_MSG_TIME = 4000;
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
createServer: PropTypes.func,
|
createServer: PropTypes.func,
|
||||||
history: PropTypes.shape({
|
history: PropTypes.shape({
|
||||||
|
@ -16,7 +17,7 @@ const propTypes = {
|
||||||
resetSelectedServer: PropTypes.func,
|
resetSelectedServer: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CreateServer extends React.Component {
|
export class CreateServerComponent extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
|
@ -24,7 +25,7 @@ export class CreateServer extends React.Component {
|
||||||
serversImported: false,
|
serversImported: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
submit = e => {
|
handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { createServer, history: { push } } = this.props;
|
const { createServer, history: { push } } = this.props;
|
||||||
|
@ -34,7 +35,7 @@ export class CreateServer extends React.Component {
|
||||||
)(this.state);
|
)(this.state);
|
||||||
|
|
||||||
createServer(server);
|
createServer(server);
|
||||||
push(`/server/${server.id}/list-short-urls/1`)
|
push(`/server/${server.id}/list-short-urls/1`);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -42,7 +43,7 @@ export class CreateServer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const renderInputGroup = (id, placeholder, type = 'text') =>
|
const renderInputGroup = (id, placeholder, type = 'text') => (
|
||||||
<div className="form-group row">
|
<div className="form-group row">
|
||||||
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
|
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
|
||||||
{placeholder}:
|
{placeholder}:
|
||||||
|
@ -54,24 +55,27 @@ export class CreateServer extends React.Component {
|
||||||
id={id}
|
id={id}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={this.state[id]}
|
value={this.state[id]}
|
||||||
onChange={e => this.setState({ [id]: e.target.value })}
|
|
||||||
required
|
required
|
||||||
|
onChange={(e) => this.setState({ [id]: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="create-server">
|
<div className="create-server">
|
||||||
<form onSubmit={this.submit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
{renderInputGroup('name', 'Name')}
|
{renderInputGroup('name', 'Name')}
|
||||||
{renderInputGroup('url', 'URL', 'url')}
|
{renderInputGroup('url', 'URL', 'url')}
|
||||||
{renderInputGroup('apiKey', 'API key')}
|
{renderInputGroup('apiKey', 'API key')}
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<ImportServersBtn onImport={() => {
|
<ImportServersBtn
|
||||||
this.setState({ serversImported: true });
|
onImport={() => {
|
||||||
setTimeout(() => this.setState({ serversImported: false }), 4000);
|
this.setState({ serversImported: true });
|
||||||
}} />
|
setTimeout(() => this.setState({ serversImported: false }), SHOW_IMPORT_MSG_TIME);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<button className="btn btn-outline-primary">Create server</button>
|
<button className="btn btn-outline-primary">Create server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -90,9 +94,11 @@ export class CreateServer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateServer.propTypes = propTypes;
|
CreateServerComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(
|
const CreateServer = connect(
|
||||||
pick(['selectedServer']),
|
pick([ 'selectedServer' ]),
|
||||||
{createServer, resetSelectedServer }
|
{ createServer, resetSelectedServer }
|
||||||
)(CreateServer);
|
)(CreateServerComponent);
|
||||||
|
|
||||||
|
export default CreateServer;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle';
|
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 DeleteServerModal from './DeleteServerModal';
|
import DeleteServerModal from './DeleteServerModal';
|
||||||
import { serverType } from './prop-types';
|
import { serverType } from './prop-types';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
server: serverType,
|
server: serverType,
|
||||||
|
@ -20,8 +20,8 @@ export default class DeleteServerButton extends React.Component {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<span
|
<span
|
||||||
className={className}
|
className={className}
|
||||||
onClick={() => this.setState({ isModalOpen: true })}
|
|
||||||
key="deleteServerBtn"
|
key="deleteServerBtn"
|
||||||
|
onClick={() => this.setState({ isModalOpen: true })}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={deleteIcon} />
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
<span className="aside-menu__item-text">Delete this server</span>
|
<span className="aside-menu__item-text">Delete this server</span>
|
||||||
|
@ -29,7 +29,7 @@ export default class DeleteServerButton extends React.Component {
|
||||||
|
|
||||||
<DeleteServerModal
|
<DeleteServerModal
|
||||||
isOpen={this.state.isModalOpen}
|
isOpen={this.state.isModalOpen}
|
||||||
toggle={() => this.setState({ isModalOpen: !this.state.isModalOpen })}
|
toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))}
|
||||||
server={server}
|
server={server}
|
||||||
key="deleteServerModal"
|
key="deleteServerModal"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,9 +11,13 @@ const propTypes = {
|
||||||
toggle: PropTypes.func.isRequired,
|
toggle: PropTypes.func.isRequired,
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
server: serverType,
|
server: serverType,
|
||||||
|
deleteServer: PropTypes.func,
|
||||||
|
history: PropTypes.shape({
|
||||||
|
push: PropTypes.func,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) => {
|
export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServer, history }) => {
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
deleteServer(server);
|
deleteServer(server);
|
||||||
toggle();
|
toggle();
|
||||||
|
@ -38,9 +42,11 @@ export const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, histor
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DeleteServerModal.propTypes = propTypes;
|
DeleteServerModalComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default compose(
|
const DeleteServerModal = compose(
|
||||||
withRouter,
|
withRouter,
|
||||||
connect(null, { deleteServer })
|
connect(null, { deleteServer })
|
||||||
)(DeleteServerModal);
|
)(DeleteServerModalComponent);
|
||||||
|
|
||||||
|
export default DeleteServerModal;
|
||||||
|
|
|
@ -3,21 +3,31 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
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 { listServers } from './reducers/server';
|
|
||||||
import { selectServer } from '../servers/reducers/selectedServer';
|
import { selectServer } from '../servers/reducers/selectedServer';
|
||||||
import serversExporter from '../servers/services/ServersExporter';
|
import serversExporter from '../servers/services/ServersExporter';
|
||||||
|
import { listServers } from './reducers/server';
|
||||||
|
import { serverType } from './prop-types';
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
serversExporter,
|
serversExporter,
|
||||||
};
|
};
|
||||||
|
const propTypes = {
|
||||||
|
servers: PropTypes.object,
|
||||||
|
serversExporter: PropTypes.shape({
|
||||||
|
exportServers: PropTypes.func,
|
||||||
|
}),
|
||||||
|
selectedServer: serverType,
|
||||||
|
selectServer: PropTypes.func,
|
||||||
|
listServers: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
export class ServersDropdown extends React.Component {
|
export class ServersDropdownComponent extends React.Component {
|
||||||
renderServers = () => {
|
renderServers = () => {
|
||||||
const { servers, selectedServer, selectServer, serversExporter } = this.props;
|
const { servers, selectedServer, selectServer, serversExporter } = 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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -28,15 +38,17 @@ export class ServersDropdown extends React.Component {
|
||||||
tag={Link}
|
tag={Link}
|
||||||
to={`/server/${id}/list-short-urls/1`}
|
to={`/server/${id}/list-short-urls/1`}
|
||||||
active={selectedServer && selectedServer.id === id}
|
active={selectedServer && selectedServer.id === id}
|
||||||
onClick={() => selectServer(id)} // FIXME This should be implicit
|
|
||||||
|
// FIXME This should be implicit
|
||||||
|
onClick={() => selectServer(id)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={serversExporter.exportServers}
|
|
||||||
className="servers-dropdown__export-item"
|
className="servers-dropdown__export-item"
|
||||||
|
onClick={() => serversExporter.exportServers()}
|
||||||
>
|
>
|
||||||
Export servers
|
Export servers
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
@ -58,9 +70,12 @@ export class ServersDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ServersDropdown.defaultProps = defaultProps;
|
ServersDropdownComponent.defaultProps = defaultProps;
|
||||||
|
ServersDropdownComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(
|
const ServersDropdown = connect(
|
||||||
pick(['servers', 'selectedServer']),
|
pick([ 'servers', 'selectedServer' ]),
|
||||||
{ listServers, selectServer }
|
{ listServers, selectServer }
|
||||||
)(ServersDropdown);
|
)(ServersDropdownComponent);
|
||||||
|
|
||||||
|
export default ServersDropdown;
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import serversImporter, { serversImporterType } from '../services/ServersImporter';
|
|
||||||
import { createServers } from '../reducers/server';
|
|
||||||
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';
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
serversImporter,
|
serversImporter,
|
||||||
onImport: () => {},
|
onImport: () => ({}),
|
||||||
};
|
};
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
onImport: PropTypes.func,
|
onImport: PropTypes.func,
|
||||||
serversImporter: serversImporterType,
|
serversImporter: serversImporterType,
|
||||||
createServers: PropTypes.func,
|
createServers: PropTypes.func,
|
||||||
fileRef: PropTypes.oneOfType([PropTypes.object, PropTypes.node]),
|
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ImportServersBtn extends React.Component {
|
export class ImportServersBtnComponent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.fileRef = props.fileRef || React.createRef();
|
this.fileRef = props.fileRef || React.createRef();
|
||||||
|
@ -26,9 +26,9 @@ export class ImportServersBtn extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { serversImporter: { importServersFromFile }, onImport, createServers } = this.props;
|
const { serversImporter: { importServersFromFile }, 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)))
|
||||||
.then(createServers)
|
.then(createServers)
|
||||||
.then(onImport);
|
.then(onImport);
|
||||||
|
|
||||||
|
@ -37,28 +37,30 @@ export class ImportServersBtn extends React.Component {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-secondary mr-2"
|
className="btn btn-outline-secondary mr-2"
|
||||||
onClick={() => this.fileRef.current.click()}
|
|
||||||
id="importBtn"
|
id="importBtn"
|
||||||
|
onClick={() => this.fileRef.current.click()}
|
||||||
>
|
>
|
||||||
Import from file
|
Import from file
|
||||||
</button>
|
</button>
|
||||||
<UncontrolledTooltip placement="top" target="importBtn">
|
<UncontrolledTooltip placement="top" target="importBtn">
|
||||||
You can create servers by importing a CSV file with columns "name", "apiKey" and "url"
|
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
onChange={onChange}
|
|
||||||
accept="text/csv"
|
accept="text/csv"
|
||||||
className="create-server__csv-select"
|
className="create-server__csv-select"
|
||||||
ref={this.fileRef}
|
ref={this.fileRef}
|
||||||
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportServersBtn.defaultProps = defaultProps;
|
ImportServersBtnComponent.defaultProps = defaultProps;
|
||||||
ImportServersBtn.propTypes = propTypes;
|
ImportServersBtnComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(null, { createServers })(ImportServersBtn);
|
const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent);
|
||||||
|
|
||||||
|
export default ImportServersBtn;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import serversService from '../../servers/services/ServersService';
|
|
||||||
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'
|
|
||||||
import { curry } from 'ramda';
|
import { curry } from 'ramda';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
import serversService from '../../servers/services/ServersService';
|
||||||
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
|
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
|
||||||
|
|
||||||
const defaultState = null;
|
const defaultState = null;
|
||||||
|
|
||||||
|
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
|
||||||
|
|
||||||
|
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
|
||||||
|
|
||||||
export default function reducer(state = defaultState, action) {
|
export default function reducer(state = defaultState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SELECT_SERVER:
|
case SELECT_SERVER:
|
||||||
|
@ -21,15 +22,17 @@ 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 = (shlinkApiClient, serversService, serverId) => (dispatch) => {
|
||||||
dispatch(resetShortUrlParams());
|
dispatch(resetShortUrlParams());
|
||||||
|
|
||||||
const selectedServer = serversService.findServerById(serverId);
|
const selectedServer = serversService.findServerById(serverId);
|
||||||
ShlinkApiClient.setConfig(selectedServer);
|
|
||||||
|
shlinkApiClient.setConfig(selectedServer);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SELECT_SERVER,
|
type: SELECT_SERVER,
|
||||||
selectedServer
|
selectedServer,
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
export const selectServer = curry(_selectServer)(ShlinkApiClient, serversService);
|
|
||||||
|
export const selectServer = curry(_selectServer)(shlinkApiClient, serversService);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import serversService from '../services/ServersService';
|
|
||||||
import { curry } from 'ramda';
|
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';
|
||||||
|
|
||||||
|
@ -12,26 +12,33 @@ 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 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 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 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);
|
export const createServers = curry(_createServers)(serversService);
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import serversService from './ServersService';
|
|
||||||
import { dissoc, head, keys, values } from 'ramda';
|
import { dissoc, head, keys, values } from 'ramda';
|
||||||
import csvjson from 'csvjson';
|
import csvjson from 'csvjson';
|
||||||
|
import serversService from './ServersService';
|
||||||
|
|
||||||
const saveCsv = (window, csv) => {
|
const saveCsv = (window, csv) => {
|
||||||
const { navigator, document } = window;
|
const { navigator, document } = window;
|
||||||
const filename = 'shlink-servers.csv';
|
const filename = 'shlink-servers.csv';
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([ csv ], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
|
||||||
// IE10 and IE11
|
// IE10 and IE11
|
||||||
if (navigator.msSaveBlob) {
|
if (navigator.msSaveBlob) {
|
||||||
navigator.msSaveBlob(blob, filename);
|
navigator.msSaveBlob(blob, filename);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modern browsers
|
// Modern browsers
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
link.setAttribute('href', url);
|
link.setAttribute('href', url);
|
||||||
link.setAttribute('download', filename);
|
link.setAttribute('download', filename);
|
||||||
link.style.visibility = 'hidden';
|
link.style.visibility = 'hidden';
|
||||||
|
@ -36,15 +38,18 @@ export class ServersExporter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const csv = this.csvjson.toCSV(servers, {
|
const csv = this.csvjson.toCSV(servers, {
|
||||||
headers: keys(head(servers)).join(',')
|
headers: keys(head(servers)).join(','),
|
||||||
});
|
});
|
||||||
|
|
||||||
saveCsv(this.window, csv);
|
saveCsv(this.window, csv);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// FIXME Handle error
|
// FIXME Handle error
|
||||||
|
/* eslint no-console: "off" */
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverExporter = new ServersExporter(serversService, global.window, csvjson);
|
const serverExporter = new ServersExporter(serversService, global.window, csvjson);
|
||||||
|
|
||||||
export default serverExporter;
|
export default serverExporter;
|
||||||
|
|
|
@ -16,8 +16,9 @@ export class ServersImporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
return new Promise(resolve => {
|
|
||||||
reader.addEventListener('loadend', e => {
|
return new Promise((resolve) => {
|
||||||
|
reader.addEventListener('loadend', (e) => {
|
||||||
const content = e.target.result;
|
const content = e.target.result;
|
||||||
const servers = this.csvjson.toObject(content);
|
const servers = this.csvjson.toObject(content);
|
||||||
|
|
||||||
|
@ -29,4 +30,5 @@ export class ServersImporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
const serversImporter = new ServersImporter(csvjson);
|
const serversImporter = new ServersImporter(csvjson);
|
||||||
|
|
||||||
export default serversImporter;
|
export default serversImporter;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Storage from '../../utils/Storage';
|
|
||||||
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';
|
||||||
|
|
||||||
|
@ -10,25 +10,27 @@ export class ServersService {
|
||||||
|
|
||||||
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
|
listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {};
|
||||||
|
|
||||||
findServerById = serverId => this.listServers()[serverId];
|
findServerById = (serverId) => this.listServers()[serverId];
|
||||||
|
|
||||||
createServer = server => this.createServers([server]);
|
createServer = (server) => this.createServers([ server ]);
|
||||||
|
|
||||||
createServers = servers => {
|
createServers = (servers) => {
|
||||||
const allServers = reduce(
|
const allServers = reduce(
|
||||||
(serversObj, server) => assoc(server.id, server, serversObj),
|
(serversObj, server) => assoc(server.id, server, serversObj),
|
||||||
this.listServers(),
|
this.listServers(),
|
||||||
servers
|
servers
|
||||||
);
|
);
|
||||||
|
|
||||||
this.storage.set(SERVERS_STORAGE_KEY, allServers);
|
this.storage.set(SERVERS_STORAGE_KEY, allServers);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteServer = server =>
|
deleteServer = (server) =>
|
||||||
this.storage.set(
|
this.storage.set(
|
||||||
SERVERS_STORAGE_KEY,
|
SERVERS_STORAGE_KEY,
|
||||||
dissoc(server.id, this.listServers())
|
dissoc(server.id, this.listServers())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serversService = new ServersService(Storage);
|
const serversService = new ServersService(storage);
|
||||||
|
|
||||||
export default serversService;
|
export default serversService;
|
||||||
|
|
|
@ -6,11 +6,11 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Collapse } from 'reactstrap';
|
import { Collapse } from 'reactstrap';
|
||||||
import DateInput from '../common/DateInput';
|
import DateInput from '../common/DateInput';
|
||||||
|
import TagsSelector from '../utils/TagsSelector';
|
||||||
import CreateShortUrlResult from './helpers/CreateShortUrlResult';
|
import CreateShortUrlResult from './helpers/CreateShortUrlResult';
|
||||||
import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult';
|
import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult';
|
||||||
import TagsSelector from '../utils/TagsSelector';
|
|
||||||
|
|
||||||
export class CreateShortUrl extends React.Component {
|
export class CreateShortUrlComponent extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
longUrl: '',
|
longUrl: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -18,35 +18,37 @@ export class CreateShortUrl extends React.Component {
|
||||||
validSince: undefined,
|
validSince: undefined,
|
||||||
validUntil: undefined,
|
validUntil: undefined,
|
||||||
maxVisits: undefined,
|
maxVisits: undefined,
|
||||||
moreOptionsVisible: false
|
moreOptionsVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props;
|
const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props;
|
||||||
|
|
||||||
const changeTags = tags => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) });
|
const changeTags = (tags) => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) });
|
||||||
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) =>
|
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => (
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className="form-control"
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={this.state[id]}
|
value={this.state[id]}
|
||||||
onChange={e => this.setState({ [id]: e.target.value })}
|
onChange={(e) => this.setState({ [id]: e.target.value })}
|
||||||
{...props}
|
{...props}
|
||||||
/>;
|
/>
|
||||||
const createDateInput = (id, placeholder, props = {}) =>
|
);
|
||||||
|
const createDateInput = (id, placeholder, props = {}) => (
|
||||||
<DateInput
|
<DateInput
|
||||||
selected={this.state[id]}
|
selected={this.state[id]}
|
||||||
placeholderText={placeholder}
|
placeholderText={placeholder}
|
||||||
onChange={date => this.setState({ [id]: date })}
|
|
||||||
isClearable
|
isClearable
|
||||||
|
onChange={(date) => this.setState({ [id]: date })}
|
||||||
{...props}
|
{...props}
|
||||||
/>;
|
/>
|
||||||
const formatDate = date => isNil(date) ? date : date.format();
|
);
|
||||||
const save = e => {
|
const formatDate = (date) => isNil(date) ? date : date.format();
|
||||||
|
const save = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createShortUrl(pipe(
|
createShortUrl(pipe(
|
||||||
dissoc('moreOptionsVisible'), // Remove moreOptionsVisible property
|
dissoc('moreOptionsVisible'),
|
||||||
assoc('validSince', formatDate(this.state.validSince)),
|
assoc('validSince', formatDate(this.state.validSince)),
|
||||||
assoc('validUntil', formatDate(this.state.validUntil))
|
assoc('validUntil', formatDate(this.state.validUntil))
|
||||||
)(this.state));
|
)(this.state));
|
||||||
|
@ -62,7 +64,7 @@ export class CreateShortUrl extends React.Component {
|
||||||
placeholder="Insert the URL to be shortened"
|
placeholder="Insert the URL to be shortened"
|
||||||
required
|
required
|
||||||
value={this.state.longUrl}
|
value={this.state.longUrl}
|
||||||
onChange={e => this.setState({ longUrl: e.target.value })}
|
onChange={(e) => this.setState({ longUrl: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -95,7 +97,7 @@ export class CreateShortUrl extends React.Component {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-secondary create-short-url__btn"
|
className="btn btn-outline-secondary create-short-url__btn"
|
||||||
onClick={() => this.setState({ moreOptionsVisible: !this.state.moreOptionsVisible })}
|
onClick={() => this.setState(({ moreOptionsVisible }) => ({ moreOptionsVisible: !moreOptionsVisible }))}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
|
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
|
||||||
|
|
||||||
|
@ -116,7 +118,9 @@ export class CreateShortUrl extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(pick(['shortUrlCreationResult']), {
|
const CreateShortUrl = connect(pick([ 'shortUrlCreationResult' ]), {
|
||||||
createShortUrl,
|
createShortUrl,
|
||||||
resetCreateShortUrl
|
resetCreateShortUrl,
|
||||||
})(CreateShortUrl);
|
})(CreateShortUrlComponent);
|
||||||
|
|
||||||
|
export default CreateShortUrl;
|
||||||
|
|
|
@ -1,52 +1,61 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default class Paginator extends React.Component {
|
const propTypes = {
|
||||||
render() {
|
serverId: PropTypes.string.isRequired,
|
||||||
const { paginator = {}, serverId } = this.props;
|
paginator: PropTypes.shape({
|
||||||
const { currentPage, pagesCount = 0 } = paginator;
|
currentPage: PropTypes.number,
|
||||||
if (pagesCount <= 1) {
|
pagesCount: PropTypes.number,
|
||||||
return null;
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Paginator({ paginator = {}, serverId }) {
|
||||||
|
const { currentPage, pagesCount = 0 } = paginator;
|
||||||
|
|
||||||
|
if (pagesCount <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPages = () => {
|
||||||
|
const pages = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= pagesCount; i++) {
|
||||||
|
pages.push(
|
||||||
|
<PaginationItem key={i} active={currentPage === i}>
|
||||||
|
<PaginationLink
|
||||||
|
tag={Link}
|
||||||
|
to={`/server/${serverId}/list-short-urls/${i}`}
|
||||||
|
>
|
||||||
|
{i}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderPages = () => {
|
return pages;
|
||||||
const pages = [];
|
};
|
||||||
|
|
||||||
for (let i = 1; i <= pagesCount; i++) {
|
return (
|
||||||
pages.push(
|
<Pagination listClassName="flex-wrap">
|
||||||
<PaginationItem key={i} active={currentPage === i}>
|
<PaginationItem disabled={currentPage === 1}>
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
tag={Link}
|
previous
|
||||||
to={`/server/${serverId}/list-short-urls/${i}`}
|
tag={Link}
|
||||||
>
|
to={`/server/${serverId}/list-short-urls/${currentPage - 1}`}
|
||||||
{i}
|
/>
|
||||||
</PaginationLink>
|
</PaginationItem>
|
||||||
</PaginationItem>
|
{renderPages()}
|
||||||
);
|
<PaginationItem disabled={currentPage >= pagesCount}>
|
||||||
}
|
<PaginationLink
|
||||||
|
next
|
||||||
return pages;
|
tag={Link}
|
||||||
};
|
to={`/server/${serverId}/list-short-urls/${currentPage + 1}`}
|
||||||
|
/>
|
||||||
return (
|
</PaginationItem>
|
||||||
<Pagination listClassName="flex-wrap">
|
</Pagination>
|
||||||
<PaginationItem disabled={currentPage === 1}>
|
);
|
||||||
<PaginationLink
|
|
||||||
previous
|
|
||||||
tag={Link}
|
|
||||||
to={`/server/${serverId}/list-short-urls/${currentPage - 1}`}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
{renderPages()}
|
|
||||||
<PaginationItem disabled={currentPage >= pagesCount}>
|
|
||||||
<PaginationLink
|
|
||||||
next
|
|
||||||
tag={Link}
|
|
||||||
to={`/server/${serverId}/list-short-urls/${currentPage + 1}`}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</Pagination>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Paginator.propTypes = propTypes;
|
||||||
|
|
|
@ -2,26 +2,34 @@ 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 { connect } from 'react-redux';
|
||||||
import Tag from '../utils/Tag';
|
|
||||||
import { listShortUrls } from './reducers/shortUrlsList';
|
|
||||||
import { isEmpty, pick } from 'ramda';
|
import { isEmpty, pick } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Tag from '../utils/Tag';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
|
import { listShortUrls } from './reducers/shortUrlsList';
|
||||||
import './SearchBar.scss';
|
import './SearchBar.scss';
|
||||||
|
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||||
|
|
||||||
export function SearchBar({ listShortUrls, shortUrlsListParams }) {
|
const propTypes = {
|
||||||
|
listShortUrls: PropTypes.func,
|
||||||
|
shortUrlsListParams: shortUrlsListParamsType,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) {
|
||||||
const selectedTags = shortUrlsListParams.tags || [];
|
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}
|
key={tag}
|
||||||
text={tag}
|
text={tag}
|
||||||
|
@ -29,7 +37,7 @@ export function SearchBar({ listShortUrls, shortUrlsListParams }) {
|
||||||
onClose={() => listShortUrls(
|
onClose={() => listShortUrls(
|
||||||
{
|
{
|
||||||
...shortUrlsListParams,
|
...shortUrlsListParams,
|
||||||
tags: selectedTags.filter(selectedTag => selectedTag !== tag)
|
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -40,4 +48,8 @@ export function SearchBar({ listShortUrls, shortUrlsListParams }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(pick(['shortUrlsListParams']), { listShortUrls })(SearchBar);
|
SearchBarComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent);
|
||||||
|
|
||||||
|
export default SearchBar;
|
||||||
|
|
|
@ -1,22 +1,35 @@
|
||||||
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, pick } from 'ramda';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { Doughnut, HorizontalBar } from 'react-chartjs-2'
|
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||||
import Moment from 'react-moment'
|
import Moment from 'react-moment';
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux';
|
||||||
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap'
|
import { Card, CardBody, CardHeader, UncontrolledTooltip } from 'reactstrap';
|
||||||
import DateInput from '../common/DateInput'
|
import PropTypes from 'prop-types';
|
||||||
|
import DateInput from '../common/DateInput';
|
||||||
import {
|
import {
|
||||||
processOsStats,
|
processOsStats,
|
||||||
processBrowserStats,
|
processBrowserStats,
|
||||||
processCountriesStats,
|
processCountriesStats,
|
||||||
processReferrersStats,
|
processReferrersStats,
|
||||||
} from '../visits/services/VisitsParser'
|
} from '../visits/services/VisitsParser';
|
||||||
import { getShortUrlVisits } from './reducers/shortUrlVisits'
|
|
||||||
import './ShortUrlVisits.scss'
|
|
||||||
import MutedMessage from '../utils/MuttedMessage';
|
import MutedMessage from '../utils/MuttedMessage';
|
||||||
|
import ExternalLink from '../utils/ExternalLink';
|
||||||
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import { getShortUrlVisits, shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||||
|
import './ShortUrlVisits.scss';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
processOsStats: PropTypes.func,
|
||||||
|
processBrowserStats: PropTypes.func,
|
||||||
|
processCountriesStats: PropTypes.func,
|
||||||
|
processReferrersStats: PropTypes.func,
|
||||||
|
match: PropTypes.object,
|
||||||
|
getShortUrlVisits: PropTypes.func,
|
||||||
|
selectedServer: serverType,
|
||||||
|
shortUrlVisits: shortUrlVisitsType,
|
||||||
|
};
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
processOsStats,
|
processOsStats,
|
||||||
processBrowserStats,
|
processBrowserStats,
|
||||||
|
@ -24,14 +37,15 @@ const defaultProps = {
|
||||||
processReferrersStats,
|
processReferrersStats,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShortUrlsVisits extends React.Component {
|
export class ShortUrlsVisitsComponent extends React.Component {
|
||||||
state = { startDate: undefined, endDate: undefined };
|
state = { startDate: undefined, endDate: undefined };
|
||||||
loadVisits = () => {
|
loadVisits = () => {
|
||||||
const { match: { params } } = this.props;
|
const { match: { params }, getShortUrlVisits } = this.props;
|
||||||
this.props.getShortUrlVisits(params.shortCode, mapObjIndexed(
|
|
||||||
value => value && value.format ? value.format('YYYY-MM-DD') : value,
|
getShortUrlVisits(params.shortCode, mapObjIndexed(
|
||||||
|
(value) => value && value.format ? value.format('YYYY-MM-DD') : value,
|
||||||
this.state
|
this.state
|
||||||
))
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -46,7 +60,7 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
processBrowserStats,
|
processBrowserStats,
|
||||||
processCountriesStats,
|
processCountriesStats,
|
||||||
processReferrersStats,
|
processReferrersStats,
|
||||||
shortUrlVisits: { visits, loading, error, shortUrl }
|
shortUrlVisits: { visits, loading, error, shortUrl },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const serverUrl = selectedServer ? selectedServer.url : '';
|
const serverUrl = selectedServer ? selectedServer.url : '';
|
||||||
const shortLink = `${serverUrl}/${params.shortCode}`;
|
const shortLink = `${serverUrl}/${params.shortCode}`;
|
||||||
|
@ -63,31 +77,42 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
'#46BFBD',
|
'#46BFBD',
|
||||||
'#FDB45C',
|
'#FDB45C',
|
||||||
'#949FB1',
|
'#949FB1',
|
||||||
'#4D5360'
|
'#4D5360',
|
||||||
],
|
],
|
||||||
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
|
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
const renderGraphCard = (title, stats, isBarChart, label) =>
|
const renderGraphCard = (title, stats, isBarChart, label) => (
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<Card className="mt-4">
|
<Card className="mt-4">
|
||||||
<CardHeader>{title}</CardHeader>
|
<CardHeader>{title}</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{!isBarChart && <Doughnut data={generateGraphData(stats, label || title, isBarChart)} options={{
|
{!isBarChart && (
|
||||||
legend: {
|
<Doughnut
|
||||||
position: 'right'
|
data={generateGraphData(stats, label || title, isBarChart)}
|
||||||
}
|
options={{
|
||||||
}} />}
|
legend: {
|
||||||
{isBarChart && <HorizontalBar data={generateGraphData(stats, label || title, isBarChart)} options={{
|
position: 'right',
|
||||||
legend: {
|
},
|
||||||
display: false
|
}}
|
||||||
}
|
/>
|
||||||
}} />}
|
)}
|
||||||
|
{isBarChart && (
|
||||||
|
<HorizontalBar
|
||||||
|
data={generateGraphData(stats, label || title, isBarChart)}
|
||||||
|
options={{
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <MutedMessage><FontAwesomeIcon icon={preloader} spin /> Loading...</MutedMessage>;
|
return <MutedMessage><FontAwesomeIcon icon={preloader} spin /> Loading...</MutedMessage>;
|
||||||
|
@ -115,13 +140,14 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCreated = () =>
|
const renderCreated = () => (
|
||||||
<span>
|
<span>
|
||||||
<b id="created"><Moment fromNow>{shortUrl.dateCreated}</Moment></b>
|
<b id="created"><Moment fromNow>{shortUrl.dateCreated}</Moment></b>
|
||||||
<UncontrolledTooltip placement="bottom" target="created">
|
<UncontrolledTooltip placement="bottom" target="created">
|
||||||
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
</span>;
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shlink-container">
|
<div className="shlink-container">
|
||||||
|
@ -133,20 +159,22 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
shortUrl.visitsCount &&
|
shortUrl.visitsCount &&
|
||||||
<span className="badge badge-main float-right">Visits: {shortUrl.visitsCount}</span>
|
<span className="badge badge-main float-right">Visits: {shortUrl.visitsCount}</span>
|
||||||
}
|
}
|
||||||
Visit stats for <a target="_blank" href={shortLink}>{shortLink}</a>
|
Visit stats for <ExternalLink href={shortLink}>{shortLink}</ExternalLink>
|
||||||
</h2>
|
</h2>
|
||||||
<hr />
|
<hr />
|
||||||
{shortUrl.dateCreated && <div>
|
{shortUrl.dateCreated && (
|
||||||
Created:
|
<div>
|
||||||
|
Created:
|
||||||
{loading && <small>Loading...</small>}
|
|
||||||
{!loading && renderCreated()}
|
{loading && <small>Loading...</small>}
|
||||||
</div>}
|
{!loading && renderCreated()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
Long URL:
|
Long URL:
|
||||||
|
|
||||||
{loading && <small>Loading...</small>}
|
{loading && <small>Loading...</small>}
|
||||||
{!loading && <a target="_blank" href={shortUrl.longUrl}>{shortUrl.longUrl}</a>}
|
{!loading && <ExternalLink href={shortUrl.longUrl}>{shortUrl.longUrl}</ExternalLink>}
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -159,7 +187,7 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
selected={this.state.startDate}
|
selected={this.state.startDate}
|
||||||
placeholderText="Since"
|
placeholderText="Since"
|
||||||
isClearable
|
isClearable
|
||||||
onChange={date => this.setState({ startDate: date }, () => this.loadVisits())}
|
onChange={(date) => this.setState({ startDate: date }, () => this.loadVisits())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-xl-3 col-lg-4 col-md-6">
|
<div className="col-xl-3 col-lg-4 col-md-6">
|
||||||
|
@ -167,8 +195,8 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
selected={this.state.endDate}
|
selected={this.state.endDate}
|
||||||
placeholderText="Until"
|
placeholderText="Until"
|
||||||
isClearable
|
isClearable
|
||||||
onChange={date => this.setState({ endDate: date }, () => this.loadVisits())}
|
|
||||||
className="short-url-visits__date-input"
|
className="short-url-visits__date-input"
|
||||||
|
onChange={(date) => this.setState({ endDate: date }, () => this.loadVisits())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -182,9 +210,12 @@ export class ShortUrlsVisits extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ShortUrlsVisits.defaultProps = defaultProps;
|
ShortUrlsVisitsComponent.propTypes = propTypes;
|
||||||
|
ShortUrlsVisitsComponent.defaultProps = defaultProps;
|
||||||
|
|
||||||
export default connect(
|
const ShortUrlsVisits = connect(
|
||||||
pick(['selectedServer', 'shortUrlVisits']),
|
pick([ 'selectedServer', 'shortUrlVisits' ]),
|
||||||
{ getShortUrlVisits }
|
{ getShortUrlVisits }
|
||||||
)(ShortUrlsVisits);
|
)(ShortUrlsVisitsComponent);
|
||||||
|
|
||||||
|
export default ShortUrlsVisits;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { assoc } from 'ramda';
|
||||||
import Paginator from './Paginator';
|
import Paginator from './Paginator';
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
import ShortUrlsList from './ShortUrlsList';
|
import ShortUrlsList from './ShortUrlsList';
|
||||||
import { assoc } from 'ramda';
|
|
||||||
|
|
||||||
export function ShortUrls(props) {
|
export function ShortUrlsComponent(props) {
|
||||||
const { match: { params } } = props;
|
const { match: { params } } = props;
|
||||||
|
|
||||||
// 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 = `${params.serverId}_${params.page}`;
|
||||||
|
|
||||||
|
@ -19,4 +20,8 @@ export function ShortUrls(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(state => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList))(ShortUrls);
|
const ShortUrls = connect(
|
||||||
|
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
|
||||||
|
)(ShortUrlsComponent);
|
||||||
|
|
||||||
|
export default ShortUrls;
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
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, pick, toPairs, keys, values } from 'ramda'
|
import { head, isEmpty, pick, toPairs, keys, values } from 'ramda';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux';
|
||||||
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'
|
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
||||||
import { ShortUrlsRow } from './helpers/ShortUrlsRow'
|
|
||||||
import { listShortUrls } from './reducers/shortUrlsList'
|
|
||||||
import './ShortUrlsList.scss'
|
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import { ShortUrlsRow } from './helpers/ShortUrlsRow';
|
||||||
|
import { listShortUrls, shortUrlType } from './reducers/shortUrlsList';
|
||||||
|
import './ShortUrlsList.scss';
|
||||||
|
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
|
||||||
|
|
||||||
const SORTABLE_FIELDS = {
|
const SORTABLE_FIELDS = {
|
||||||
dateCreated: 'Created at',
|
dateCreated: 'Created at',
|
||||||
|
@ -17,29 +20,43 @@ const SORTABLE_FIELDS = {
|
||||||
visits: 'Visits',
|
visits: 'Visits',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShortUrlsList extends React.Component {
|
const propTypes = {
|
||||||
refreshList = extraParams => {
|
listShortUrls: PropTypes.func,
|
||||||
|
shortUrlsListParams: shortUrlsListParamsType,
|
||||||
|
match: PropTypes.object,
|
||||||
|
location: PropTypes.object,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
shortUrlsList: PropTypes.arrayOf(shortUrlType),
|
||||||
|
selectedServer: serverType,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ShortUrlsListComponent extends React.Component {
|
||||||
|
refreshList = (extraParams) => {
|
||||||
const { listShortUrls, shortUrlsListParams } = this.props;
|
const { listShortUrls, shortUrlsListParams } = this.props;
|
||||||
|
|
||||||
listShortUrls({
|
listShortUrls({
|
||||||
...shortUrlsListParams,
|
...shortUrlsListParams,
|
||||||
...extraParams
|
...extraParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
determineOrderDir = field => {
|
determineOrderDir = (field) => {
|
||||||
if (this.state.orderField !== field) {
|
if (this.state.orderField !== field) {
|
||||||
return 'ASC';
|
return 'ASC';
|
||||||
}
|
}
|
||||||
|
|
||||||
const newOrderMap = {
|
const newOrderMap = {
|
||||||
'ASC': 'DESC',
|
ASC: 'DESC',
|
||||||
'DESC': undefined,
|
DESC: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.state.orderDir ? newOrderMap[this.state.orderDir] : 'ASC';
|
return this.state.orderDir ? newOrderMap[this.state.orderDir] : 'ASC';
|
||||||
};
|
};
|
||||||
orderBy = field => {
|
orderBy = (field) => {
|
||||||
const newOrderDir = this.determineOrderDir(field);
|
const newOrderDir = this.determineOrderDir(field);
|
||||||
|
|
||||||
this.setState({ orderField: newOrderDir !== undefined ? field : undefined, orderDir: newOrderDir });
|
this.setState({ orderField: newOrderDir !== undefined ? field : undefined, orderDir: newOrderDir });
|
||||||
this.refreshList({ orderBy: { [field]: newOrderDir } })
|
this.refreshList({ orderBy: { [field]: newOrderDir } });
|
||||||
};
|
};
|
||||||
renderOrderIcon = (field, className = 'short-urls-list__header-icon') => {
|
renderOrderIcon = (field, className = 'short-urls-list__header-icon') => {
|
||||||
if (this.state.orderField !== field) {
|
if (this.state.orderField !== field) {
|
||||||
|
@ -58,21 +75,23 @@ export class ShortUrlsList extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { orderBy } = props.shortUrlsListParams;
|
const { orderBy } = props.shortUrlsListParams;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
orderField: orderBy ? head(keys(orderBy)) : undefined,
|
orderField: orderBy ? head(keys(orderBy)) : undefined,
|
||||||
orderDir: orderBy ? head(values(orderBy)) : undefined,
|
orderDir: orderBy ? head(values(orderBy)) : undefined,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params }, location } = this.props;
|
const { match: { params }, location } = this.props;
|
||||||
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
||||||
|
|
||||||
this.refreshList({ page: params.page, tags: query.tag ? [query.tag] : [] });
|
this.refreshList({ page: params.page, tags: query.tag ? [ query.tag ] : [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderShortUrls() {
|
renderShortUrls() {
|
||||||
const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props;
|
const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -85,11 +104,11 @@ export class ShortUrlsList extends React.Component {
|
||||||
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
|
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! loading && isEmpty(shortUrlsList)) {
|
if (!loading && isEmpty(shortUrlsList)) {
|
||||||
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
|
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shortUrlsList.map(shortUrl => (
|
return shortUrlsList.map((shortUrl) => (
|
||||||
<ShortUrlsRow
|
<ShortUrlsRow
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
|
@ -108,11 +127,12 @@ export class ShortUrlsList extends React.Component {
|
||||||
Order by
|
Order by
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu className="short-urls-list__order-dropdown">
|
<DropdownMenu className="short-urls-list__order-dropdown">
|
||||||
{toPairs(SORTABLE_FIELDS).map(([key, value]) =>
|
{toPairs(SORTABLE_FIELDS).map(([ key, value ]) => (
|
||||||
<DropdownItem key={key} active={this.state.orderField === key} onClick={() => this.orderBy(key)}>
|
<DropdownItem key={key} active={this.state.orderField === key} onClick={() => this.orderBy(key)}>
|
||||||
{value}
|
{value}
|
||||||
{this.renderOrderIcon(key, 'short-urls-list__header-icon--mobile')}
|
{this.renderOrderIcon(key, 'short-urls-list__header-icon--mobile')}
|
||||||
</DropdownItem>)}
|
</DropdownItem>
|
||||||
|
))}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,4 +186,11 @@ export class ShortUrlsList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(pick(['selectedServer', 'shortUrlsListParams']), { listShortUrls })(ShortUrlsList);
|
ShortUrlsListComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
const ShortUrlsList = connect(
|
||||||
|
pick([ 'selectedServer', 'shortUrlsListParams' ]),
|
||||||
|
{ listShortUrls }
|
||||||
|
)(ShortUrlsListComponent);
|
||||||
|
|
||||||
|
export default ShortUrlsList;
|
||||||
|
|
|
@ -3,8 +3,18 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import { isNil } from 'ramda';
|
import { isNil } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import './CreateShortUrlResult.scss'
|
|
||||||
import { Card, CardBody, Tooltip } from 'reactstrap';
|
import { Card, CardBody, Tooltip } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { createShortUrlResultType } from '../reducers/shortUrlCreationResult';
|
||||||
|
import './CreateShortUrlResult.scss';
|
||||||
|
|
||||||
|
const TIME_TO_SHOW_COPY_TOOLTIP = 2000;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
resetCreateShortUrl: PropTypes.func,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
result: createShortUrlResultType,
|
||||||
|
};
|
||||||
|
|
||||||
export default class CreateShortUrlResult extends React.Component {
|
export default class CreateShortUrlResult extends React.Component {
|
||||||
state = { showCopyTooltip: false };
|
state = { showCopyTooltip: false };
|
||||||
|
@ -30,7 +40,7 @@ export default class CreateShortUrlResult extends React.Component {
|
||||||
const { shortUrl } = result;
|
const { shortUrl } = result;
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
this.setState({ showCopyTooltip: true });
|
this.setState({ showCopyTooltip: true });
|
||||||
setTimeout(() => this.setState({ showCopyTooltip: false }), 2000);
|
setTimeout(() => this.setState({ showCopyTooltip: false }), TIME_TO_SHOW_COPY_TOOLTIP);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -39,8 +49,12 @@ export default class CreateShortUrlResult extends React.Component {
|
||||||
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
<b>Great!</b> The short URL is <b>{shortUrl}</b>
|
||||||
|
|
||||||
<CopyToClipboard text={shortUrl} onCopy={onCopy}>
|
<CopyToClipboard text={shortUrl} onCopy={onCopy}>
|
||||||
<button className="btn btn-light btn-sm create-short-url-result__copy-btn" id="copyBtn" type="button">
|
<button
|
||||||
<FontAwesomeIcon icon={copyIcon}/> Copy
|
className="btn btn-light btn-sm create-short-url-result__copy-btn"
|
||||||
|
id="copyBtn"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={copyIcon} /> Copy
|
||||||
</button>
|
</button>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
|
||||||
|
@ -51,4 +65,6 @@ export default class CreateShortUrlResult extends React.Component {
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
CreateShortUrlResult.propTypes = propTypes;
|
||||||
|
|
|
@ -1,30 +1,33 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import TagsSelector from '../../utils/TagsSelector';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { pick } from 'ramda';
|
||||||
|
import TagsSelector from '../../utils/TagsSelector';
|
||||||
import {
|
import {
|
||||||
editShortUrlTags,
|
editShortUrlTags,
|
||||||
resetShortUrlsTags,
|
resetShortUrlsTags,
|
||||||
shortUrlTagsType,
|
shortUrlTagsType,
|
||||||
shortUrlTagsEdited
|
shortUrlTagsEdited,
|
||||||
} from '../reducers/shortUrlTags';
|
} from '../reducers/shortUrlTags';
|
||||||
import { pick } from 'ramda';
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
toggle: PropTypes.func.isRequired,
|
toggle: PropTypes.func.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
shortUrl: PropTypes.shape({
|
shortUrl: shortUrlType.isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
shortCode: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
shortUrlTags: shortUrlTagsType,
|
shortUrlTags: shortUrlTagsType,
|
||||||
|
editShortUrlTags: PropTypes.func,
|
||||||
|
shortUrlTagsEdited: PropTypes.func,
|
||||||
|
resetShortUrlsTags: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class EditTagsModal extends React.Component {
|
export class EditTagsModalComponent extends React.Component {
|
||||||
saveTags = () => {
|
saveTags = () => {
|
||||||
const { editShortUrlTags, shortUrl, toggle } = this.props;
|
const { editShortUrlTags, shortUrl, toggle } = this.props;
|
||||||
|
|
||||||
editShortUrlTags(shortUrl.shortCode, this.state.tags)
|
editShortUrlTags(shortUrl.shortCode, this.state.tags)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.tagsSaved = true;
|
this.tagsSaved = true;
|
||||||
|
@ -39,11 +42,13 @@ export class EditTagsModal extends React.Component {
|
||||||
|
|
||||||
const { shortUrlTagsEdited, shortUrl } = this.props;
|
const { shortUrlTagsEdited, shortUrl } = this.props;
|
||||||
const { tags } = this.state;
|
const { tags } = this.state;
|
||||||
|
|
||||||
shortUrlTagsEdited(shortUrl.shortCode, tags);
|
shortUrlTagsEdited(shortUrl.shortCode, tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { resetShortUrlsTags } = this.props;
|
const { resetShortUrlsTags } = this.props;
|
||||||
|
|
||||||
resetShortUrlsTags();
|
resetShortUrlsTags();
|
||||||
this.tagsSaved = false;
|
this.tagsSaved = false;
|
||||||
}
|
}
|
||||||
|
@ -57,12 +62,12 @@ export class EditTagsModal extends React.Component {
|
||||||
const { isOpen, toggle, url, shortUrlTags } = this.props;
|
const { isOpen, toggle, url, shortUrlTags } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.refreshShortUrls}>
|
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={() => this.refreshShortUrls}>
|
||||||
<ModalHeader toggle={toggle}>
|
<ModalHeader toggle={toggle}>
|
||||||
Edit tags for <a target="_blank" href={url}>{url}</a>
|
Edit tags for <ExternalLink href={url}>{url}</ExternalLink>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<TagsSelector tags={this.state.tags} onChange={tags => this.setState({ tags })} />
|
<TagsSelector tags={this.state.tags} onChange={(tags) => this.setState({ tags })} />
|
||||||
{shortUrlTags.error && (
|
{shortUrlTags.error && (
|
||||||
<div className="p-2 mt-2 bg-danger text-white text-center">
|
<div className="p-2 mt-2 bg-danger text-white text-center">
|
||||||
Something went wrong while saving the tags :(
|
Something went wrong while saving the tags :(
|
||||||
|
@ -74,8 +79,8 @@ export class EditTagsModal extends React.Component {
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this.saveTags}
|
|
||||||
disabled={shortUrlTags.saving}
|
disabled={shortUrlTags.saving}
|
||||||
|
onClick={() => this.saveTags}
|
||||||
>
|
>
|
||||||
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
|
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
|
||||||
</button>
|
</button>
|
||||||
|
@ -85,9 +90,11 @@ export class EditTagsModal extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditTagsModal.propTypes = propTypes;
|
EditTagsModalComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(
|
const EditTagsModal = connect(
|
||||||
pick(['shortUrlTags']),
|
pick([ 'shortUrlTags' ]),
|
||||||
{ editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited }
|
{ editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited }
|
||||||
)(EditTagsModal);
|
)(EditTagsModalComponent);
|
||||||
|
|
||||||
|
export default EditTagsModal;
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import './PreviewModal.scss';
|
import './PreviewModal.scss';
|
||||||
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
|
|
||||||
export default function PreviewModal ({ url, toggle, isOpen }) {
|
const propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
toggle: PropTypes.func,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PreviewModal({ url, toggle, isOpen }) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} size="lg">
|
<Modal isOpen={isOpen} toggle={toggle} size="lg">
|
||||||
<ModalHeader toggle={toggle}>Preview for <a target="_blank" href={url}>{url}</a></ModalHeader>
|
<ModalHeader toggle={toggle}>
|
||||||
|
Preview for <ExternalLink href={url}>{url}</ExternalLink>
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="preview-modal__loader">Loading...</p>
|
<p className="preview-modal__loader">Loading...</p>
|
||||||
|
@ -15,3 +25,5 @@ export default function PreviewModal ({ url, toggle, isOpen }) {
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PreviewModal.propTypes = propTypes;
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
|
|
||||||
export default function QrCodeModal ({ url, toggle, isOpen }) {
|
const propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
toggle: PropTypes.func,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function QrCodeModal({ url, toggle, isOpen }) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
<ModalHeader toggle={toggle}>QR code for <a target="_blank" href={url}>{url}</a></ModalHeader>
|
<ModalHeader toggle={toggle}>
|
||||||
|
QR code for <ExternalLink href={url}>{url}</ExternalLink>
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<img src={`${url}/qr-code`} className="qr-code-modal__img" alt="QR code" />
|
<img src={`${url}/qr-code`} className="qr-code-modal__img" alt="QR code" />
|
||||||
|
@ -14,3 +24,5 @@ export default function QrCodeModal ({ url, toggle, isOpen }) {
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QrCodeModal.propTypes = propTypes;
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
import { isEmpty } from 'ramda';
|
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 Tag from '../../utils/Tag';
|
import Tag from '../../utils/Tag';
|
||||||
import './ShortUrlsRow.scss';
|
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
|
||||||
|
import { serverType } from '../../servers/prop-types';
|
||||||
|
import ExternalLink from '../../utils/ExternalLink';
|
||||||
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import { ShortUrlsRowMenu } from './ShortUrlsRowMenu';
|
import { ShortUrlsRowMenu } from './ShortUrlsRowMenu';
|
||||||
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
|
const COPIED_MSG_TIME = 2000;
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
refreshList: PropTypes.func,
|
||||||
|
shortUrlsListParams: shortUrlsListParamsType,
|
||||||
|
selectedServer: serverType,
|
||||||
|
shortUrl: shortUrlType,
|
||||||
|
};
|
||||||
|
|
||||||
export class ShortUrlsRow extends React.Component {
|
export class ShortUrlsRow extends React.Component {
|
||||||
state = { copiedToClipboard: false };
|
state = { copiedToClipboard: false };
|
||||||
|
@ -15,7 +29,8 @@ export class ShortUrlsRow extends React.Component {
|
||||||
|
|
||||||
const { refreshList, shortUrlsListParams } = this.props;
|
const { refreshList, shortUrlsListParams } = this.props;
|
||||||
const selectedTags = shortUrlsListParams.tags || [];
|
const selectedTags = shortUrlsListParams.tags || [];
|
||||||
return tags.map(tag => (
|
|
||||||
|
return tags.map((tag) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag}
|
key={tag}
|
||||||
text={tag}
|
text={tag}
|
||||||
|
@ -34,10 +49,10 @@ export class ShortUrlsRow extends React.Component {
|
||||||
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell" data-th="Short URL: ">
|
<td className="short-urls-row__cell" data-th="Short URL: ">
|
||||||
<a href={completeShortUrl} target="_blank">{completeShortUrl}</a>
|
<ExternalLink href={completeShortUrl}>{completeShortUrl}</ExternalLink>
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
|
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
|
||||||
<a href={shortUrl.originalUrl} target="_blank">{shortUrl.originalUrl}</a>
|
<ExternalLink href={shortUrl.originalUrl}>{shortUrl.originalUrl}</ExternalLink>
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
|
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
|
||||||
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">{shortUrl.visitsCount}</td>
|
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">{shortUrl.visitsCount}</td>
|
||||||
|
@ -54,7 +69,7 @@ export class ShortUrlsRow extends React.Component {
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
onCopyToClipboard={() => {
|
onCopyToClipboard={() => {
|
||||||
this.setState({ copiedToClipboard: true });
|
this.setState({ copiedToClipboard: true });
|
||||||
setTimeout(() => this.setState({ copiedToClipboard: false }), 2000);
|
setTimeout(() => this.setState({ copiedToClipboard: false }), COPIED_MSG_TIME);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
@ -62,3 +77,5 @@ export class ShortUrlsRow extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShortUrlsRow.propTypes = propTypes;
|
||||||
|
|
|
@ -9,11 +9,21 @@ import React from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { serverType } from '../../servers/prop-types';
|
||||||
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import PreviewModal from './PreviewModal';
|
import PreviewModal from './PreviewModal';
|
||||||
import QrCodeModal from './QrCodeModal';
|
import QrCodeModal from './QrCodeModal';
|
||||||
import './ShortUrlsRowMenu.scss';
|
import './ShortUrlsRowMenu.scss';
|
||||||
import EditTagsModal from './EditTagsModal';
|
import EditTagsModal from './EditTagsModal';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
completeShortUrl: PropTypes.string,
|
||||||
|
onCopyToClipboard: PropTypes.func,
|
||||||
|
selectedServer: serverType,
|
||||||
|
shortUrl: shortUrlType,
|
||||||
|
};
|
||||||
|
|
||||||
export class ShortUrlsRowMenu extends React.Component {
|
export class ShortUrlsRowMenu extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
@ -21,26 +31,26 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
isPreviewOpen: false,
|
isPreviewOpen: false,
|
||||||
isTagsModalOpen: false,
|
isTagsModalOpen: false,
|
||||||
};
|
};
|
||||||
toggle = () => this.setState({ isOpen: !this.state.isOpen });
|
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
|
const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
|
||||||
const serverId = selectedServer ? selectedServer.id : '';
|
const serverId = selectedServer ? selectedServer.id : '';
|
||||||
const toggleQrCode = () => this.setState({isQrModalOpen: !this.state.isQrModalOpen});
|
const toggleQrCode = () => this.setState(({ isQrModalOpen }) => ({ isQrModalOpen: !isQrModalOpen }));
|
||||||
const togglePreview = () => this.setState({isPreviewOpen: !this.state.isPreviewOpen});
|
const togglePreview = () => this.setState(({ isPreviewOpen }) => ({ isPreviewOpen: !isPreviewOpen }));
|
||||||
const toggleTags = () => this.setState({isTagsModalOpen: !this.state.isTagsModalOpen});
|
const toggleTags = () => this.setState(({ isTagsModalOpen }) => ({ isTagsModalOpen: !isTagsModalOpen }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonDropdown toggle={this.toggle} isOpen={this.state.isOpen} direction="left">
|
<ButtonDropdown toggle={this.toggle} isOpen={this.state.isOpen} direction="left">
|
||||||
<DropdownToggle size="sm" caret className="short-urls-row-menu__dropdown-toggle btn-outline-secondary">
|
<DropdownToggle size="sm" caret className="short-urls-row-menu__dropdown-toggle btn-outline-secondary">
|
||||||
<FontAwesomeIcon icon={menuIcon}/>
|
<FontAwesomeIcon icon={menuIcon} />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortUrl.shortCode}/visits`}>
|
<DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortUrl.shortCode}/visits`}>
|
||||||
<FontAwesomeIcon icon={pieChartIcon}/> Visit Stats
|
<FontAwesomeIcon icon={pieChartIcon} /> Visit Stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem onClick={toggleTags}>
|
<DropdownItem onClick={toggleTags}>
|
||||||
<FontAwesomeIcon icon={tagsIcon}/> Edit tags
|
<FontAwesomeIcon icon={tagsIcon} /> Edit tags
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<EditTagsModal
|
<EditTagsModal
|
||||||
url={completeShortUrl}
|
url={completeShortUrl}
|
||||||
|
@ -49,10 +59,10 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
toggle={toggleTags}
|
toggle={toggleTags}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownItem divider/>
|
<DropdownItem divider />
|
||||||
|
|
||||||
<DropdownItem onClick={togglePreview}>
|
<DropdownItem onClick={togglePreview}>
|
||||||
<FontAwesomeIcon icon={pictureIcon}/> Preview
|
<FontAwesomeIcon icon={pictureIcon} /> Preview
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<PreviewModal
|
<PreviewModal
|
||||||
url={completeShortUrl}
|
url={completeShortUrl}
|
||||||
|
@ -61,7 +71,7 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownItem onClick={toggleQrCode}>
|
<DropdownItem onClick={toggleQrCode}>
|
||||||
<FontAwesomeIcon icon={qrIcon}/> QR code
|
<FontAwesomeIcon icon={qrIcon} /> QR code
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<QrCodeModal
|
<QrCodeModal
|
||||||
url={completeShortUrl}
|
url={completeShortUrl}
|
||||||
|
@ -69,11 +79,11 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
toggle={toggleQrCode}
|
toggle={toggleQrCode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownItem divider/>
|
<DropdownItem divider />
|
||||||
|
|
||||||
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
|
<CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
|
||||||
<DropdownItem>
|
<DropdownItem>
|
||||||
<FontAwesomeIcon icon={copyIcon}/> Copy to clipboard
|
<FontAwesomeIcon icon={copyIcon} /> Copy to clipboard
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
@ -81,3 +91,5 @@ export class ShortUrlsRowMenu extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShortUrlsRowMenu.propTypes = propTypes;
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { curry } from 'ramda';
|
import { curry } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
|
||||||
const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
const CREATE_SHORT_URL_START = 'shlink/createShortUrl/CREATE_SHORT_URL_START';
|
||||||
const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
|
const CREATE_SHORT_URL_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR';
|
||||||
const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
|
const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
|
||||||
const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
|
const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
|
||||||
|
|
||||||
|
export const createShortUrlResultType = {
|
||||||
|
result: PropTypes.shape({
|
||||||
|
shortUrl: PropTypes.string,
|
||||||
|
}),
|
||||||
|
saving: PropTypes.bool,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
result: null,
|
result: null,
|
||||||
saving: false,
|
saving: false,
|
||||||
|
@ -38,16 +47,18 @@ export default function reducer(state = defaultState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _createShortUrl = (ShlinkApiClient, data) => async dispatch => {
|
export const _createShortUrl = (shlinkApiClient, data) => async (dispatch) => {
|
||||||
dispatch({ type: CREATE_SHORT_URL_START });
|
dispatch({ type: CREATE_SHORT_URL_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await ShlinkApiClient.createShortUrl(data);
|
const result = await shlinkApiClient.createShortUrl(data);
|
||||||
|
|
||||||
dispatch({ type: CREATE_SHORT_URL, result });
|
dispatch({ type: CREATE_SHORT_URL, result });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: CREATE_SHORT_URL_ERROR });
|
dispatch({ type: CREATE_SHORT_URL_ERROR });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const createShortUrl = curry(_createShortUrl)(ShlinkApiClient);
|
|
||||||
|
export const createShortUrl = curry(_createShortUrl)(shlinkApiClient);
|
||||||
|
|
||||||
export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL });
|
export const resetCreateShortUrl = () => ({ type: RESET_CREATE_SHORT_URL });
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { curry } from 'ramda';
|
import { curry } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
export const EDIT_SHORT_URL_TAGS_ERROR = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_ERROR';
|
export const EDIT_SHORT_URL_TAGS_ERROR = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_ERROR';
|
||||||
|
|
||||||
export const EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS';
|
export const EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS';
|
||||||
|
|
||||||
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
|
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
|
||||||
|
|
||||||
export const SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED';
|
export const SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED';
|
||||||
|
|
||||||
export const shortUrlTagsType = PropTypes.shape({
|
export const shortUrlTagsType = PropTypes.shape({
|
||||||
|
@ -50,19 +54,21 @@ export default function reducer(state = defaultState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _editShortUrlTags = (ShlinkApiClient, shortCode, tags) => async (dispatch, getState) => {
|
export const _editShortUrlTags = (shlinkApiClient, shortCode, tags) => async (dispatch) => {
|
||||||
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
|
dispatch({ type: EDIT_SHORT_URL_TAGS_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update short URL tags
|
// Update short URL tags
|
||||||
await ShlinkApiClient.updateShortUrlTags(shortCode, tags);
|
await shlinkApiClient.updateShortUrlTags(shortCode, tags);
|
||||||
dispatch({ tags, shortCode, type: EDIT_SHORT_URL_TAGS });
|
dispatch({ tags, shortCode, type: EDIT_SHORT_URL_TAGS });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR });
|
dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const editShortUrlTags = curry(_editShortUrlTags)(ShlinkApiClient);
|
|
||||||
|
export const editShortUrlTags = curry(_editShortUrlTags)(shlinkApiClient);
|
||||||
|
|
||||||
export const resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS });
|
export const resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS });
|
||||||
|
|
||||||
|
|
|
@ -1,50 +1,60 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { curry } from 'ramda';
|
import { curry } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
import { shortUrlType } from './shortUrlsList';
|
||||||
|
|
||||||
const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
||||||
const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
|
const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
|
||||||
const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
|
const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
|
||||||
|
|
||||||
|
export const shortUrlVisitsType = {
|
||||||
|
shortUrl: shortUrlType,
|
||||||
|
visits: PropTypes.array,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
shortUrl: {},
|
shortUrl: {},
|
||||||
visits: [],
|
visits: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function dispatch (state = initialState, action) {
|
export default function dispatch(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case GET_SHORT_URL_VISITS_START:
|
case GET_SHORT_URL_VISITS_START:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: true
|
loading: true,
|
||||||
};
|
};
|
||||||
case GET_SHORT_URL_VISITS_ERROR:
|
case GET_SHORT_URL_VISITS_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true
|
error: true,
|
||||||
};
|
};
|
||||||
case GET_SHORT_URL_VISITS:
|
case GET_SHORT_URL_VISITS:
|
||||||
return {
|
return {
|
||||||
shortUrl: action.shortUrl,
|
shortUrl: action.shortUrl,
|
||||||
visits: action.visits,
|
visits: action.visits,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false
|
error: false,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _getShortUrlVisits = (ShlinkApiClient, shortCode, dates) => dispatch => {
|
export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => (dispatch) => {
|
||||||
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
ShlinkApiClient.getShortUrlVisits(shortCode, dates),
|
shlinkApiClient.getShortUrlVisits(shortCode, dates),
|
||||||
ShlinkApiClient.getShortUrl(shortCode)
|
shlinkApiClient.getShortUrl(shortCode),
|
||||||
])
|
])
|
||||||
.then(([visits, shortUrl]) => dispatch({ visits, shortUrl, type: GET_SHORT_URL_VISITS }))
|
.then(([ visits, shortUrl ]) => dispatch({ visits, shortUrl, type: GET_SHORT_URL_VISITS }))
|
||||||
.catch(() => dispatch({ type: GET_SHORT_URL_VISITS_ERROR }));
|
.catch(() => dispatch({ type: GET_SHORT_URL_VISITS_ERROR }));
|
||||||
};
|
};
|
||||||
export const getShortUrlVisits = curry(_getShortUrlVisits)(ShlinkApiClient);
|
|
||||||
|
export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient);
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
|
||||||
import { assoc, assocPath } from 'ramda';
|
import { assoc, assocPath } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||||
|
|
||||||
const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
||||||
const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
|
const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
|
||||||
|
|
||||||
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
||||||
|
|
||||||
|
export const shortUrlType = PropTypes.shape({
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
shortCode: PropTypes.string,
|
||||||
|
originalUrl: PropTypes.string,
|
||||||
|
});
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
shortUrls: {},
|
shortUrls: {},
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -19,34 +27,36 @@ export default function reducer(state = initialState, action) {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
shortUrls: action.shortUrls
|
shortUrls: action.shortUrls,
|
||||||
};
|
};
|
||||||
case LIST_SHORT_URLS_ERROR:
|
case LIST_SHORT_URLS_ERROR:
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true,
|
error: true,
|
||||||
shortUrls: []
|
shortUrls: [],
|
||||||
};
|
};
|
||||||
case SHORT_URL_TAGS_EDITED:
|
case SHORT_URL_TAGS_EDITED:
|
||||||
const { data } = state.shortUrls;
|
const { data } = state.shortUrls;
|
||||||
return assocPath(['shortUrls', 'data'], data.map(shortUrl =>
|
|
||||||
|
return assocPath([ 'shortUrls', 'data' ], data.map((shortUrl) =>
|
||||||
shortUrl.shortCode === action.shortCode
|
shortUrl.shortCode === action.shortCode
|
||||||
? assoc('tags', action.tags, shortUrl)
|
? assoc('tags', action.tags, shortUrl)
|
||||||
: shortUrl
|
: shortUrl), state);
|
||||||
), state);
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _listShortUrls = (ShlinkApiClient, params = {}) => async dispatch => {
|
export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) => {
|
||||||
dispatch({ type: LIST_SHORT_URLS_START });
|
dispatch({ type: LIST_SHORT_URLS_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shortUrls = await ShlinkApiClient.listShortUrls(params);
|
const shortUrls = await shlinkApiClient.listShortUrls(params);
|
||||||
|
|
||||||
dispatch({ type: LIST_SHORT_URLS, shortUrls, params });
|
dispatch({ type: LIST_SHORT_URLS, shortUrls, params });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
|
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const listShortUrls = (params = {}) => _listShortUrls(ShlinkApiClient, params);
|
|
||||||
|
export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params);
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { LIST_SHORT_URLS } from './shortUrlsList';
|
import { LIST_SHORT_URLS } from './shortUrlsList';
|
||||||
|
|
||||||
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
|
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
|
||||||
|
|
||||||
|
export const shortUrlsListParamsType = {
|
||||||
|
page: PropTypes.string,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
searchTerm: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
const defaultState = { page: '1' };
|
const defaultState = { page: '1' };
|
||||||
|
|
||||||
export default function reducer(state = defaultState, action) {
|
export default function reducer(state = defaultState, action) {
|
||||||
|
|
|
@ -2,20 +2,21 @@ import { Card, CardBody } from 'reactstrap';
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import deleteIcon from '@fortawesome/fontawesome-free-solid/faTrash';
|
import deleteIcon from '@fortawesome/fontawesome-free-solid/faTrash';
|
||||||
import editIcon from '@fortawesome/fontawesome-free-solid/faPencilAlt';
|
import editIcon from '@fortawesome/fontawesome-free-solid/faPencilAlt';
|
||||||
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ColorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
|
||||||
import './TagCard.scss';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import colorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
||||||
|
import './TagCard.scss';
|
||||||
|
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
|
||||||
import EditTagModal from './helpers/EditTagModal';
|
import EditTagModal from './helpers/EditTagModal';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
tag: PropTypes.string,
|
tag: PropTypes.string,
|
||||||
|
currentServerId: PropTypes.string,
|
||||||
colorGenerator: colorGeneratorType,
|
colorGenerator: colorGeneratorType,
|
||||||
};
|
};
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class TagCard extends React.Component {
|
export default class TagCard extends React.Component {
|
||||||
|
@ -24,9 +25,9 @@ export default class TagCard extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { tag, colorGenerator, currentServerId } = this.props;
|
const { tag, colorGenerator, currentServerId } = this.props;
|
||||||
const toggleDelete = () =>
|
const toggleDelete = () =>
|
||||||
this.setState({ isDeleteModalOpen: !this.state.isDeleteModalOpen });
|
this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen }));
|
||||||
const toggleEdit = () =>
|
const toggleEdit = () =>
|
||||||
this.setState({ isEditModalOpen: !this.state.isEditModalOpen });
|
this.setState(({ isEditModalOpen }) => ({ isEditModalOpen: !isEditModalOpen }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
|
@ -35,17 +36,17 @@ export default class TagCard extends React.Component {
|
||||||
className="btn btn-light btn-sm tag-card__btn tag-card__btn--last"
|
className="btn btn-light btn-sm tag-card__btn tag-card__btn--last"
|
||||||
onClick={toggleDelete}
|
onClick={toggleDelete}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={deleteIcon}/>
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-light btn-sm tag-card__btn"
|
className="btn btn-light btn-sm tag-card__btn"
|
||||||
onClick={toggleEdit}
|
onClick={toggleEdit}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={editIcon}/>
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</button>
|
</button>
|
||||||
<h5 className="tag-card__tag-title">
|
<h5 className="tag-card__tag-title">
|
||||||
<div
|
<div
|
||||||
style={{backgroundColor: colorGenerator.getColorForKey(tag)}}
|
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
|
||||||
className="tag-card__tag-bullet"
|
className="tag-card__tag-bullet"
|
||||||
/>
|
/>
|
||||||
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>
|
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>
|
||||||
|
|
|
@ -1,25 +1,35 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { pick, splitEvery } from 'ramda';
|
import { pick, splitEvery } from 'ramda';
|
||||||
import { filterTags, listTags } from './reducers/tagsList';
|
import PropTypes from 'prop-types';
|
||||||
import MuttedMessage from '../utils/MuttedMessage';
|
import MuttedMessage from '../utils/MuttedMessage';
|
||||||
import TagCard from './TagCard';
|
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
|
import { filterTags, listTags } from './reducers/tagsList';
|
||||||
|
import TagCard from './TagCard';
|
||||||
|
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
|
const TAGS_GROUP_SIZE = 4;
|
||||||
|
const propTypes = {
|
||||||
|
filterTags: PropTypes.func,
|
||||||
|
listTags: PropTypes.func,
|
||||||
|
tagsList: PropTypes.shape({
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
match: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
export class TagsList extends React.Component {
|
export class TagsListComponent extends React.Component {
|
||||||
state = { isDeleteModalOpen: false };
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { listTags } = this.props;
|
const { listTags } = this.props;
|
||||||
|
|
||||||
listTags();
|
listTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { tagsList, match } = this.props;
|
const { tagsList, match } = this.props;
|
||||||
|
|
||||||
if (tagsList.loading) {
|
if (tagsList.loading) {
|
||||||
return <MuttedMessage marginSize={0}>Loading...</MuttedMessage>
|
return <MuttedMessage marginSize={0}>Loading...</MuttedMessage>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagsList.error) {
|
if (tagsList.error) {
|
||||||
|
@ -31,17 +41,18 @@ export class TagsList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsCount = tagsList.filteredTags.length;
|
const tagsCount = tagsList.filteredTags.length;
|
||||||
|
|
||||||
if (tagsCount < 1) {
|
if (tagsCount < 1) {
|
||||||
return <MuttedMessage>No tags found</MuttedMessage>;
|
return <MuttedMessage>No tags found</MuttedMessage>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsGroups = splitEvery(ceil(tagsCount / 4), tagsList.filteredTags);
|
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUP_SIZE), tagsList.filteredTags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{tagsGroups.map((group, index) => (
|
{tagsGroups.map((group, index) => (
|
||||||
<div key={index} className="col-md-6 col-xl-3">
|
<div key={index} className="col-md-6 col-xl-3">
|
||||||
{group.map(tag => (
|
{group.map((tag) => (
|
||||||
<TagCard
|
<TagCard
|
||||||
key={tag}
|
key={tag}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
|
@ -61,9 +72,9 @@ export class TagsList extends React.Component {
|
||||||
<div className="shlink-container">
|
<div className="shlink-container">
|
||||||
{!this.props.tagsList.loading && (
|
{!this.props.tagsList.loading && (
|
||||||
<SearchField
|
<SearchField
|
||||||
onChange={filterTags}
|
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
placeholder="Search tags..."
|
placeholder="Search tags..."
|
||||||
|
onChange={filterTags}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
@ -74,4 +85,8 @@ export class TagsList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(pick(['tagsList']), { listTags, filterTags })(TagsList);
|
TagsListComponent.propTypes = propTypes;
|
||||||
|
|
||||||
|
const TagsList = connect(pick([ 'tagsList' ]), { listTags, filterTags })(TagsListComponent);
|
||||||
|
|
||||||
|
export default TagsList;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
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';
|
||||||
|
@ -11,22 +11,25 @@ const propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
deleteTag: PropTypes.func,
|
deleteTag: PropTypes.func,
|
||||||
tagDelete: tagDeleteType,
|
tagDelete: tagDeleteType,
|
||||||
|
tagDeleted: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DeleteTagConfirmModal extends Component {
|
export class DeleteTagConfirmModalComponent extends React.Component {
|
||||||
doDelete = () => {
|
doDelete = () => {
|
||||||
const { tag, toggle, deleteTag } = this.props;
|
const { tag, toggle, deleteTag } = this.props;
|
||||||
|
|
||||||
deleteTag(tag).then(() => {
|
deleteTag(tag).then(() => {
|
||||||
this.tagWasDeleted = true;
|
this.tagWasDeleted = true;
|
||||||
toggle();
|
toggle();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
onClosed = () => {
|
handleOnClosed = () => {
|
||||||
if (!this.tagWasDeleted) {
|
if (!this.tagWasDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tagDeleted, tag } = this.props;
|
const { tagDeleted, tag } = this.props;
|
||||||
|
|
||||||
tagDeleted(tag);
|
tagDeleted(tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,7 +41,7 @@ export class DeleteTagConfirmModal extends Component {
|
||||||
const { tag, toggle, isOpen, tagDelete } = this.props;
|
const { tag, toggle, isOpen, tagDelete } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal toggle={toggle} isOpen={isOpen} centered onClosed={this.onClosed}>
|
<Modal toggle={toggle} isOpen={isOpen} centered onClosed={this.handleOnClosed}>
|
||||||
<ModalHeader toggle={toggle}>
|
<ModalHeader toggle={toggle}>
|
||||||
<span className="text-danger">Delete tag</span>
|
<span className="text-danger">Delete tag</span>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
@ -54,8 +57,8 @@ export class DeleteTagConfirmModal extends Component {
|
||||||
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
<button className="btn btn-link" onClick={toggle}>Cancel</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
onClick={this.doDelete}
|
|
||||||
disabled={tagDelete.deleting}
|
disabled={tagDelete.deleting}
|
||||||
|
onClick={() => this.doDelete()}
|
||||||
>
|
>
|
||||||
{tagDelete.deleting ? 'Deleting tag...' : 'Delete tag'}
|
{tagDelete.deleting ? 'Deleting tag...' : 'Delete tag'}
|
||||||
</button>
|
</button>
|
||||||
|
@ -65,9 +68,11 @@ export class DeleteTagConfirmModal extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteTagConfirmModal.propTypes = propTypes;
|
DeleteTagConfirmModalComponent.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(
|
const DeleteTagConfirmModal = connect(
|
||||||
pick(['tagDelete']),
|
pick([ 'tagDelete' ]),
|
||||||
{ deleteTag, tagDeleted }
|
{ deleteTag, tagDeleted }
|
||||||
)(DeleteTagConfirmModal);
|
)(DeleteTagConfirmModalComponent);
|
||||||
|
|
||||||
|
export default DeleteTagConfirmModal;
|
||||||
|
|
|
@ -2,20 +2,32 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
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 { pick } from 'ramda';
|
||||||
import { editTag, tagEdited } from '../reducers/tagEdit';
|
|
||||||
import { ChromePicker } from 'react-color';
|
import { ChromePicker } from 'react-color';
|
||||||
import ColorGenerator from '../../utils/ColorGenerator';
|
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 colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
|
||||||
|
import { editTag, tagEdited } from '../reducers/tagEdit';
|
||||||
import './EditTagModal.scss';
|
import './EditTagModal.scss';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
tag: PropTypes.string,
|
||||||
|
editTag: PropTypes.func,
|
||||||
|
toggle: PropTypes.func,
|
||||||
|
tagEdited: PropTypes.func,
|
||||||
|
colorGenerator: colorGeneratorType,
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
tagEdit: PropTypes.shape({
|
||||||
|
error: PropTypes.bool,
|
||||||
|
editing: PropTypes.bool,
|
||||||
|
}),
|
||||||
|
};
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class EditTagModalComponent extends React.Component {
|
||||||
export class EditTagModal extends React.Component {
|
saveTag = (e) => {
|
||||||
saveTag = e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { tag: oldName, editTag, toggle } = this.props;
|
const { tag: oldName, editTag, toggle } = this.props;
|
||||||
const { tag: newName, color } = this.state;
|
const { tag: newName, color } = this.state;
|
||||||
|
@ -27,13 +39,14 @@ export class EditTagModal extends React.Component {
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
onClosed = () => {
|
handleOnClosed = () => {
|
||||||
if (!this.tagWasEdited) {
|
if (!this.tagWasEdited) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tag: oldName, tagEdited } = this.props;
|
const { tag: oldName, tagEdited } = this.props;
|
||||||
const { tag: newName, color } = this.state;
|
const { tag: newName, color } = this.state;
|
||||||
|
|
||||||
tagEdited(oldName, newName, color);
|
tagEdited(oldName, newName, color);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,11 +54,12 @@ export class EditTagModal extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { colorGenerator, tag } = props;
|
const { colorGenerator, tag } = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showColorPicker: false,
|
showColorPicker: false,
|
||||||
tag,
|
tag,
|
||||||
color: colorGenerator.getColorForKey(tag)
|
color: colorGenerator.getColorForKey(tag),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -56,11 +70,11 @@ export class EditTagModal extends React.Component {
|
||||||
const { isOpen, toggle, tagEdit } = this.props;
|
const { isOpen, toggle, tagEdit } = this.props;
|
||||||
const { tag, color } = this.state;
|
const { tag, color } = this.state;
|
||||||
const toggleColorPicker = () =>
|
const toggleColorPicker = () =>
|
||||||
this.setState({ showColorPicker: !this.state.showColorPicker });
|
this.setState(({ showColorPicker }) => ({ showColorPicker: !showColorPicker }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.onClosed}>
|
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={this.handleOnClosed}>
|
||||||
<form onSubmit={this.saveTag}>
|
<form onSubmit={() => this.saveTag()}>
|
||||||
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
<ModalHeader toggle={toggle}>Edit tag</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
|
@ -87,17 +101,17 @@ export class EditTagModal extends React.Component {
|
||||||
>
|
>
|
||||||
<ChromePicker
|
<ChromePicker
|
||||||
color={color}
|
color={color}
|
||||||
onChange={color => this.setState({ color: color.hex })}
|
|
||||||
disableAlpha
|
disableAlpha
|
||||||
|
onChange={(color) => this.setState({ color: color.hex })}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tag}
|
value={tag}
|
||||||
onChange={e => this.setState({ tag: e.target.value })}
|
|
||||||
placeholder="Tag"
|
placeholder="Tag"
|
||||||
required
|
required
|
||||||
className="form-control"
|
className="form-control"
|
||||||
|
onChange={(e) => this.setState({ tag: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -119,6 +133,9 @@ export class EditTagModal extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditTagModal.defaultProps = defaultProps;
|
EditTagModalComponent.propTypes = propTypes;
|
||||||
|
EditTagModalComponent.defaultProps = defaultProps;
|
||||||
|
|
||||||
export default connect(pick(['tagEdit']), { editTag, tagEdited })(EditTagModal);
|
const EditTagModal = connect(pick([ 'tagEdit' ]), { editTag, tagEdited })(EditTagModalComponent);
|
||||||
|
|
||||||
|
export default EditTagModal;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { curry } from 'ramda';
|
import { curry } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
|
||||||
const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
||||||
const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR';
|
const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR';
|
||||||
const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
||||||
|
|
||||||
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
||||||
|
|
||||||
export const tagDeleteType = PropTypes.shape({
|
export const tagDeleteType = PropTypes.shape({
|
||||||
|
@ -39,17 +40,19 @@ export default function reduce(state = defaultState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _deleteTag = (ShlinkApiClient, tag) => async dispatch => {
|
export const _deleteTag = (shlinkApiClient, tag) => async (dispatch) => {
|
||||||
dispatch({ type: DELETE_TAG_START });
|
dispatch({ type: DELETE_TAG_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ShlinkApiClient.deleteTags([tag]);
|
await shlinkApiClient.deleteTags([ tag ]);
|
||||||
dispatch({ type: DELETE_TAG });
|
dispatch({ type: DELETE_TAG });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: DELETE_TAG_ERROR });
|
dispatch({ type: DELETE_TAG_ERROR });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const deleteTag = curry(_deleteTag)(ShlinkApiClient);
|
|
||||||
|
|
||||||
export const tagDeleted = tag => ({ type: TAG_DELETED, tag });
|
export const deleteTag = curry(_deleteTag)(shlinkApiClient);
|
||||||
|
|
||||||
|
export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag });
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import ColorGenerator from '../../utils/ColorGenerator';
|
|
||||||
import { curry, pick } from 'ramda';
|
import { curry, pick } from 'ramda';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
import colorGenerator from '../../utils/ColorGenerator';
|
||||||
|
|
||||||
const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||||
const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
|
const EDIT_TAG_ERROR = 'shlink/editTag/EDIT_TAG_ERROR';
|
||||||
const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
||||||
|
|
||||||
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
|
@ -30,7 +31,7 @@ export default function reducer(state = defaultState, action) {
|
||||||
};
|
};
|
||||||
case EDIT_TAG:
|
case EDIT_TAG:
|
||||||
return {
|
return {
|
||||||
...pick(['oldName', 'newName'], action),
|
...pick([ 'oldName', 'newName' ], action),
|
||||||
editing: false,
|
editing: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
@ -39,20 +40,22 @@ export default function reducer(state = defaultState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _editTag = (ShlinkApiClient, ColorGenerator, oldName, newName, color) =>
|
export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, color) =>
|
||||||
async dispatch => {
|
async (dispatch) => {
|
||||||
dispatch({ type: EDIT_TAG_START });
|
dispatch({ type: EDIT_TAG_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ShlinkApiClient.editTag(oldName, newName);
|
await shlinkApiClient.editTag(oldName, newName);
|
||||||
ColorGenerator.setColorForKey(newName, color);
|
colorGenerator.setColorForKey(newName, color);
|
||||||
dispatch({ type: EDIT_TAG, oldName, newName });
|
dispatch({ type: EDIT_TAG, oldName, newName });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: EDIT_TAG_ERROR });
|
dispatch({ type: EDIT_TAG_ERROR });
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const editTag = curry(_editTag)(ShlinkApiClient, ColorGenerator);
|
|
||||||
|
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,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import ShlinkApiClient from '../../api/ShlinkApiClient';
|
|
||||||
import { TAG_DELETED } from './tagDelete';
|
|
||||||
import { reject } from 'ramda';
|
import { reject } from 'ramda';
|
||||||
|
import shlinkApiClient from '../../api/ShlinkApiClient';
|
||||||
|
import { TAG_DELETED } from './tagDelete';
|
||||||
import { TAG_EDITED } from './tagEdit';
|
import { TAG_EDITED } from './tagEdit';
|
||||||
|
|
||||||
const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
||||||
|
@ -16,7 +16,7 @@ const defaultState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(state = defaultState, action) {
|
export default function reducer(state = defaultState, action) {
|
||||||
switch(action.type) {
|
switch (action.type) {
|
||||||
case LIST_TAGS_START:
|
case LIST_TAGS_START:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -39,14 +39,17 @@ export default function reducer(state = defaultState, action) {
|
||||||
case TAG_DELETED:
|
case TAG_DELETED:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
||||||
// FIXME This should be optimized somehow...
|
// FIXME This should be optimized somehow...
|
||||||
tags: reject(tag => tag === action.tag, state.tags),
|
tags: reject((tag) => tag === action.tag, state.tags),
|
||||||
filteredTags: reject(tag => tag === action.tag, state.filteredTags),
|
filteredTags: reject((tag) => tag === action.tag, state.filteredTags),
|
||||||
};
|
};
|
||||||
case TAG_EDITED:
|
case TAG_EDITED:
|
||||||
const renameTag = tag => tag === action.oldName ? action.newName : tag;
|
const renameTag = (tag) => tag === action.oldName ? action.newName : tag;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
||||||
// FIXME This should be optimized somehow...
|
// FIXME This should be optimized somehow...
|
||||||
tags: state.tags.map(renameTag).sort(),
|
tags: state.tags.map(renameTag).sort(),
|
||||||
filteredTags: state.filteredTags.map(renameTag).sort(),
|
filteredTags: state.filteredTags.map(renameTag).sort(),
|
||||||
|
@ -55,7 +58,7 @@ export default function reducer(state = defaultState, action) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
filteredTags: state.tags.filter(
|
filteredTags: state.tags.filter(
|
||||||
tag => tag.toLowerCase().match(action.searchTerm),
|
(tag) => tag.toLowerCase().match(action.searchTerm),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
@ -63,19 +66,21 @@ export default function reducer(state = defaultState, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const _listTags = ShlinkApiClient => async dispatch => {
|
export const _listTags = (shlinkApiClient) => async (dispatch) => {
|
||||||
dispatch({ type: LIST_TAGS_START });
|
dispatch({ type: LIST_TAGS_START });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tags = await ShlinkApiClient.listTags();
|
const tags = await shlinkApiClient.listTags();
|
||||||
|
|
||||||
dispatch({ tags, type: LIST_TAGS });
|
dispatch({ tags, type: LIST_TAGS });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: LIST_TAGS_ERROR });
|
dispatch({ type: LIST_TAGS_ERROR });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const listTags = () => _listTags(ShlinkApiClient);
|
|
||||||
|
|
||||||
export const filterTags = searchTerm => ({
|
export const listTags = () => _listTags(shlinkApiClient);
|
||||||
|
|
||||||
|
export const filterTags = (searchTerm) => ({
|
||||||
type: FILTER_TAGS,
|
type: FILTER_TAGS,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import Storage from './Storage';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { range } from 'ramda';
|
import { range } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import storage from './Storage';
|
||||||
|
|
||||||
|
const HEX_COLOR_LENGTH = 6;
|
||||||
const { floor, random } = Math;
|
const { floor, random } = Math;
|
||||||
const letters = '0123456789ABCDEF';
|
const letters = '0123456789ABCDEF';
|
||||||
const buildRandomColor = () =>
|
const buildRandomColor = () =>
|
||||||
`#${
|
`#${
|
||||||
range(0, 6)
|
range(0, HEX_COLOR_LENGTH)
|
||||||
.map(() => letters[floor(random() * 16)])
|
.map(() => letters[floor(random() * letters.length)])
|
||||||
.join('')
|
.join('')
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
@ -17,12 +18,13 @@ export class ColorGenerator {
|
||||||
this.colors = this.storage.get('colors') || {};
|
this.colors = this.storage.get('colors') || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorForKey = key => {
|
getColorForKey = (key) => {
|
||||||
const color = this.colors[key];
|
const color = this.colors[key];
|
||||||
|
|
||||||
// If a color has not been set yet, generate a random one and save it
|
// If a color has not been set yet, generate a random one and save it
|
||||||
if (!color) {
|
if (!color) {
|
||||||
this.setColorForKey(key, buildRandomColor());
|
this.setColorForKey(key, buildRandomColor());
|
||||||
|
|
||||||
return this.getColorForKey(key);
|
return this.getColorForKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,4 +42,6 @@ export const colorGeneratorType = PropTypes.shape({
|
||||||
setColorForKey: PropTypes.func,
|
setColorForKey: PropTypes.func,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default new ColorGenerator(Storage);
|
const colorGenerator = new ColorGenerator(storage);
|
||||||
|
|
||||||
|
export default colorGenerator;
|
||||||
|
|
19
src/utils/ExternalLink.js
Normal file
19
src/utils/ExternalLink.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
href: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ExternalLink(props) {
|
||||||
|
const { href, children, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href={href} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalLink.propTypes = propTypes;
|
|
@ -1,8 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Card } from 'reactstrap';
|
import { Card } from 'reactstrap';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default function MutedMessage({ children, marginSize = 4 }) {
|
const DEFAULT_MARGIN_SIZE = 4;
|
||||||
|
const propTypes = {
|
||||||
|
marginSize: PropTypes.number,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MutedMessage({ children, marginSize = DEFAULT_MARGIN_SIZE }) {
|
||||||
const cardClasses = classnames('bg-light', {
|
const cardClasses = classnames('bg-light', {
|
||||||
[`mt-${marginSize}`]: marginSize > 0,
|
[`mt-${marginSize}`]: marginSize > 0,
|
||||||
});
|
});
|
||||||
|
@ -17,3 +24,5 @@ export default function MutedMessage({ children, marginSize = 4 }) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MutedMessage.propTypes = propTypes;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import './SearchField.scss';
|
import './SearchField.scss';
|
||||||
|
|
||||||
|
const DEFAULT_SEARCH_INTERVAL = 500;
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
@ -19,7 +20,7 @@ export default class SearchField extends React.Component {
|
||||||
state = { showClearBtn: false, searchTerm: '' };
|
state = { showClearBtn: false, searchTerm: '' };
|
||||||
timer = null;
|
timer = null;
|
||||||
|
|
||||||
searchTermChanged(searchTerm, timeout = 500) {
|
searchTermChanged(searchTerm, timeout = DEFAULT_SEARCH_INTERVAL) {
|
||||||
this.setState({
|
this.setState({
|
||||||
showClearBtn: searchTerm !== '',
|
showClearBtn: searchTerm !== '',
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
@ -29,6 +30,7 @@ export default class SearchField extends React.Component {
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
resetTimer();
|
resetTimer();
|
||||||
|
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
|
@ -46,15 +48,15 @@ export default class SearchField extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-lg search-field__input"
|
className="form-control form-control-lg search-field__input"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={e => this.searchTermChanged(e.target.value)}
|
|
||||||
value={this.state.searchTerm}
|
value={this.state.searchTerm}
|
||||||
|
onChange={(e) => this.searchTermChanged(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon icon={searchIcon} className="search-field__icon" />
|
<FontAwesomeIcon icon={searchIcon} className="search-field__icon" />
|
||||||
<div
|
<div
|
||||||
className="close search-field__close"
|
className="close search-field__close"
|
||||||
hidden={! this.state.showClearBtn}
|
hidden={!this.state.showClearBtn}
|
||||||
onClick={() => this.searchTermChanged('', 0)}
|
|
||||||
id="search-field__close"
|
id="search-field__close"
|
||||||
|
onClick={() => this.searchTermChanged('', 0)}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
const PREFIX = 'shlink';
|
const PREFIX = 'shlink';
|
||||||
const buildPath = path => `${PREFIX}.${path}`;
|
const buildPath = (path) => `${PREFIX}.${path}`;
|
||||||
|
|
||||||
export class Storage {
|
export class Storage {
|
||||||
constructor(localStorage) {
|
constructor(localStorage) {
|
||||||
this.localStorage = localStorage;
|
this.localStorage = localStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
get = key => {
|
get = (key) => {
|
||||||
const item = this.localStorage.getItem(buildPath(key));
|
const item = this.localStorage.getItem(buildPath(key));
|
||||||
|
|
||||||
return item ? JSON.parse(item) : undefined;
|
return item ? JSON.parse(item) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,4 +16,5 @@ export class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
const storage = typeof localStorage !== 'undefined' ? localStorage : {};
|
const storage = typeof localStorage !== 'undefined' ? localStorage : {};
|
||||||
|
|
||||||
export default new Storage(storage);
|
export default new Storage(storage);
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ColorGenerator from '../utils/ColorGenerator';
|
import PropTypes from 'prop-types';
|
||||||
|
import colorGenerator, { colorGeneratorType } from '../utils/ColorGenerator';
|
||||||
import './Tag.scss';
|
import './Tag.scss';
|
||||||
|
|
||||||
export default function Tag (
|
const propTypes = {
|
||||||
|
colorGenerator: colorGeneratorType,
|
||||||
|
text: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
clearable: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
const defaultProps = {
|
||||||
|
colorGenerator,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Tag(
|
||||||
{
|
{
|
||||||
colorGenerator,
|
colorGenerator,
|
||||||
text,
|
text,
|
||||||
children,
|
children,
|
||||||
clearable,
|
clearable,
|
||||||
onClick = () => ({}),
|
onClick = () => ({}),
|
||||||
onClose = () => ({})
|
onClose = () => ({}),
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
@ -24,6 +37,5 @@ export default function Tag (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tag.defaultProps = {
|
Tag.defaultProps = defaultProps;
|
||||||
colorGenerator: ColorGenerator
|
Tag.propTypes = propTypes;
|
||||||
};
|
|
||||||
|
|
|
@ -1,38 +1,41 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TagsInput from 'react-tagsinput';
|
import TagsInput from 'react-tagsinput';
|
||||||
import ColorGenerator, { colorGeneratorType } from './ColorGenerator';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import colorGenerator, { colorGeneratorType } from './ColorGenerator';
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator,
|
||||||
placeholder: 'Add tags to the URL',
|
placeholder: 'Add tags to the URL',
|
||||||
};
|
};
|
||||||
const propTypes = {
|
const 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
|
colorGenerator: colorGeneratorType,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) {
|
export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) {
|
||||||
const renderTag = (props) => {
|
const renderTag = (props) => {
|
||||||
const { tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other } = props;
|
const { tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||||
{getTagDisplayValue(tag)}
|
{getTagDisplayValue(tag)}
|
||||||
{!disabled && <span className={classNameRemove} onClick={() => onRemove(key)} />}
|
{!disabled && <span className={classNameRemove} onClick={() => onRemove(key)} />}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsInput
|
<TagsInput
|
||||||
value={tags}
|
value={tags}
|
||||||
onChange={onChange}
|
|
||||||
inputProps={{ placeholder }}
|
inputProps={{ placeholder }}
|
||||||
onlyUnique
|
onlyUnique
|
||||||
addOnBlur // FIXME Workaround to be able to add tags on Android
|
|
||||||
renderTag={renderTag}
|
renderTag={renderTag}
|
||||||
|
|
||||||
|
// FIXME Workaround to be able to add tags on Android
|
||||||
|
addOnBlur
|
||||||
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,90 @@
|
||||||
import { assoc, isNil, isEmpty, reduce } from 'ramda';
|
import { assoc, isNil, isEmpty, reduce } from 'ramda';
|
||||||
|
|
||||||
const osFromUserAgent = userAgent => {
|
const osFromUserAgent = (userAgent) => {
|
||||||
const lowerUserAgent = userAgent.toLowerCase();
|
const lowerUserAgent = userAgent.toLowerCase();
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case (lowerUserAgent.indexOf('linux') >= 0):
|
case lowerUserAgent.indexOf('linux') >= 0:
|
||||||
return 'Linux';
|
return 'Linux';
|
||||||
case (lowerUserAgent.indexOf('windows') >= 0):
|
case lowerUserAgent.indexOf('windows') >= 0:
|
||||||
return 'Windows';
|
return 'Windows';
|
||||||
case (lowerUserAgent.indexOf('mac') >= 0):
|
case lowerUserAgent.indexOf('mac') >= 0:
|
||||||
return 'MacOS';
|
return 'MacOS';
|
||||||
case (lowerUserAgent.indexOf('mobi') >= 0):
|
case lowerUserAgent.indexOf('mobi') >= 0:
|
||||||
return 'Mobile';
|
return 'Mobile';
|
||||||
default:
|
default:
|
||||||
return 'Others';
|
return 'Others';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const browserFromUserAgent = userAgent => {
|
const browserFromUserAgent = (userAgent) => {
|
||||||
const lowerUserAgent = userAgent.toLowerCase();
|
const lowerUserAgent = userAgent.toLowerCase();
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case (lowerUserAgent.indexOf('opera') >= 0 || lowerUserAgent.indexOf('opr') >= 0):
|
case lowerUserAgent.indexOf('opera') >= 0 || lowerUserAgent.indexOf('opr') >= 0:
|
||||||
return 'Opera';
|
return 'Opera';
|
||||||
case (lowerUserAgent.indexOf('firefox') >= 0):
|
case lowerUserAgent.indexOf('firefox') >= 0:
|
||||||
return 'Firefox';
|
return 'Firefox';
|
||||||
case (lowerUserAgent.indexOf('chrome') >= 0):
|
case lowerUserAgent.indexOf('chrome') >= 0:
|
||||||
return 'Chrome';
|
return 'Chrome';
|
||||||
case (lowerUserAgent.indexOf('safari') >= 0):
|
case lowerUserAgent.indexOf('safari') >= 0:
|
||||||
return 'Safari';
|
return 'Safari';
|
||||||
case (lowerUserAgent.indexOf('msie') >= 0):
|
case lowerUserAgent.indexOf('msie') >= 0:
|
||||||
return 'Internet Explorer';
|
return 'Internet Explorer';
|
||||||
default:
|
default:
|
||||||
return 'Others';
|
return 'Others';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractDomain = url => {
|
const extractDomain = (url) => {
|
||||||
const domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0];
|
const domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0];
|
||||||
|
|
||||||
return domain.split(':')[0];
|
return domain.split(':')[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processOsStats = visits =>
|
export const processOsStats = (visits) =>
|
||||||
reduce(
|
reduce(
|
||||||
(stats, visit) => {
|
(stats, { userAgent }) => {
|
||||||
const userAgent = visit.userAgent;
|
|
||||||
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
|
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
|
||||||
|
|
||||||
return assoc(os, (stats[os] || 0) + 1, stats);
|
return assoc(os, (stats[os] || 0) + 1, stats);
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
visits,
|
visits,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const processBrowserStats = visits =>
|
export const processBrowserStats = (visits) =>
|
||||||
reduce(
|
reduce(
|
||||||
(stats, visit) => {
|
(stats, { userAgent }) => {
|
||||||
const userAgent = visit.userAgent;
|
|
||||||
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
|
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
|
||||||
|
|
||||||
return assoc(browser, (stats[browser] || 0) + 1, stats);
|
return assoc(browser, (stats[browser] || 0) + 1, stats);
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
visits,
|
visits,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const processReferrersStats = visits =>
|
export const processReferrersStats = (visits) =>
|
||||||
reduce(
|
reduce(
|
||||||
(stats, visit) => {
|
(stats, visit) => {
|
||||||
const notHasDomain = isNil(visit.referer) || isEmpty(visit.referer);
|
const notHasDomain = isNil(visit.referer) || isEmpty(visit.referer);
|
||||||
const domain = notHasDomain ? 'Unknown' : extractDomain(visit.referer);
|
const domain = notHasDomain ? 'Unknown' : extractDomain(visit.referer);
|
||||||
return assoc(domain, (stats[domain]|| 0) + 1, stats);
|
|
||||||
|
return assoc(domain, (stats[domain] || 0) + 1, stats);
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
visits,
|
visits,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const processCountriesStats = visits =>
|
export const processCountriesStats = (visits) =>
|
||||||
reduce(
|
reduce(
|
||||||
(stats, { visitLocation }) => {
|
(stats, { visitLocation }) => {
|
||||||
const notHasCountry = isNil(visitLocation)
|
const notHasCountry = isNil(visitLocation)
|
||||||
|| isNil(visitLocation.countryName)
|
|| isNil(visitLocation.countryName)
|
||||||
|| isEmpty(visitLocation.countryName);
|
|| isEmpty(visitLocation.countryName);
|
||||||
const country = notHasCountry ? 'Unknown' : visitLocation.countryName;
|
const country = notHasCountry ? 'Unknown' : visitLocation.countryName;
|
||||||
return assoc(country, (stats[country]|| 0) + 1, stats);
|
|
||||||
|
return assoc(country, (stats[country] || 0) + 1, stats);
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
visits,
|
visits,
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { ShlinkApiClient } from '../../src/api/ShlinkApiClient'
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { head, last } from 'ramda';
|
import { head, last } from 'ramda';
|
||||||
|
import { ShlinkApiClient } from '../../src/api/ShlinkApiClient';
|
||||||
|
|
||||||
describe('ShlinkApiClient', () => {
|
describe('ShlinkApiClient', () => {
|
||||||
const createAxiosMock = extraData => () =>
|
const createAxiosMock = (extraData) => () =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
headers: { authorization: 'Bearer abc123' },
|
headers: { authorization: 'Bearer abc123' },
|
||||||
data: { token: 'abc123' },
|
data: { token: 'abc123' },
|
||||||
...extraData,
|
...extraData,
|
||||||
});
|
});
|
||||||
const createApiClient = extraData =>
|
const createApiClient = (extraData) =>
|
||||||
new ShlinkApiClient(createAxiosMock(extraData));
|
new ShlinkApiClient(createAxiosMock(extraData));
|
||||||
|
|
||||||
describe('listShortUrls', () => {
|
describe('listShortUrls', () => {
|
||||||
it('properly returns short URLs list', async () => {
|
it('properly returns short URLs list', async () => {
|
||||||
const expectedList = ['foo', 'bar'];
|
const expectedList = [ 'foo', 'bar' ];
|
||||||
|
|
||||||
const apiClient = createApiClient({
|
const apiClient = createApiClient({
|
||||||
data: {
|
data: {
|
||||||
|
@ -23,6 +23,7 @@ describe('ShlinkApiClient', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const actualList = await apiClient.listShortUrls();
|
const actualList = await apiClient.listShortUrls();
|
||||||
|
|
||||||
expect(expectedList).toEqual(actualList);
|
expect(expectedList).toEqual(actualList);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -35,6 +36,7 @@ describe('ShlinkApiClient', () => {
|
||||||
it('returns create short URL', async () => {
|
it('returns create short URL', async () => {
|
||||||
const apiClient = createApiClient({ data: shortUrl });
|
const apiClient = createApiClient({ data: shortUrl });
|
||||||
const result = await apiClient.createShortUrl({});
|
const result = await apiClient.createShortUrl({});
|
||||||
|
|
||||||
expect(result).toEqual(shortUrl);
|
expect(result).toEqual(shortUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,7 +56,7 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('getShortUrlVisits', () => {
|
describe('getShortUrlVisits', () => {
|
||||||
it('properly returns short URL visits', async () => {
|
it('properly returns short URL visits', async () => {
|
||||||
const expectedVisits = ['foo', 'bar'];
|
const expectedVisits = [ 'foo', 'bar' ];
|
||||||
const axiosSpy = sinon.spy(createAxiosMock({
|
const axiosSpy = sinon.spy(createAxiosMock({
|
||||||
data: {
|
data: {
|
||||||
visits: {
|
visits: {
|
||||||
|
@ -94,7 +96,7 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('updateShortUrlTags', () => {
|
describe('updateShortUrlTags', () => {
|
||||||
it('properly updates short URL tags', async () => {
|
it('properly updates short URL tags', async () => {
|
||||||
const expectedTags = ['foo', 'bar'];
|
const expectedTags = [ 'foo', 'bar' ];
|
||||||
const axiosSpy = sinon.spy(createAxiosMock({
|
const axiosSpy = sinon.spy(createAxiosMock({
|
||||||
data: { tags: expectedTags },
|
data: { tags: expectedTags },
|
||||||
}));
|
}));
|
||||||
|
@ -112,10 +114,10 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('listTags', () => {
|
describe('listTags', () => {
|
||||||
it('properly returns list of tags', async () => {
|
it('properly returns list of tags', async () => {
|
||||||
const expectedTags = ['foo', 'bar'];
|
const expectedTags = [ 'foo', 'bar' ];
|
||||||
const axiosSpy = sinon.spy(createAxiosMock({
|
const axiosSpy = sinon.spy(createAxiosMock({
|
||||||
data: {
|
data: {
|
||||||
tags: { data: expectedTags }
|
tags: { data: expectedTags },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const apiClient = new ShlinkApiClient(axiosSpy);
|
const apiClient = new ShlinkApiClient(axiosSpy);
|
||||||
|
@ -132,7 +134,7 @@ describe('ShlinkApiClient', () => {
|
||||||
|
|
||||||
describe('deleteTags', () => {
|
describe('deleteTags', () => {
|
||||||
it('properly deletes provided tags', async () => {
|
it('properly deletes provided tags', async () => {
|
||||||
const tags = ['foo', 'bar'];
|
const tags = [ 'foo', 'bar' ];
|
||||||
const axiosSpy = sinon.spy(createAxiosMock({}));
|
const axiosSpy = sinon.spy(createAxiosMock({}));
|
||||||
const apiClient = new ShlinkApiClient(axiosSpy);
|
const apiClient = new ShlinkApiClient(axiosSpy);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { shallow } from 'enzyme'
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react'
|
import React from 'react';
|
||||||
import AsideMenu from '../../src/common/AsideMenu'
|
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import AsideMenu from '../../src/common/AsideMenu';
|
||||||
|
|
||||||
describe('<AsideMenu />', () => {
|
describe('<AsideMenu />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
|
@ -15,9 +15,10 @@ describe('<AsideMenu />', () => {
|
||||||
|
|
||||||
it('contains links to different sections', () => {
|
it('contains links to different sections', () => {
|
||||||
const links = wrapped.find(NavLink);
|
const links = wrapped.find(NavLink);
|
||||||
|
const expectedLength = 3;
|
||||||
|
|
||||||
expect(links).toHaveLength(3);
|
expect(links).toHaveLength(expectedLength);
|
||||||
links.forEach(link => expect(link.prop('to')).toContain('abc123'));
|
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a button to delete server', () => {
|
it('contains a button to delete server', () => {
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import DateInput from '../../src/common/DateInput';
|
|
||||||
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import DateInput from '../../src/common/DateInput';
|
||||||
|
|
||||||
describe('<DateInput />', () => {
|
describe('<DateInput />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
|
|
||||||
const createComponent = (props = {}) => {
|
const createComponent = (props = {}) => {
|
||||||
wrapped = shallow(<DateInput {...props} />);
|
wrapped = shallow(<DateInput {...props} />);
|
||||||
|
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (wrapped !== undefined) {
|
if (wrapped !== undefined) {
|
||||||
wrapped.unmount();
|
wrapped.unmount();
|
||||||
|
|
|
@ -2,17 +2,21 @@ 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 { Home } from '../../src/common/Home';
|
import { HomeComponent } from '../../src/common/Home';
|
||||||
|
|
||||||
describe('<Home />', () => {
|
describe('<Home />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
resetSelectedServer: () => {},
|
resetSelectedServer() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
servers: {},
|
servers: {},
|
||||||
};
|
};
|
||||||
const createComponent = props => {
|
const createComponent = (props) => {
|
||||||
const actualProps = { ...defaultProps, ...props };
|
const actualProps = { ...defaultProps, ...props };
|
||||||
wrapped = shallow(<Home {...actualProps} />);
|
|
||||||
|
wrapped = shallow(<HomeComponent {...actualProps} />);
|
||||||
|
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +46,7 @@ describe('<Home />', () => {
|
||||||
const servers = {
|
const servers = {
|
||||||
1: { name: 'foo', id: '123' },
|
1: { name: 'foo', id: '123' },
|
||||||
2: { name: 'bar', id: '456' },
|
2: { name: 'bar', id: '456' },
|
||||||
}
|
};
|
||||||
const wrapped = createComponent({ servers });
|
const wrapped = createComponent({ servers });
|
||||||
|
|
||||||
expect(wrapped.find('Link')).toHaveLength(0);
|
expect(wrapped.find('Link')).toHaveLength(0);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { CreateServer } from '../../src/servers/CreateServer';
|
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { CreateServerComponent } from '../../src/servers/CreateServer';
|
||||||
import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn';
|
import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn';
|
||||||
|
|
||||||
describe('<CreateServer />', () => {
|
describe('<CreateServer />', () => {
|
||||||
|
@ -17,7 +17,7 @@ describe('<CreateServer />', () => {
|
||||||
historyMock.push.resetHistory();
|
historyMock.push.resetHistory();
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<CreateServer
|
<CreateServerComponent
|
||||||
createServer={createServerMock}
|
createServer={createServerMock}
|
||||||
resetSelectedServer={identity}
|
resetSelectedServer={identity}
|
||||||
history={historyMock}
|
history={historyMock}
|
||||||
|
@ -41,7 +41,10 @@ describe('<CreateServer />', () => {
|
||||||
|
|
||||||
it('creates server and redirects to it when form is submitted', () => {
|
it('creates server and redirects to it when form is submitted', () => {
|
||||||
const form = wrapper.find('form');
|
const form = wrapper.find('form');
|
||||||
form.simulate('submit', { preventDefault: () => {} });
|
|
||||||
|
form.simulate('submit', { preventDefault() {
|
||||||
|
return '';
|
||||||
|
} });
|
||||||
|
|
||||||
expect(createServerMock.callCount).toEqual(1);
|
expect(createServerMock.callCount).toEqual(1);
|
||||||
expect(historyMock.push.callCount).toEqual(1);
|
expect(historyMock.push.callCount).toEqual(1);
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DeleteServerButton from '../../src/servers/DeleteServerButton';
|
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
import DeleteServerButton 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(() => {
|
||||||
wrapper = shallow(<DeleteServerButton server={{}} className="button" />));
|
wrapper = shallow(<DeleteServerButton server={{}} className="button" />);
|
||||||
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
it('renders a button and a modal', () => {
|
it('renders a button and a modal', () => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
|
|
||||||
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';
|
||||||
|
|
||||||
describe('<DeleteServerModal />', () => {
|
describe('<DeleteServerModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -17,7 +17,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
historyMock.push.resetHistory();
|
historyMock.push.resetHistory();
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<DeleteServerModal
|
<DeleteServerModalComponent
|
||||||
server={{ name: serverName }}
|
server={{ name: serverName }}
|
||||||
toggle={toggleMock}
|
toggle={toggleMock}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
@ -37,6 +37,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
|
|
||||||
it('displays the name of the server as part of the content', () => {
|
it('displays the name of the server as part of the content', () => {
|
||||||
const modalBody = wrapper.find(ModalBody);
|
const modalBody = wrapper.find(ModalBody);
|
||||||
|
|
||||||
expect(modalBody.find('p').first().text()).toEqual(
|
expect(modalBody.find('p').first().text()).toEqual(
|
||||||
`Are you sure you want to delete server ${serverName}?`
|
`Are you sure you want to delete server ${serverName}?`
|
||||||
);
|
);
|
||||||
|
@ -44,6 +45,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
|
|
||||||
it('toggles when clicking cancel button', () => {
|
it('toggles when clicking cancel button', () => {
|
||||||
const cancelBtn = wrapper.find('button').first();
|
const cancelBtn = wrapper.find('button').first();
|
||||||
|
|
||||||
cancelBtn.simulate('click');
|
cancelBtn.simulate('click');
|
||||||
|
|
||||||
expect(toggleMock.callCount).toEqual(1);
|
expect(toggleMock.callCount).toEqual(1);
|
||||||
|
@ -53,6 +55,7 @@ describe('<DeleteServerModal />', () => {
|
||||||
|
|
||||||
it('deletes server when clicking accept button', () => {
|
it('deletes server when clicking accept button', () => {
|
||||||
const acceptBtn = wrapper.find('button').last();
|
const acceptBtn = wrapper.find('button').last();
|
||||||
|
|
||||||
acceptBtn.simulate('click');
|
acceptBtn.simulate('click');
|
||||||
|
|
||||||
expect(toggleMock.callCount).toEqual(1);
|
expect(toggleMock.callCount).toEqual(1);
|
||||||
|
|
|
@ -1,34 +1,37 @@
|
||||||
import { identity } from 'ramda';
|
import { identity, values } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ServersDropdown } from '../../src/servers/ServersDropdown';
|
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
import { DropdownItem, DropdownToggle } from 'reactstrap';
|
||||||
|
import { ServersDropdownComponent } from '../../src/servers/ServersDropdown';
|
||||||
|
|
||||||
describe('<ServersDropdown />', () => {
|
describe('<ServersDropdown />', () => {
|
||||||
let wrapped;
|
let wrapped;
|
||||||
const servers = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }, { name: 'baz', id: 3 }];
|
const servers = {
|
||||||
|
'1a': { name: 'foo', id: 1 },
|
||||||
|
'2b': { name: 'bar', id: 2 },
|
||||||
|
'3c': { name: 'baz', id: 3 },
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapped = shallow(<ServersDropdown servers={servers} listServers={identity} />);
|
wrapped = shallow(<ServersDropdownComponent servers={servers} listServers={identity} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapped.unmount());
|
afterEach(() => wrapped.unmount());
|
||||||
|
|
||||||
it('contains the list of servers', () =>
|
it('contains the list of servers', () =>
|
||||||
expect(wrapped.find(DropdownItem).filter('[to]')).toHaveLength(servers.length)
|
expect(wrapped.find(DropdownItem).filter('[to]')).toHaveLength(values(servers).length));
|
||||||
);
|
|
||||||
|
|
||||||
it('contains a toggle with proper title', () =>
|
it('contains a toggle with proper title', () =>
|
||||||
expect(wrapped.find(DropdownToggle)).toHaveLength(1)
|
expect(wrapped.find(DropdownToggle)).toHaveLength(1));
|
||||||
);
|
|
||||||
|
|
||||||
it('contains a button to export servers', () => {
|
it('contains a button to export servers', () => {
|
||||||
const items = wrapped.find(DropdownItem);
|
const items = wrapped.find(DropdownItem);
|
||||||
|
|
||||||
expect(items.filter('[divider]')).toHaveLength(1);
|
expect(items.filter('[divider]')).toHaveLength(1);
|
||||||
expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1);
|
expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a message when no servers exist yet', () => {
|
it('contains a message when no servers exist yet', () => {
|
||||||
wrapped = shallow(<ServersDropdown servers={[]} listServers={identity} />);
|
wrapped = shallow(<ServersDropdownComponent servers={{}} listServers={identity} />);
|
||||||
const item = wrapped.find(DropdownItem);
|
const item = wrapped.find(DropdownItem);
|
||||||
|
|
||||||
expect(item).toHaveLength(1);
|
expect(item).toHaveLength(1);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn';
|
|
||||||
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';
|
||||||
|
|
||||||
describe('<ImportServersBtn />', () => {
|
describe('<ImportServersBtn />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -12,7 +12,7 @@ describe('<ImportServersBtn />', () => {
|
||||||
importServersFromFile: sinon.fake.returns(Promise.resolve([])),
|
importServersFromFile: sinon.fake.returns(Promise.resolve([])),
|
||||||
};
|
};
|
||||||
const fileRef = {
|
const fileRef = {
|
||||||
current: { click: sinon.fake() }
|
current: { click: sinon.fake() },
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -22,11 +22,11 @@ describe('<ImportServersBtn />', () => {
|
||||||
fileRef.current.click.resetHistory();
|
fileRef.current.click.resetHistory();
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ImportServersBtn
|
<ImportServersBtnComponent
|
||||||
onImport={onImportMock}
|
|
||||||
createServers={createServersMock}
|
createServers={createServersMock}
|
||||||
serversImporter={serversImporterMock}
|
serversImporter={serversImporterMock}
|
||||||
fileRef={fileRef}
|
fileRef={fileRef}
|
||||||
|
onImport={onImportMock}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -40,14 +40,16 @@ describe('<ImportServersBtn />', () => {
|
||||||
|
|
||||||
it('triggers click on file ref when button is clicked', () => {
|
it('triggers click on file ref when button is clicked', () => {
|
||||||
const btn = wrapper.find('#importBtn');
|
const btn = wrapper.find('#importBtn');
|
||||||
|
|
||||||
btn.simulate('click');
|
btn.simulate('click');
|
||||||
|
|
||||||
expect(fileRef.current.click.callCount).toEqual(1);
|
expect(fileRef.current.click.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('imports servers when file input changes', done => {
|
it('imports servers when file input changes', (done) => {
|
||||||
const file = wrapper.find('.create-server__csv-select');
|
const file = wrapper.find('.create-server__csv-select');
|
||||||
file.simulate('change', { target: { files: [''] } });
|
|
||||||
|
file.simulate('change', { target: { files: [ '' ] } });
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(serversImporterMock.importServersFromFile.callCount).toEqual(1);
|
expect(serversImporterMock.importServersFromFile.callCount).toEqual(1);
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
|
import * as sinon from 'sinon';
|
||||||
import reduce, {
|
import reduce, {
|
||||||
_selectServer,
|
_selectServer,
|
||||||
RESET_SELECTED_SERVER,
|
RESET_SELECTED_SERVER,
|
||||||
resetSelectedServer,
|
resetSelectedServer,
|
||||||
SELECT_SERVER,
|
SELECT_SERVER,
|
||||||
} from '../../../src/servers/reducers/selectedServer';
|
} from '../../../src/servers/reducers/selectedServer';
|
||||||
import * as sinon from 'sinon';
|
|
||||||
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
|
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
|
||||||
|
|
||||||
describe('selectedServerReducer', () => {
|
describe('selectedServerReducer', () => {
|
||||||
describe('reduce', () => {
|
describe('reduce', () => {
|
||||||
it('returns default when action is not handled', () =>
|
it('returns default when action is not handled', () =>
|
||||||
expect(reduce(null, { type: 'unknown' })).toEqual(null)
|
expect(reduce(null, { type: 'unknown' })).toEqual(null));
|
||||||
);
|
|
||||||
|
|
||||||
it('returns default when action is RESET_SELECTED_SERVER', () =>
|
it('returns default when action is RESET_SELECTED_SERVER', () =>
|
||||||
expect(reduce(null, { type: RESET_SELECTED_SERVER })).toEqual(null)
|
expect(reduce(null, { type: RESET_SELECTED_SERVER })).toEqual(null));
|
||||||
);
|
|
||||||
|
|
||||||
it('returns selected server when action is SELECT_SERVER', () => {
|
it('returns selected server when action is SELECT_SERVER', () => {
|
||||||
const selectedServer = { id: 'abc123' };
|
const selectedServer = { id: 'abc123' };
|
||||||
|
|
||||||
expect(reduce(null, { type: SELECT_SERVER, selectedServer })).toEqual(selectedServer);
|
expect(reduce(null, { type: SELECT_SERVER, selectedServer })).toEqual(selectedServer);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -31,14 +30,14 @@ describe('selectedServerReducer', () => {
|
||||||
|
|
||||||
describe('selectServer', () => {
|
describe('selectServer', () => {
|
||||||
const ShlinkApiClientMock = {
|
const ShlinkApiClientMock = {
|
||||||
setConfig: sinon.spy()
|
setConfig: sinon.spy(),
|
||||||
};
|
};
|
||||||
const serverId = 'abc123';
|
const serverId = 'abc123';
|
||||||
const selectedServer = {
|
const selectedServer = {
|
||||||
id: serverId
|
id: serverId,
|
||||||
};
|
};
|
||||||
const ServersServiceMock = {
|
const ServersServiceMock = {
|
||||||
findServerById: sinon.fake.returns(selectedServer)
|
findServerById: sinon.fake.returns(selectedServer),
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -48,14 +47,15 @@ describe('selectedServerReducer', () => {
|
||||||
|
|
||||||
it('dispatches proper actions', () => {
|
it('dispatches proper actions', () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
const expectedDispatchCalls = 2;
|
||||||
|
|
||||||
_selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(dispatch);
|
_selectServer(ShlinkApiClientMock, ServersServiceMock, serverId)(dispatch);
|
||||||
|
|
||||||
expect(dispatch.callCount).toEqual(2);
|
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);
|
||||||
expect(dispatch.secondCall.calledWith({
|
expect(dispatch.secondCall.calledWith({
|
||||||
type: SELECT_SERVER,
|
type: SELECT_SERVER,
|
||||||
selectedServer
|
selectedServer,
|
||||||
})).toEqual(true);
|
})).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { values } from 'ramda';
|
||||||
import reduce, {
|
import reduce, {
|
||||||
_createServer,
|
_createServer,
|
||||||
_deleteServer,
|
_deleteServer,
|
||||||
|
@ -5,13 +7,11 @@ import reduce, {
|
||||||
_createServers,
|
_createServers,
|
||||||
FETCH_SERVERS,
|
FETCH_SERVERS,
|
||||||
} from '../../../src/servers/reducers/server';
|
} from '../../../src/servers/reducers/server';
|
||||||
import * as sinon from 'sinon';
|
|
||||||
import { values } from 'ramda';
|
|
||||||
|
|
||||||
describe('serverReducer', () => {
|
describe('serverReducer', () => {
|
||||||
const servers = {
|
const servers = {
|
||||||
abc123: { id: 'abc123' },
|
abc123: { id: 'abc123' },
|
||||||
def456: { id: 'def456' }
|
def456: { id: 'def456' },
|
||||||
};
|
};
|
||||||
const ServersServiceMock = {
|
const ServersServiceMock = {
|
||||||
listServers: sinon.fake.returns(servers),
|
listServers: sinon.fake.returns(servers),
|
||||||
|
@ -22,12 +22,10 @@ describe('serverReducer', () => {
|
||||||
|
|
||||||
describe('reduce', () => {
|
describe('reduce', () => {
|
||||||
it('returns servers when action is FETCH_SERVERS', () =>
|
it('returns servers when action is FETCH_SERVERS', () =>
|
||||||
expect(reduce({}, { type: FETCH_SERVERS, servers })).toEqual(servers)
|
expect(reduce({}, { type: FETCH_SERVERS, servers })).toEqual(servers));
|
||||||
);
|
|
||||||
|
|
||||||
it('returns default when action is unknown', () =>
|
it('returns default when action is unknown', () =>
|
||||||
expect(reduce({}, { type: 'unknown' })).toEqual({})
|
expect(reduce({}, { type: 'unknown' })).toEqual({}));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('action creators', () => {
|
describe('action creators', () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ServersExporter } from '../../../src/servers/services/ServersExporter';
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { ServersExporter } from '../../../src/servers/services/ServersExporter';
|
||||||
|
|
||||||
describe('ServersExporter', () => {
|
describe('ServersExporter', () => {
|
||||||
const createLinkMock = () => ({
|
const createLinkMock = () => ({
|
||||||
|
@ -17,7 +17,7 @@ describe('ServersExporter', () => {
|
||||||
appendChild: sinon.fake(),
|
appendChild: sinon.fake(),
|
||||||
removeChild: sinon.fake(),
|
removeChild: sinon.fake(),
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const serversServiceMock = {
|
const serversServiceMock = {
|
||||||
listServers: sinon.fake.returns({
|
listServers: sinon.fake.returns({
|
||||||
|
@ -41,11 +41,13 @@ describe('ServersExporter', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalConsole = global.console;
|
originalConsole = global.console;
|
||||||
global.console = { error: sinon.fake() };
|
global.console = { error: sinon.fake() };
|
||||||
global.Blob = function Blob() {};
|
global.Blob = class Blob {};
|
||||||
global.URL = { createObjectURL: () => '' };
|
global.URL = { createObjectURL: () => '' };
|
||||||
serversServiceMock.listServers.resetHistory();
|
serversServiceMock.listServers.resetHistory();
|
||||||
});
|
});
|
||||||
afterEach(() => global.console = originalConsole);
|
afterEach(() => {
|
||||||
|
global.console = originalConsole;
|
||||||
|
});
|
||||||
|
|
||||||
it('logs an error if something fails', () => {
|
it('logs an error if something fails', () => {
|
||||||
const csvjsonMock = createCsvjsonMock(true);
|
const csvjsonMock = createCsvjsonMock(true);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { ServersImporter } from '../../../src/servers/services/ServersImporter';
|
||||||
|
|
||||||
describe('ServersImporter', () => {
|
describe('ServersImporter', () => {
|
||||||
const servers = [{ name: 'foo' }, { name: 'bar' }];
|
const servers = [{ name: 'foo' }, { name: 'bar' }];
|
||||||
|
@ -29,10 +29,13 @@ describe('ServersImporter', () => {
|
||||||
|
|
||||||
it('reads file when a CSV is provided', async () => {
|
it('reads file when a CSV is provided', async () => {
|
||||||
const readAsText = sinon.fake.returns('');
|
const readAsText = sinon.fake.returns('');
|
||||||
global.FileReader = function FileReader() {
|
|
||||||
this.readAsText = readAsText;
|
global.FileReader = class FileReader {
|
||||||
this.addEventListener = (eventName, listener) =>
|
constructor() {
|
||||||
listener({ target: { result: '' } });
|
this.readAsText = readAsText;
|
||||||
|
this.addEventListener = (eventName, listener) =>
|
||||||
|
listener({ target: { result: '' } });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await importer.importServersFromFile({ type: 'text/csv' });
|
await importer.importServersFromFile({ type: 'text/csv' });
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { ServersService } from '../../../src/servers/services/ServersService';
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { last } from 'ramda';
|
import { last } from 'ramda';
|
||||||
|
import { ServersService } from '../../../src/servers/services/ServersService';
|
||||||
|
|
||||||
describe('ServersService', () => {
|
describe('ServersService', () => {
|
||||||
const servers = {
|
const servers = {
|
||||||
abc123: { id: 'abc123' },
|
abc123: { id: 'abc123' },
|
||||||
def456: { id: 'def456' },
|
def456: { id: 'def456' },
|
||||||
};
|
};
|
||||||
const createStorageMock = returnValue => ({
|
const createStorageMock = (returnValue) => ({
|
||||||
set: sinon.fake(),
|
set: sinon.fake(),
|
||||||
get: sinon.fake.returns(returnValue),
|
get: sinon.fake.returns(returnValue),
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,24 +9,20 @@ describe('shortUrlsListParamsReducer', () => {
|
||||||
const defaultState = { page: '1' };
|
const defaultState = { page: '1' };
|
||||||
|
|
||||||
it('returns default value when action is unknown', () =>
|
it('returns default value when action is unknown', () =>
|
||||||
expect(reduce(defaultState, { type: 'unknown' })).toEqual(defaultState)
|
expect(reduce(defaultState, { type: 'unknown' })).toEqual(defaultState));
|
||||||
);
|
|
||||||
|
|
||||||
it('returns params when action is LIST_SHORT_URLS', () =>
|
it('returns params when action is LIST_SHORT_URLS', () =>
|
||||||
expect(reduce(defaultState, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo' } })).toEqual({
|
expect(reduce(defaultState, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo' } })).toEqual({
|
||||||
...defaultState,
|
...defaultState,
|
||||||
searchTerm: 'foo'
|
searchTerm: 'foo',
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
it('returns default value when action is RESET_SHORT_URL_PARAMS', () =>
|
it('returns default value when action is RESET_SHORT_URL_PARAMS', () =>
|
||||||
expect(reduce(defaultState, { type: RESET_SHORT_URL_PARAMS })).toEqual(defaultState)
|
expect(reduce(defaultState, { type: RESET_SHORT_URL_PARAMS })).toEqual(defaultState));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetShortUrlParams', () => {
|
describe('resetShortUrlParams', () => {
|
||||||
it('returns proper action', () =>
|
it('returns proper action', () =>
|
||||||
expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS })
|
expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS }));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,9 +42,9 @@ describe('VisitsParser', () => {
|
||||||
describe('processOsStats', () => {
|
describe('processOsStats', () => {
|
||||||
it('properly parses OS stats', () => {
|
it('properly parses OS stats', () => {
|
||||||
expect(processOsStats(visits)).toEqual({
|
expect(processOsStats(visits)).toEqual({
|
||||||
'Linux': 3,
|
Linux: 3,
|
||||||
'Windows': 1,
|
Windows: 1,
|
||||||
'MacOS': 1,
|
MacOS: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -52,9 +52,9 @@ describe('VisitsParser', () => {
|
||||||
describe('processBrowserStats', () => {
|
describe('processBrowserStats', () => {
|
||||||
it('properly parses browser stats', () => {
|
it('properly parses browser stats', () => {
|
||||||
expect(processBrowserStats(visits)).toEqual({
|
expect(processBrowserStats(visits)).toEqual({
|
||||||
'Firefox': 2,
|
Firefox: 2,
|
||||||
'Chrome': 2,
|
Chrome: 2,
|
||||||
'Opera': 1,
|
Opera: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
332
yarn.lock
332
yarn.lock
|
@ -198,24 +198,24 @@ acorn-globals@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^4.0.4"
|
acorn "^4.0.4"
|
||||||
|
|
||||||
acorn-jsx@^3.0.0:
|
acorn-jsx@^4.1.1:
|
||||||
version "3.0.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e"
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^3.0.4"
|
acorn "^5.0.3"
|
||||||
|
|
||||||
acorn@^3.0.4:
|
|
||||||
version "3.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
|
||||||
|
|
||||||
acorn@^4.0.3, acorn@^4.0.4:
|
acorn@^4.0.3, acorn@^4.0.4:
|
||||||
version "4.0.13"
|
version "4.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
|
||||||
|
|
||||||
acorn@^5.0.0, acorn@^5.5.0:
|
acorn@^5.0.0:
|
||||||
version "5.7.1"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
|
||||||
|
|
||||||
|
acorn@^5.0.3, acorn@^5.6.0:
|
||||||
|
version "5.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.2.tgz#91fa871883485d06708800318404e72bfb26dcc5"
|
||||||
|
|
||||||
address@1.0.3, address@^1.0.1:
|
address@1.0.3, address@^1.0.1:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
|
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
|
||||||
|
@ -232,7 +232,7 @@ ajv-keywords@^3.0.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
|
||||||
|
|
||||||
ajv@6.5.3:
|
ajv@6.5.3, ajv@^6.5.0:
|
||||||
version "6.5.3"
|
version "6.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -248,7 +248,7 @@ ajv@^4.9.1:
|
||||||
co "^4.6.0"
|
co "^4.6.0"
|
||||||
json-stable-stringify "^1.0.1"
|
json-stable-stringify "^1.0.1"
|
||||||
|
|
||||||
ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.0:
|
ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5:
|
||||||
version "5.5.2"
|
version "5.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -363,13 +363,6 @@ argparse@^1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js "~1.0.2"
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
aria-query@^0.7.0:
|
|
||||||
version "0.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-0.7.1.tgz#26cbb5aff64144b0a825be1846e0b16cfa00b11e"
|
|
||||||
dependencies:
|
|
||||||
ast-types-flow "0.0.7"
|
|
||||||
commander "^2.11.0"
|
|
||||||
|
|
||||||
arr-diff@^2.0.0:
|
arr-diff@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
|
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
|
||||||
|
@ -481,10 +474,6 @@ assign-symbols@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
|
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
|
||||||
|
|
||||||
ast-types-flow@0.0.7:
|
|
||||||
version "0.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
|
|
||||||
|
|
||||||
async-each@^1.0.0:
|
async-each@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
|
||||||
|
@ -563,12 +552,6 @@ axios@^0.18.0:
|
||||||
follow-redirects "^1.3.0"
|
follow-redirects "^1.3.0"
|
||||||
is-buffer "^1.1.5"
|
is-buffer "^1.1.5"
|
||||||
|
|
||||||
axobject-query@^0.1.0:
|
|
||||||
version "0.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-0.1.0.tgz#62f59dbc59c9f9242759ca349960e7a2fe3c36c0"
|
|
||||||
dependencies:
|
|
||||||
ast-types-flow "0.0.7"
|
|
||||||
|
|
||||||
babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||||
version "6.26.0"
|
version "6.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||||
|
@ -764,9 +747,9 @@ babel-jest@20.0.3, babel-jest@^20.0.3:
|
||||||
babel-plugin-istanbul "^4.0.0"
|
babel-plugin-istanbul "^4.0.0"
|
||||||
babel-preset-jest "^20.0.3"
|
babel-preset-jest "^20.0.3"
|
||||||
|
|
||||||
babel-loader@7.1.2:
|
babel-loader@^7.1.2:
|
||||||
version "7.1.2"
|
version "7.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68"
|
||||||
dependencies:
|
dependencies:
|
||||||
find-cache-dir "^1.0.0"
|
find-cache-dir "^1.0.0"
|
||||||
loader-utils "^1.0.2"
|
loader-utils "^1.0.2"
|
||||||
|
@ -1468,10 +1451,6 @@ bser@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-int64 "^0.4.0"
|
node-int64 "^0.4.0"
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
|
||||||
|
|
||||||
buffer-indexof@^1.0.0:
|
buffer-indexof@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
|
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
|
||||||
|
@ -1488,7 +1467,7 @@ buffer@^4.3.0:
|
||||||
ieee754 "^1.1.4"
|
ieee754 "^1.1.4"
|
||||||
isarray "^1.0.0"
|
isarray "^1.0.0"
|
||||||
|
|
||||||
builtin-modules@^1.0.0, builtin-modules@^1.1.1:
|
builtin-modules@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
|
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
|
||||||
|
|
||||||
|
@ -1901,10 +1880,6 @@ commander@2.16.x, commander@~2.16.0:
|
||||||
version "2.16.0"
|
version "2.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
|
||||||
|
|
||||||
commander@^2.11.0:
|
|
||||||
version "2.17.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.0.tgz#9d07b25e2a6f198b76d8b756a0e8a9604a6a1a60"
|
|
||||||
|
|
||||||
commander@^2.9.0:
|
commander@^2.9.0:
|
||||||
version "2.17.1"
|
version "2.17.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
|
||||||
|
@ -1943,15 +1918,6 @@ concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
|
||||||
concat-stream@^1.6.0:
|
|
||||||
version "1.6.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
|
|
||||||
dependencies:
|
|
||||||
buffer-from "^1.0.0"
|
|
||||||
inherits "^2.0.3"
|
|
||||||
readable-stream "^2.2.2"
|
|
||||||
typedarray "^0.0.6"
|
|
||||||
|
|
||||||
configstore@^3.0.0:
|
configstore@^3.0.0:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
|
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
|
||||||
|
@ -2085,7 +2051,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
|
cross-spawn@5.1.0, cross-spawn@^5.0.1:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2100,6 +2066,16 @@ cross-spawn@^3.0.0:
|
||||||
lru-cache "^4.0.1"
|
lru-cache "^4.0.1"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
|
cross-spawn@^6.0.5:
|
||||||
|
version "6.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||||
|
dependencies:
|
||||||
|
nice-try "^1.0.4"
|
||||||
|
path-key "^2.0.1"
|
||||||
|
semver "^5.5.0"
|
||||||
|
shebang-command "^1.2.0"
|
||||||
|
which "^1.2.9"
|
||||||
|
|
||||||
cryptiles@2.x.x:
|
cryptiles@2.x.x:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
|
||||||
|
@ -2248,10 +2224,6 @@ d@1:
|
||||||
dependencies:
|
dependencies:
|
||||||
es5-ext "^0.10.9"
|
es5-ext "^0.10.9"
|
||||||
|
|
||||||
damerau-levenshtein@^1.0.0:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
|
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -2268,7 +2240,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.
|
||||||
dependencies:
|
dependencies:
|
||||||
ms "2.0.0"
|
ms "2.0.0"
|
||||||
|
|
||||||
debug@^3.0.0, debug@^3.0.1, debug@^3.1.0:
|
debug@^3.0.0, debug@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2459,7 +2431,7 @@ doctrine@1.5.0:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
isarray "^1.0.0"
|
isarray "^1.0.0"
|
||||||
|
|
||||||
doctrine@^2.0.0:
|
doctrine@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2585,10 +2557,6 @@ elliptic@^6.0.0:
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.0"
|
||||||
|
|
||||||
emoji-regex@^6.1.0:
|
|
||||||
version "6.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
|
|
||||||
|
|
||||||
emojis-list@^2.0.0:
|
emojis-list@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||||
|
@ -2771,9 +2739,33 @@ escope@^3.6.0:
|
||||||
esrecurse "^4.1.0"
|
esrecurse "^4.1.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-config-react-app@^2.1.0:
|
eslint-config-adidas-babel@^1.0.1:
|
||||||
version "2.1.0"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.1.0.tgz#23c909f71cbaff76b945b831d2d814b8bde169eb"
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-babel/-/eslint-config-adidas-babel-1.0.1.tgz#21389403f9bef47d6ad0eaa3e6f9b28c20075f88"
|
||||||
|
|
||||||
|
eslint-config-adidas-env@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-env/-/eslint-config-adidas-env-1.0.1.tgz#75f59460410ad60777f9fd8bdffb066502cee801"
|
||||||
|
|
||||||
|
eslint-config-adidas-es5@~1.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-es5/-/eslint-config-adidas-es5-1.0.1.tgz#2b6b1b0f36dd90a18762d97b1ea44ce9bc65fc5b"
|
||||||
|
|
||||||
|
eslint-config-adidas-es6@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-es6/-/eslint-config-adidas-es6-1.0.1.tgz#3695648792124f3e7de30b33f3c84edcc22c5b79"
|
||||||
|
dependencies:
|
||||||
|
eslint-config-adidas-es5 "~1.0"
|
||||||
|
|
||||||
|
eslint-config-adidas-jsx@~1.0:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-jsx/-/eslint-config-adidas-jsx-1.0.1.tgz#3fd5ec53b19e4eecf0a1dab10fe209ca4f6b816c"
|
||||||
|
|
||||||
|
eslint-config-adidas-react@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-adidas-react/-/eslint-config-adidas-react-1.0.1.tgz#accb5537c98ee0f71ed77480f66abe18c3c0db52"
|
||||||
|
dependencies:
|
||||||
|
eslint-config-adidas-jsx "~1.0"
|
||||||
|
|
||||||
eslint-import-resolver-node@^0.3.1:
|
eslint-import-resolver-node@^0.3.1:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
|
@ -2792,110 +2784,110 @@ eslint-loader@1.9.0:
|
||||||
object-hash "^1.1.4"
|
object-hash "^1.1.4"
|
||||||
rimraf "^2.6.1"
|
rimraf "^2.6.1"
|
||||||
|
|
||||||
eslint-module-utils@^2.1.1:
|
eslint-module-utils@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
|
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^2.6.8"
|
debug "^2.6.8"
|
||||||
pkg-dir "^1.0.0"
|
pkg-dir "^1.0.0"
|
||||||
|
|
||||||
eslint-plugin-flowtype@2.39.1:
|
eslint-plugin-import@^2.8.0:
|
||||||
version "2.39.1"
|
version "2.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.39.1.tgz#b5624622a0388bcd969f4351131232dcb9649cd5"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8"
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.15.0"
|
|
||||||
|
|
||||||
eslint-plugin-import@2.8.0:
|
|
||||||
version "2.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894"
|
|
||||||
dependencies:
|
|
||||||
builtin-modules "^1.1.1"
|
|
||||||
contains-path "^0.1.0"
|
contains-path "^0.1.0"
|
||||||
debug "^2.6.8"
|
debug "^2.6.8"
|
||||||
doctrine "1.5.0"
|
doctrine "1.5.0"
|
||||||
eslint-import-resolver-node "^0.3.1"
|
eslint-import-resolver-node "^0.3.1"
|
||||||
eslint-module-utils "^2.1.1"
|
eslint-module-utils "^2.2.0"
|
||||||
has "^1.0.1"
|
has "^1.0.1"
|
||||||
lodash.cond "^4.3.0"
|
lodash "^4.17.4"
|
||||||
minimatch "^3.0.3"
|
minimatch "^3.0.3"
|
||||||
read-pkg-up "^2.0.0"
|
read-pkg-up "^2.0.0"
|
||||||
|
resolve "^1.6.0"
|
||||||
|
|
||||||
eslint-plugin-jsx-a11y@5.1.1:
|
eslint-plugin-jest@^21.22.0:
|
||||||
version "5.1.1"
|
version "21.22.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz#5c96bb5186ca14e94db1095ff59b3e2bd94069b1"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.22.0.tgz#1b9e49b3e5ce9a3d0a51af4579991d517f33726e"
|
||||||
|
|
||||||
|
eslint-plugin-promise@^3.0.0:
|
||||||
|
version "3.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz#65ebf27a845e3c1e9d6f6a5622ddd3801694b621"
|
||||||
|
|
||||||
|
eslint-plugin-react@^7.4.0:
|
||||||
|
version "7.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz#c01a7af6f17519457d6116aa94fc6d2ccad5443c"
|
||||||
dependencies:
|
dependencies:
|
||||||
aria-query "^0.7.0"
|
|
||||||
array-includes "^3.0.3"
|
array-includes "^3.0.3"
|
||||||
ast-types-flow "0.0.7"
|
doctrine "^2.1.0"
|
||||||
axobject-query "^0.1.0"
|
has "^1.0.3"
|
||||||
damerau-levenshtein "^1.0.0"
|
jsx-ast-utils "^2.0.1"
|
||||||
emoji-regex "^6.1.0"
|
prop-types "^15.6.2"
|
||||||
jsx-ast-utils "^1.4.0"
|
|
||||||
|
|
||||||
eslint-plugin-react@7.4.0:
|
eslint-scope@^4.0.0:
|
||||||
version "7.4.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
|
||||||
dependencies:
|
|
||||||
doctrine "^2.0.0"
|
|
||||||
has "^1.0.1"
|
|
||||||
jsx-ast-utils "^2.0.0"
|
|
||||||
prop-types "^15.5.10"
|
|
||||||
|
|
||||||
eslint-scope@^3.7.1:
|
|
||||||
version "3.7.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535"
|
|
||||||
dependencies:
|
dependencies:
|
||||||
esrecurse "^4.1.0"
|
esrecurse "^4.1.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint@4.10.0:
|
eslint-utils@^1.3.1:
|
||||||
version "4.10.0"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.10.0.tgz#f25d0d7955c81968c2309aa5c9a229e045176bb7"
|
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
|
||||||
|
|
||||||
|
eslint-visitor-keys@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
||||||
|
|
||||||
|
eslint@^5.4.0:
|
||||||
|
version "5.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.4.0.tgz#d068ec03006bb9e06b429dc85f7e46c1b69fac62"
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "^5.2.0"
|
ajv "^6.5.0"
|
||||||
babel-code-frame "^6.22.0"
|
babel-code-frame "^6.26.0"
|
||||||
chalk "^2.1.0"
|
chalk "^2.1.0"
|
||||||
concat-stream "^1.6.0"
|
cross-spawn "^6.0.5"
|
||||||
cross-spawn "^5.1.0"
|
debug "^3.1.0"
|
||||||
debug "^3.0.1"
|
doctrine "^2.1.0"
|
||||||
doctrine "^2.0.0"
|
eslint-scope "^4.0.0"
|
||||||
eslint-scope "^3.7.1"
|
eslint-utils "^1.3.1"
|
||||||
espree "^3.5.1"
|
eslint-visitor-keys "^1.0.0"
|
||||||
esquery "^1.0.0"
|
espree "^4.0.0"
|
||||||
estraverse "^4.2.0"
|
esquery "^1.0.1"
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
file-entry-cache "^2.0.0"
|
file-entry-cache "^2.0.0"
|
||||||
functional-red-black-tree "^1.0.1"
|
functional-red-black-tree "^1.0.1"
|
||||||
glob "^7.1.2"
|
glob "^7.1.2"
|
||||||
globals "^9.17.0"
|
globals "^11.7.0"
|
||||||
ignore "^3.3.3"
|
ignore "^4.0.2"
|
||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
inquirer "^3.0.6"
|
inquirer "^5.2.0"
|
||||||
is-resolvable "^1.0.0"
|
is-resolvable "^1.1.0"
|
||||||
js-yaml "^3.9.1"
|
js-yaml "^3.11.0"
|
||||||
json-stable-stringify "^1.0.1"
|
json-stable-stringify-without-jsonify "^1.0.1"
|
||||||
levn "^0.3.0"
|
levn "^0.3.0"
|
||||||
lodash "^4.17.4"
|
lodash "^4.17.5"
|
||||||
minimatch "^3.0.2"
|
minimatch "^3.0.4"
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
optionator "^0.8.2"
|
optionator "^0.8.2"
|
||||||
path-is-inside "^1.0.2"
|
path-is-inside "^1.0.2"
|
||||||
pluralize "^7.0.0"
|
pluralize "^7.0.0"
|
||||||
progress "^2.0.0"
|
progress "^2.0.0"
|
||||||
|
regexpp "^2.0.0"
|
||||||
require-uncached "^1.0.3"
|
require-uncached "^1.0.3"
|
||||||
semver "^5.3.0"
|
semver "^5.5.0"
|
||||||
strip-ansi "^4.0.0"
|
strip-ansi "^4.0.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "^2.0.1"
|
||||||
table "^4.0.1"
|
table "^4.0.3"
|
||||||
text-table "~0.2.0"
|
text-table "^0.2.0"
|
||||||
|
|
||||||
espree@^3.5.1:
|
espree@^4.0.0:
|
||||||
version "3.5.4"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634"
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^5.5.0"
|
acorn "^5.6.0"
|
||||||
acorn-jsx "^3.0.0"
|
acorn-jsx "^4.1.1"
|
||||||
|
|
||||||
esprima@^2.6.0:
|
esprima@^2.6.0:
|
||||||
version "2.7.3"
|
version "2.7.3"
|
||||||
|
@ -2909,7 +2901,7 @@ esprima@^4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
|
|
||||||
esquery@^1.0.0:
|
esquery@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3079,7 +3071,7 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
|
|
||||||
external-editor@^2.0.4:
|
external-editor@^2.0.4, external-editor@^2.1.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
|
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3546,11 +3538,11 @@ global-prefix@^1.0.1:
|
||||||
is-windows "^1.0.1"
|
is-windows "^1.0.1"
|
||||||
which "^1.2.14"
|
which "^1.2.14"
|
||||||
|
|
||||||
globals@^11.1.0:
|
globals@^11.1.0, globals@^11.7.0:
|
||||||
version "11.7.0"
|
version "11.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
|
||||||
|
|
||||||
globals@^9.17.0, globals@^9.18.0:
|
globals@^9.18.0:
|
||||||
version "9.18.0"
|
version "9.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
|
||||||
|
|
||||||
|
@ -3724,7 +3716,7 @@ has-values@^1.0.0:
|
||||||
is-number "^3.0.0"
|
is-number "^3.0.0"
|
||||||
kind-of "^4.0.0"
|
kind-of "^4.0.0"
|
||||||
|
|
||||||
has@^1.0.1:
|
has@^1.0.1, has@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3967,11 +3959,11 @@ ignore-walk@^3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
|
|
||||||
ignore@^3.3.3, ignore@^3.3.5:
|
ignore@^3.3.5:
|
||||||
version "3.3.10"
|
version "3.3.10"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
|
||||||
|
|
||||||
ignore@^4.0.0:
|
ignore@^4.0.0, ignore@^4.0.2:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||||
|
|
||||||
|
@ -4035,7 +4027,7 @@ ini@^1.3.4, ini@~1.3.0:
|
||||||
version "1.3.5"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
|
|
||||||
inquirer@3.3.0, inquirer@^3.0.6:
|
inquirer@3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4054,6 +4046,24 @@ inquirer@3.3.0, inquirer@^3.0.6:
|
||||||
strip-ansi "^4.0.0"
|
strip-ansi "^4.0.0"
|
||||||
through "^2.3.6"
|
through "^2.3.6"
|
||||||
|
|
||||||
|
inquirer@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
|
||||||
|
dependencies:
|
||||||
|
ansi-escapes "^3.0.0"
|
||||||
|
chalk "^2.0.0"
|
||||||
|
cli-cursor "^2.1.0"
|
||||||
|
cli-width "^2.0.0"
|
||||||
|
external-editor "^2.1.0"
|
||||||
|
figures "^2.0.0"
|
||||||
|
lodash "^4.3.0"
|
||||||
|
mute-stream "0.0.7"
|
||||||
|
run-async "^2.2.0"
|
||||||
|
rxjs "^5.5.2"
|
||||||
|
string-width "^2.1.0"
|
||||||
|
strip-ansi "^4.0.0"
|
||||||
|
through "^2.3.6"
|
||||||
|
|
||||||
internal-ip@1.2.0:
|
internal-ip@1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
|
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
|
||||||
|
@ -4340,7 +4350,7 @@ is-regexp@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
|
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
|
||||||
|
|
||||||
is-resolvable@^1.0.0:
|
is-resolvable@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
||||||
|
|
||||||
|
@ -4732,7 +4742,7 @@ js-tokens@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||||
|
|
||||||
js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1:
|
js-yaml@^3.11.0, js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.0:
|
||||||
version "3.12.0"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4806,6 +4816,10 @@ json-schema@0.2.3:
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||||
|
|
||||||
|
json-stable-stringify-without-jsonify@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||||
|
|
||||||
json-stable-stringify@^1.0.1:
|
json-stable-stringify@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
|
||||||
|
@ -4849,11 +4863,7 @@ jsprim@^1.2.2:
|
||||||
json-schema "0.2.3"
|
json-schema "0.2.3"
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
jsx-ast-utils@^1.4.0:
|
jsx-ast-utils@^2.0.1:
|
||||||
version "1.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
|
|
||||||
|
|
||||||
jsx-ast-utils@^2.0.0:
|
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
|
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5007,10 +5017,6 @@ lodash.clonedeep@^4.3.2:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||||
|
|
||||||
lodash.cond@^4.3.0:
|
|
||||||
version "4.5.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
|
|
||||||
|
|
||||||
lodash.debounce@^4.0.8:
|
lodash.debounce@^4.0.8:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
|
@ -5483,6 +5489,10 @@ next-tick@1:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||||
|
|
||||||
|
nice-try@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
|
||||||
|
|
||||||
nise@^1.4.2:
|
nise@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.2.tgz#a9a3800e3994994af9e452333d549d60f72b8e8c"
|
resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.2.tgz#a9a3800e3994994af9e452333d549d60f72b8e8c"
|
||||||
|
@ -6022,7 +6032,7 @@ path-is-inside@1.0.2, path-is-inside@^1.0.1, path-is-inside@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||||
|
|
||||||
path-key@^2.0.0:
|
path-key@^2.0.0, path-key@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||||
|
|
||||||
|
@ -6996,7 +7006,7 @@ readable-stream@1.0:
|
||||||
isarray "0.0.1"
|
isarray "0.0.1"
|
||||||
string_decoder "~0.10.x"
|
string_decoder "~0.10.x"
|
||||||
|
|
||||||
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6:
|
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6:
|
||||||
version "2.3.6"
|
version "2.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7091,6 +7101,10 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
||||||
extend-shallow "^3.0.2"
|
extend-shallow "^3.0.2"
|
||||||
safe-regex "^1.1.0"
|
safe-regex "^1.1.0"
|
||||||
|
|
||||||
|
regexpp@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.0.tgz#b2a7534a85ca1b033bcf5ce9ff8e56d4e0755365"
|
||||||
|
|
||||||
regexpu-core@^1.0.0:
|
regexpu-core@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
||||||
|
@ -7331,7 +7345,7 @@ resolve@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-parse "^1.0.5"
|
path-parse "^1.0.5"
|
||||||
|
|
||||||
resolve@^1.3.2, resolve@^1.5.0:
|
resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7390,6 +7404,12 @@ rx-lite@*, rx-lite@^4.0.8:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
||||||
|
|
||||||
|
rxjs@^5.5.2:
|
||||||
|
version "5.5.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87"
|
||||||
|
dependencies:
|
||||||
|
symbol-observable "1.0.1"
|
||||||
|
|
||||||
safe-buffer@5.1.1:
|
safe-buffer@5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||||
|
@ -7954,7 +7974,7 @@ strip-indent@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
|
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
|
||||||
|
|
||||||
strip-json-comments@~2.0.1:
|
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
|
|
||||||
|
@ -8142,6 +8162,10 @@ sw-toolbox@^3.4.0:
|
||||||
path-to-regexp "^1.0.1"
|
path-to-regexp "^1.0.1"
|
||||||
serviceworker-cache-polyfill "^4.0.0"
|
serviceworker-cache-polyfill "^4.0.0"
|
||||||
|
|
||||||
|
symbol-observable@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
||||||
|
|
||||||
symbol-observable@^1.2.0:
|
symbol-observable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
@ -8150,7 +8174,7 @@ symbol-tree@^3.2.1:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
|
||||||
|
|
||||||
table@^4.0.1:
|
table@^4.0.1, table@^4.0.3:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
|
resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8205,7 +8229,7 @@ text-encoding@^0.6.4:
|
||||||
version "0.6.4"
|
version "0.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
|
resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
|
||||||
|
|
||||||
text-table@0.2.0, text-table@~0.2.0:
|
text-table@0.2.0, text-table@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
|
|
||||||
|
@ -8369,10 +8393,6 @@ type-is@~1.6.15, type-is@~1.6.16:
|
||||||
media-typer "0.3.0"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.18"
|
mime-types "~2.1.18"
|
||||||
|
|
||||||
typedarray@^0.0.6:
|
|
||||||
version "0.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
|
||||||
|
|
||||||
ua-parser-js@^0.7.18:
|
ua-parser-js@^0.7.18:
|
||||||
version "0.7.18"
|
version "0.7.18"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||||
|
|
Loading…
Reference in a new issue