diff --git a/package.json b/package.json index c7c253ed..b059bcf0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "private": false, "scripts": { "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:fix": "yarn lint:css --fix", "start": "node scripts/start.js", @@ -51,7 +52,7 @@ "babel-core": "6.26.0", "babel-eslint": "7.2.3", "babel-jest": "20.0.3", - "babel-loader": "7.1.2", + "babel-loader": "^7.1.2", "babel-preset-react-app": "^3.1.1", "babel-runtime": "6.26.0", "case-sensitive-paths-webpack-plugin": "2.1.1", @@ -61,13 +62,16 @@ "dotenv-expand": "4.2.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", - "eslint": "4.10.0", - "eslint-config-react-app": "^2.1.0", + "eslint": "^5.4.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-plugin-flowtype": "2.39.1", - "eslint-plugin-import": "2.8.0", - "eslint-plugin-jsx-a11y": "5.1.1", - "eslint-plugin-react": "7.4.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jest": "^21.22.0", + "eslint-plugin-promise": "^3.0.0", + "eslint-plugin-react": "^7.4.0", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "1.1.5", "fs-extra": "3.0.1", @@ -138,6 +142,37 @@ ] }, "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" + } } } diff --git a/src/App.js b/src/App.js index 75832867..f7203923 100644 --- a/src/App.js +++ b/src/App.js @@ -6,20 +6,18 @@ import MainHeader from './common/MainHeader'; import MenuLayout from './common/MenuLayout'; import CreateServer from './servers/CreateServer'; -export default class App extends React.Component { - render() { - return ( -
- +export default function App() { + return ( +
+ -
- - - - - -
+
+ + + + +
- ); - } +
+ ); } diff --git a/src/api/ShlinkApiClient.js b/src/api/ShlinkApiClient.js index fea32406..9355ac81 100644 --- a/src/api/ShlinkApiClient.js +++ b/src/api/ShlinkApiClient.js @@ -3,6 +3,7 @@ import qs from 'qs'; import { isEmpty, isNil, reject } from 'ramda'; const API_VERSION = '1'; +const STATUS_UNAUTHORIZED = 401; export class ShlinkApiClient { constructor(axios) { @@ -14,8 +15,6 @@ export class ShlinkApiClient { /** * Sets the base URL to be used on any request - * @param {String} baseUrl - * @param {String} apiKey */ setConfig = ({ url, apiKey }) => { this._baseUrl = `${url}/rest/v${API_VERSION}`; @@ -24,45 +23,46 @@ export class ShlinkApiClient { listShortUrls = (options = {}) => this._performRequest('/short-codes', 'GET', options) - .then(resp => resp.data.shortUrls) - .catch(e => this._handleAuthError(e, this.listShortUrls, [options])); + .then((resp) => resp.data.shortUrls) + .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) - .then(resp => resp.data) - .catch(e => this._handleAuthError(e, this.createShortUrl, [filteredOptions])); + .then((resp) => resp.data) + .catch((e) => this._handleAuthError(e, this.createShortUrl, [ filteredOptions ])); }; getShortUrlVisits = (shortCode, dates) => this._performRequest(`/short-codes/${shortCode}/visits`, 'GET', dates) - .then(resp => resp.data.visits.data) - .catch(e => this._handleAuthError(e, this.getShortUrlVisits, [shortCode, dates])); + .then((resp) => resp.data.visits.data) + .catch((e) => this._handleAuthError(e, this.getShortUrlVisits, [ shortCode, dates ])); - getShortUrl = shortCode => + getShortUrl = (shortCode) => this._performRequest(`/short-codes/${shortCode}`, 'GET') - .then(resp => resp.data) - .catch(e => this._handleAuthError(e, this.getShortUrl, [shortCode])); + .then((resp) => resp.data) + .catch((e) => this._handleAuthError(e, this.getShortUrl, [ shortCode ])); updateShortUrlTags = (shortCode, tags) => this._performRequest(`/short-codes/${shortCode}/tags`, 'PUT', {}, { tags }) - .then(resp => resp.data.tags) - .catch(e => this._handleAuthError(e, this.updateShortUrlTags, [shortCode, tags])); + .then((resp) => resp.data.tags) + .catch((e) => this._handleAuthError(e, this.updateShortUrlTags, [ shortCode, tags ])); listTags = () => this._performRequest('/tags', 'GET') - .then(resp => resp.data.tags.data) - .catch(e => this._handleAuthError(e, this.listTags, [])); + .then((resp) => resp.data.tags.data) + .catch((e) => this._handleAuthError(e, this.listTags, [])); - deleteTags = tags => + deleteTags = (tags) => this._performRequest('/tags', 'DELETE', { tags }) .then(() => ({ tags })) - .catch(e => this._handleAuthError(e, this.deleteTags, [tags])); + .catch((e) => this._handleAuthError(e, this.deleteTags, [ tags ])); editTag = (oldName, newName) => this._performRequest('/tags', 'PUT', {}, { 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 = {}) => { if (isEmpty(this._token)) { @@ -72,14 +72,16 @@ export class ShlinkApiClient { return await this.axios({ method, url: `${this._baseUrl}${url}`, - headers: { 'Authorization': `Bearer ${this._token}` }, + headers: { Authorization: `Bearer ${this._token}` }, params: query, data: body, - paramsSerializer: params => qs.stringify(params, { arrayFormat: 'brackets' }) - }).then(resp => { + paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }), + }).then((resp) => { // Save new token const { authorization = '' } = resp.headers; + this._token = authorization.substr('Bearer '.length); + return resp; }); }; @@ -88,15 +90,17 @@ export class ShlinkApiClient { const resp = await this.axios({ method: 'POST', url: `${this._baseUrl}/authenticate`, - data: { apiKey: this._apiKey } + data: { apiKey: this._apiKey }, }); + return resp.data.token; }; _handleAuthError = (e, method, args) => { // 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 = ''; + return method(...args); } @@ -105,4 +109,6 @@ export class ShlinkApiClient { }; } -export default new ShlinkApiClient(axios); +const shlinkApiClient = new ShlinkApiClient(axios); + +export default shlinkApiClient; diff --git a/src/common/AsideMenu.js b/src/common/AsideMenu.js index bf2f0a32..ace7c5c0 100644 --- a/src/common/AsideMenu.js +++ b/src/common/AsideMenu.js @@ -4,11 +4,11 @@ import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; import { NavLink } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; import DeleteServerButton from '../servers/DeleteServerButton'; import './AsideMenu.scss'; -import PropTypes from 'prop-types'; import { serverType } from '../servers/prop-types'; -import classnames from 'classnames'; const defaultProps = { className: '', diff --git a/src/common/Home.js b/src/common/Home.js index 8efcd317..3aa66f89 100644 --- a/src/common/Home.js +++ b/src/common/Home.js @@ -1,14 +1,20 @@ -import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight' -import FontAwesomeIcon from '@fortawesome/react-fontawesome' -import { isEmpty, pick, values } from 'ramda' -import React from 'react' -import { connect } from 'react-redux' -import { Link } from 'react-router-dom' -import { ListGroup, ListGroupItem } from 'reactstrap' -import { resetSelectedServer } from '../servers/reducers/selectedServer' -import './Home.scss' +import chevronIcon from '@fortawesome/fontawesome-free-solid/faChevronRight'; +import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import { isEmpty, pick, values } from 'ramda'; +import React from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { ListGroup, ListGroupItem } from 'reactstrap'; +import PropTypes from 'prop-types'; +import { resetSelectedServer } from '../servers/reducers/selectedServer'; +import './Home.scss'; -export class Home extends React.Component { +const propTypes = { + resetSelectedServer: PropTypes.func, + servers: PropTypes.object, +}; + +export class HomeComponent extends React.Component { componentDidMount() { 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; diff --git a/src/common/MainHeader.js b/src/common/MainHeader.js index 54261623..87b6d2ef 100644 --- a/src/common/MainHeader.js +++ b/src/common/MainHeader.js @@ -2,18 +2,23 @@ import plusIcon from '@fortawesome/fontawesome-free-solid/faPlus'; import arrowIcon from '@fortawesome/fontawesome-free-solid/faChevronDown'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; -import { Link, withRouter } from 'react-router-dom' +import { Link, withRouter } from 'react-router-dom'; import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; import ServersDropdown from '../servers/ServersDropdown'; import './MainHeader.scss'; import shlinkLogo from './shlink-logo-white.png'; -import classnames from 'classnames'; -export class MainHeader extends React.Component { +const propTypes = { + location: PropTypes.object, +}; + +export class MainHeaderComponent extends React.Component { state = { isOpen: false }; - toggle = () => { + handleToggle = () => { this.setState(({ isOpen }) => ({ - isOpen: !isOpen + isOpen: !isOpen, })); }; @@ -33,10 +38,10 @@ export class MainHeader extends React.Component { return ( - Shlink Shlink + Shlink Shlink - + @@ -48,7 +53,7 @@ export class MainHeader extends React.Component { to={createServerPath} active={location.pathname === createServerPath} > -   Add server +   Add server @@ -59,4 +64,8 @@ export class MainHeader extends React.Component { } } -export default withRouter(MainHeader); +MainHeaderComponent.propTypes = propTypes; + +const MainHeader = withRouter(MainHeaderComponent); + +export default MainHeader; diff --git a/src/common/MenuLayout.js b/src/common/MenuLayout.js index 99ad9e7b..3f3c7b41 100644 --- a/src/common/MenuLayout.js +++ b/src/common/MenuLayout.js @@ -2,26 +2,38 @@ import React from 'react'; import { Route, Switch, withRouter } from 'react-router-dom'; import { connect } from 'react-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 Swipeable from 'react-swipeable'; import burgerIcon from '@fortawesome/fontawesome-free-solid/faBars'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; +import * as PropTypes from 'prop-types'; +import ShortUrlsVisits from '../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 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 }; // FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered + /* eslint react/no-deprecated: "off" */ componentWillMount() { - const { serverId } = this.props.match.params; - this.props.selectServer(serverId); + const { match, selectServer } = this.props; + const { params: { serverId } } = match; + + selectServer(serverId); } componentDidUpdate(prevProps) { @@ -44,14 +56,14 @@ export class MenuLayout extends React.Component { this.setState({ showSideBar: !this.state.showSideBar })} + onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))} /> this.setState({ showSideBar: false })} onSwipedRight={() => this.setState({ showSideBar: true })} - className="menu-layout__swipeable" >
{ + .then((registration) => { registration.onupdatefound = () => { const installingWorker = registration.installing; + installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { @@ -76,7 +82,7 @@ function registerValidSW(swUrl) { }; }; }) - .catch(error => { + .catch((error) => { console.error('Error during service worker registration:', error); }); } @@ -84,14 +90,16 @@ function registerValidSW(swUrl) { function checkValidServiceWorker(swUrl) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) - .then(response => { + .then((response) => { + const NOT_FOUND_STATUS = 404; + // Ensure service worker exists, and that we really are getting a JS file. if ( - response.status === 404 || + response.status === NOT_FOUND_STATUS || response.headers.get('content-type').indexOf('javascript') === -1 ) { // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { + navigator.serviceWorker.ready.then((registration) => { registration.unregister().then(() => { window.location.reload(); }); @@ -110,7 +118,7 @@ function checkValidServiceWorker(swUrl) { export function unregister() { if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { + navigator.serviceWorker.ready.then((registration) => { registration.unregister(); }); } diff --git a/src/servers/CreateServer.js b/src/servers/CreateServer.js index 4efbbe30..5cf18de0 100644 --- a/src/servers/CreateServer.js +++ b/src/servers/CreateServer.js @@ -1,13 +1,14 @@ import { assoc, dissoc, pick, pipe } from 'ramda'; import React from 'react'; import { connect } from 'react-redux'; -import { createServer } from './reducers/server'; -import { resetSelectedServer } from './reducers/selectedServer'; 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 ImportServersBtn from './helpers/ImportServersBtn'; -import PropTypes from 'prop-types'; +const SHOW_IMPORT_MSG_TIME = 4000; const propTypes = { createServer: PropTypes.func, history: PropTypes.shape({ @@ -16,7 +17,7 @@ const propTypes = { resetSelectedServer: PropTypes.func, }; -export class CreateServer extends React.Component { +export class CreateServerComponent extends React.Component { state = { name: '', url: '', @@ -24,7 +25,7 @@ export class CreateServer extends React.Component { serversImported: false, }; - submit = e => { + handleSubmit = (e) => { e.preventDefault(); const { createServer, history: { push } } = this.props; @@ -34,7 +35,7 @@ export class CreateServer extends React.Component { )(this.state); createServer(server); - push(`/server/${server.id}/list-short-urls/1`) + push(`/server/${server.id}/list-short-urls/1`); }; componentDidMount() { @@ -42,7 +43,7 @@ export class CreateServer extends React.Component { } render() { - const renderInputGroup = (id, placeholder, type = 'text') => + const renderInputGroup = (id, placeholder, type = 'text') => (
-
; +
+ ); return (
-
+ {renderInputGroup('name', 'Name')} {renderInputGroup('url', 'URL', 'url')} {renderInputGroup('apiKey', 'API key')}
- { - this.setState({ serversImported: true }); - setTimeout(() => this.setState({ serversImported: false }), 4000); - }} /> + { + this.setState({ serversImported: true }); + setTimeout(() => this.setState({ serversImported: false }), SHOW_IMPORT_MSG_TIME); + }} + />
@@ -90,9 +94,11 @@ export class CreateServer extends React.Component { } } -CreateServer.propTypes = propTypes; +CreateServerComponent.propTypes = propTypes; -export default connect( - pick(['selectedServer']), - {createServer, resetSelectedServer } -)(CreateServer); +const CreateServer = connect( + pick([ 'selectedServer' ]), + { createServer, resetSelectedServer } +)(CreateServerComponent); + +export default CreateServer; diff --git a/src/servers/DeleteServerButton.js b/src/servers/DeleteServerButton.js index 4b2b0dd1..5dff7244 100644 --- a/src/servers/DeleteServerButton.js +++ b/src/servers/DeleteServerButton.js @@ -1,9 +1,9 @@ import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import React from 'react'; +import PropTypes from 'prop-types'; import DeleteServerModal from './DeleteServerModal'; import { serverType } from './prop-types'; -import PropTypes from 'prop-types'; const propTypes = { server: serverType, @@ -20,8 +20,8 @@ export default class DeleteServerButton extends React.Component { this.setState({ isModalOpen: true })} key="deleteServerBtn" + onClick={() => this.setState({ isModalOpen: true })} > Delete this server @@ -29,7 +29,7 @@ export default class DeleteServerButton extends React.Component { this.setState({ isModalOpen: !this.state.isModalOpen })} + toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))} server={server} key="deleteServerModal" /> diff --git a/src/servers/DeleteServerModal.js b/src/servers/DeleteServerModal.js index 0d69bd52..ca1b8f72 100644 --- a/src/servers/DeleteServerModal.js +++ b/src/servers/DeleteServerModal.js @@ -11,9 +11,13 @@ const propTypes = { toggle: PropTypes.func.isRequired, isOpen: PropTypes.bool.isRequired, 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 = () => { deleteServer(server); 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, connect(null, { deleteServer }) -)(DeleteServerModal); +)(DeleteServerModalComponent); + +export default DeleteServerModal; diff --git a/src/servers/ServersDropdown.js b/src/servers/ServersDropdown.js index 68ecf85e..a990f302 100644 --- a/src/servers/ServersDropdown.js +++ b/src/servers/ServersDropdown.js @@ -3,21 +3,31 @@ import React from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; - -import { listServers } from './reducers/server'; +import PropTypes from 'prop-types'; import { selectServer } from '../servers/reducers/selectedServer'; import serversExporter from '../servers/services/ServersExporter'; +import { listServers } from './reducers/server'; +import { serverType } from './prop-types'; const defaultProps = { 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 = () => { const { servers, selectedServer, selectServer, serversExporter } = this.props; if (isEmpty(servers)) { - return Add a server first... + return Add a server first...; } return ( @@ -28,15 +38,17 @@ export class ServersDropdown extends React.Component { tag={Link} to={`/server/${id}/list-short-urls/1`} active={selectedServer && selectedServer.id === id} - onClick={() => selectServer(id)} // FIXME This should be implicit + + // FIXME This should be implicit + onClick={() => selectServer(id)} > {name} ))} serversExporter.exportServers()} > Export servers @@ -58,9 +70,12 @@ export class ServersDropdown extends React.Component { } } -ServersDropdown.defaultProps = defaultProps; +ServersDropdownComponent.defaultProps = defaultProps; +ServersDropdownComponent.propTypes = propTypes; -export default connect( - pick(['servers', 'selectedServer']), +const ServersDropdown = connect( + pick([ 'servers', 'selectedServer' ]), { listServers, selectServer } -)(ServersDropdown); +)(ServersDropdownComponent); + +export default ServersDropdown; diff --git a/src/servers/helpers/ImportServersBtn.js b/src/servers/helpers/ImportServersBtn.js index 7d6e277c..431d3d13 100644 --- a/src/servers/helpers/ImportServersBtn.js +++ b/src/servers/helpers/ImportServersBtn.js @@ -1,24 +1,24 @@ -import React from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { UncontrolledTooltip } from 'reactstrap'; -import serversImporter, { serversImporterType } from '../services/ServersImporter'; -import { createServers } from '../reducers/server'; import { assoc } from 'ramda'; import { v4 as uuid } from 'uuid'; import PropTypes from 'prop-types'; +import { createServers } from '../reducers/server'; +import serversImporter, { serversImporterType } from '../services/ServersImporter'; const defaultProps = { serversImporter, - onImport: () => {}, + onImport: () => ({}), }; const propTypes = { onImport: PropTypes.func, serversImporter: serversImporterType, 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) { super(props); this.fileRef = props.fileRef || React.createRef(); @@ -26,9 +26,9 @@ export class ImportServersBtn extends React.Component { render() { const { serversImporter: { importServersFromFile }, onImport, createServers } = this.props; - const onChange = e => + const onChange = (e) => 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(onImport); @@ -37,28 +37,30 @@ export class ImportServersBtn extends React.Component { - 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 name, apiKey and url ); } } -ImportServersBtn.defaultProps = defaultProps; -ImportServersBtn.propTypes = propTypes; +ImportServersBtnComponent.defaultProps = defaultProps; +ImportServersBtnComponent.propTypes = propTypes; -export default connect(null, { createServers })(ImportServersBtn); +const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent); + +export default ImportServersBtn; diff --git a/src/servers/reducers/selectedServer.js b/src/servers/reducers/selectedServer.js index 1fc9b2e5..aec65e58 100644 --- a/src/servers/reducers/selectedServer.js +++ b/src/servers/reducers/selectedServer.js @@ -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'; - -export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER'; -export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER'; +import shlinkApiClient from '../../api/ShlinkApiClient'; +import serversService from '../../servers/services/ServersService'; +import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams'; 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) { switch (action.type) { case SELECT_SERVER: @@ -21,15 +22,17 @@ export default function reducer(state = defaultState, action) { export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER }); -export const _selectServer = (ShlinkApiClient, serversService, serverId) => dispatch => { +export const _selectServer = (shlinkApiClient, serversService, serverId) => (dispatch) => { dispatch(resetShortUrlParams()); const selectedServer = serversService.findServerById(serverId); - ShlinkApiClient.setConfig(selectedServer); + + shlinkApiClient.setConfig(selectedServer); dispatch({ type: SELECT_SERVER, - selectedServer - }) + selectedServer, + }); }; -export const selectServer = curry(_selectServer)(ShlinkApiClient, serversService); + +export const selectServer = curry(_selectServer)(shlinkApiClient, serversService); diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index 978c82fd..cf2db109 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -1,5 +1,5 @@ -import serversService from '../services/ServersService'; import { curry } from 'ramda'; +import serversService from '../services/ServersService'; 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, servers: serversService.listServers(), }); + export const listServers = () => _listServers(serversService); export const _createServer = (serversService, server) => { serversService.createServer(server); + return _listServers(serversService); }; + export const createServer = curry(_createServer)(serversService); export const _deleteServer = (serversService, server) => { serversService.deleteServer(server); + return _listServers(serversService); }; + export const deleteServer = curry(_deleteServer)(serversService); export const _createServers = (serversService, servers) => { serversService.createServers(servers); + return _listServers(serversService); }; + export const createServers = curry(_createServers)(serversService); diff --git a/src/servers/services/ServersExporter.js b/src/servers/services/ServersExporter.js index 37005b1e..7262ac0f 100644 --- a/src/servers/services/ServersExporter.js +++ b/src/servers/services/ServersExporter.js @@ -1,21 +1,23 @@ -import serversService from './ServersService'; import { dissoc, head, keys, values } from 'ramda'; import csvjson from 'csvjson'; +import serversService from './ServersService'; const saveCsv = (window, csv) => { const { navigator, document } = window; 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 if (navigator.msSaveBlob) { navigator.msSaveBlob(blob, filename); + return; } // Modern browsers const link = document.createElement('a'); const url = URL.createObjectURL(blob); + link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; @@ -36,15 +38,18 @@ export class ServersExporter { try { const csv = this.csvjson.toCSV(servers, { - headers: keys(head(servers)).join(',') + headers: keys(head(servers)).join(','), }); + saveCsv(this.window, csv); } catch (e) { // FIXME Handle error + /* eslint no-console: "off" */ console.error(e); } }; } const serverExporter = new ServersExporter(serversService, global.window, csvjson); + export default serverExporter; diff --git a/src/servers/services/ServersImporter.js b/src/servers/services/ServersImporter.js index c60770a1..0734952c 100644 --- a/src/servers/services/ServersImporter.js +++ b/src/servers/services/ServersImporter.js @@ -16,8 +16,9 @@ export class ServersImporter { } 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 servers = this.csvjson.toObject(content); @@ -29,4 +30,5 @@ export class ServersImporter { } const serversImporter = new ServersImporter(csvjson); + export default serversImporter; diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js index a8b96b8f..25568526 100644 --- a/src/servers/services/ServersService.js +++ b/src/servers/services/ServersService.js @@ -1,5 +1,5 @@ -import Storage from '../../utils/Storage'; import { assoc, dissoc, reduce } from 'ramda'; +import storage from '../../utils/Storage'; const SERVERS_STORAGE_KEY = 'servers'; @@ -10,25 +10,27 @@ export class ServersService { 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( (serversObj, server) => assoc(server.id, server, serversObj), this.listServers(), servers ); + this.storage.set(SERVERS_STORAGE_KEY, allServers); }; - deleteServer = server => + deleteServer = (server) => this.storage.set( SERVERS_STORAGE_KEY, dissoc(server.id, this.listServers()) ); } -const serversService = new ServersService(Storage); +const serversService = new ServersService(storage); + export default serversService; diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js index ca82e29a..3db73243 100644 --- a/src/short-urls/CreateShortUrl.js +++ b/src/short-urls/CreateShortUrl.js @@ -6,11 +6,11 @@ import React from 'react'; import { connect } from 'react-redux'; import { Collapse } from 'reactstrap'; import DateInput from '../common/DateInput'; +import TagsSelector from '../utils/TagsSelector'; import CreateShortUrlResult from './helpers/CreateShortUrlResult'; import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult'; -import TagsSelector from '../utils/TagsSelector'; -export class CreateShortUrl extends React.Component { +export class CreateShortUrlComponent extends React.Component { state = { longUrl: '', tags: [], @@ -18,35 +18,37 @@ export class CreateShortUrl extends React.Component { validSince: undefined, validUntil: undefined, maxVisits: undefined, - moreOptionsVisible: false + moreOptionsVisible: false, }; render() { const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props; - const changeTags = tags => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) }); - const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => + const changeTags = (tags) => this.setState({ tags: tags.map(pipe(trim, replace(/ /g, '-'))) }); + const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => ( this.setState({ [id]: e.target.value })} + onChange={(e) => this.setState({ [id]: e.target.value })} {...props} - />; - const createDateInput = (id, placeholder, props = {}) => + /> + ); + const createDateInput = (id, placeholder, props = {}) => ( this.setState({ [id]: date })} isClearable + onChange={(date) => this.setState({ [id]: date })} {...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(); createShortUrl(pipe( - dissoc('moreOptionsVisible'), // Remove moreOptionsVisible property + dissoc('moreOptionsVisible'), assoc('validSince', formatDate(this.state.validSince)), assoc('validUntil', formatDate(this.state.validUntil)) )(this.state)); @@ -62,7 +64,7 @@ export class CreateShortUrl extends React.Component { placeholder="Insert the URL to be shortened" required value={this.state.longUrl} - onChange={e => this.setState({ longUrl: e.target.value })} + onChange={(e) => this.setState({ longUrl: e.target.value })} />
@@ -95,7 +97,7 @@ export class CreateShortUrl extends React.Component { @@ -51,4 +65,6 @@ export default class CreateShortUrlResult extends React.Component { ); } -}; +} + +CreateShortUrlResult.propTypes = propTypes; diff --git a/src/short-urls/helpers/EditTagsModal.js b/src/short-urls/helpers/EditTagsModal.js index 8bef6daf..8eda0b9c 100644 --- a/src/short-urls/helpers/EditTagsModal.js +++ b/src/short-urls/helpers/EditTagsModal.js @@ -1,30 +1,33 @@ import React from 'react'; import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import TagsSelector from '../../utils/TagsSelector'; import PropTypes from 'prop-types'; +import { pick } from 'ramda'; +import TagsSelector from '../../utils/TagsSelector'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsType, - shortUrlTagsEdited + shortUrlTagsEdited, } from '../reducers/shortUrlTags'; -import { pick } from 'ramda'; +import ExternalLink from '../../utils/ExternalLink'; +import { shortUrlType } from '../reducers/shortUrlsList'; const propTypes = { isOpen: PropTypes.bool.isRequired, toggle: PropTypes.func.isRequired, url: PropTypes.string.isRequired, - shortUrl: PropTypes.shape({ - tags: PropTypes.arrayOf(PropTypes.string), - shortCode: PropTypes.string, - }).isRequired, + shortUrl: shortUrlType.isRequired, shortUrlTags: shortUrlTagsType, + editShortUrlTags: PropTypes.func, + shortUrlTagsEdited: PropTypes.func, + resetShortUrlsTags: PropTypes.func, }; -export class EditTagsModal extends React.Component { +export class EditTagsModalComponent extends React.Component { saveTags = () => { const { editShortUrlTags, shortUrl, toggle } = this.props; + editShortUrlTags(shortUrl.shortCode, this.state.tags) .then(() => { this.tagsSaved = true; @@ -39,11 +42,13 @@ export class EditTagsModal extends React.Component { const { shortUrlTagsEdited, shortUrl } = this.props; const { tags } = this.state; + shortUrlTagsEdited(shortUrl.shortCode, tags); }; componentDidMount() { const { resetShortUrlsTags } = this.props; + resetShortUrlsTags(); this.tagsSaved = false; } @@ -57,12 +62,12 @@ export class EditTagsModal extends React.Component { const { isOpen, toggle, url, shortUrlTags } = this.props; return ( - + this.refreshShortUrls}> - Edit tags for {url} + Edit tags for {url} - this.setState({ tags })} /> + this.setState({ tags })} /> {shortUrlTags.error && (
Something went wrong while saving the tags :( @@ -74,8 +79,8 @@ export class EditTagsModal extends React.Component { @@ -85,9 +90,11 @@ export class EditTagsModal extends React.Component { } } -EditTagsModal.propTypes = propTypes; +EditTagsModalComponent.propTypes = propTypes; -export default connect( - pick(['shortUrlTags']), +const EditTagsModal = connect( + pick([ 'shortUrlTags' ]), { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } -)(EditTagsModal); +)(EditTagsModalComponent); + +export default EditTagsModal; diff --git a/src/short-urls/helpers/PreviewModal.js b/src/short-urls/helpers/PreviewModal.js index ee5393ca..87e171aa 100644 --- a/src/short-urls/helpers/PreviewModal.js +++ b/src/short-urls/helpers/PreviewModal.js @@ -1,11 +1,21 @@ -import React from 'react' +import React from 'react'; import { Modal, ModalBody, ModalHeader } from 'reactstrap'; +import PropTypes from 'prop-types'; 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 ( - Preview for {url} + + Preview for {url} +

Loading...

@@ -15,3 +25,5 @@ export default function PreviewModal ({ url, toggle, isOpen }) { ); } + +PreviewModal.propTypes = propTypes; diff --git a/src/short-urls/helpers/QrCodeModal.js b/src/short-urls/helpers/QrCodeModal.js index 194443f2..ee83b1df 100644 --- a/src/short-urls/helpers/QrCodeModal.js +++ b/src/short-urls/helpers/QrCodeModal.js @@ -1,11 +1,21 @@ -import React from 'react' +import React from 'react'; import { Modal, ModalBody, ModalHeader } from 'reactstrap'; +import PropTypes from 'prop-types'; 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 ( - QR code for {url} + + QR code for {url} +
QR code @@ -14,3 +24,5 @@ export default function QrCodeModal ({ url, toggle, isOpen }) { ); } + +QrCodeModal.propTypes = propTypes; diff --git a/src/short-urls/helpers/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js index 3190e327..ae183cbc 100644 --- a/src/short-urls/helpers/ShortUrlsRow.js +++ b/src/short-urls/helpers/ShortUrlsRow.js @@ -1,9 +1,23 @@ import { isEmpty } from 'ramda'; import React from 'react'; import Moment from 'react-moment'; +import PropTypes from 'prop-types'; 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 './ShortUrlsRow.scss'; + +const COPIED_MSG_TIME = 2000; + +const propTypes = { + refreshList: PropTypes.func, + shortUrlsListParams: shortUrlsListParamsType, + selectedServer: serverType, + shortUrl: shortUrlType, +}; export class ShortUrlsRow extends React.Component { state = { copiedToClipboard: false }; @@ -15,7 +29,8 @@ export class ShortUrlsRow extends React.Component { const { refreshList, shortUrlsListParams } = this.props; const selectedTags = shortUrlsListParams.tags || []; - return tags.map(tag => ( + + return tags.map((tag) => ( {shortUrl.dateCreated} - {completeShortUrl} + {completeShortUrl} - {shortUrl.originalUrl} + {shortUrl.originalUrl} {this.renderTags(shortUrl.tags)} {shortUrl.visitsCount} @@ -54,7 +69,7 @@ export class ShortUrlsRow extends React.Component { shortUrl={shortUrl} onCopyToClipboard={() => { this.setState({ copiedToClipboard: true }); - setTimeout(() => this.setState({ copiedToClipboard: false }), 2000); + setTimeout(() => this.setState({ copiedToClipboard: false }), COPIED_MSG_TIME); }} /> @@ -62,3 +77,5 @@ export class ShortUrlsRow extends React.Component { ); } } + +ShortUrlsRow.propTypes = propTypes; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js index 916736d2..436817c3 100644 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ b/src/short-urls/helpers/ShortUrlsRowMenu.js @@ -9,11 +9,21 @@ import React from 'react'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Link } from 'react-router-dom'; 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 QrCodeModal from './QrCodeModal'; import './ShortUrlsRowMenu.scss'; import EditTagsModal from './EditTagsModal'; +const propTypes = { + completeShortUrl: PropTypes.string, + onCopyToClipboard: PropTypes.func, + selectedServer: serverType, + shortUrl: shortUrlType, +}; + export class ShortUrlsRowMenu extends React.Component { state = { isOpen: false, @@ -21,26 +31,26 @@ export class ShortUrlsRowMenu extends React.Component { isPreviewOpen: false, isTagsModalOpen: false, }; - toggle = () => this.setState({ isOpen: !this.state.isOpen }); + toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen })); render() { const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props; const serverId = selectedServer ? selectedServer.id : ''; - const toggleQrCode = () => this.setState({isQrModalOpen: !this.state.isQrModalOpen}); - const togglePreview = () => this.setState({isPreviewOpen: !this.state.isPreviewOpen}); - const toggleTags = () => this.setState({isTagsModalOpen: !this.state.isTagsModalOpen}); + const toggleQrCode = () => this.setState(({ isQrModalOpen }) => ({ isQrModalOpen: !isQrModalOpen })); + const togglePreview = () => this.setState(({ isPreviewOpen }) => ({ isPreviewOpen: !isPreviewOpen })); + const toggleTags = () => this.setState(({ isTagsModalOpen }) => ({ isTagsModalOpen: !isTagsModalOpen })); return ( -    +    -  Visit Stats +  Visit Stats -  Edit tags +  Edit tags - + -  Preview +  Preview -  QR code +  QR code - + -  Copy to clipboard +  Copy to clipboard @@ -81,3 +91,5 @@ export class ShortUrlsRowMenu extends React.Component { ); } } + +ShortUrlsRowMenu.propTypes = propTypes; diff --git a/src/short-urls/reducers/shortUrlCreationResult.js b/src/short-urls/reducers/shortUrlCreationResult.js index c74b97d5..4e974cfc 100644 --- a/src/short-urls/reducers/shortUrlCreationResult.js +++ b/src/short-urls/reducers/shortUrlCreationResult.js @@ -1,11 +1,20 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; 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_ERROR = 'shlink/createShortUrl/CREATE_SHORT_URL_ERROR'; const CREATE_SHORT_URL = 'shlink/createShortUrl/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 = { result: null, 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 }); try { - const result = await ShlinkApiClient.createShortUrl(data); + const result = await shlinkApiClient.createShortUrl(data); + dispatch({ type: CREATE_SHORT_URL, result }); } catch (e) { 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 }); diff --git a/src/short-urls/reducers/shortUrlTags.js b/src/short-urls/reducers/shortUrlTags.js index e2244a92..9502edfe 100644 --- a/src/short-urls/reducers/shortUrlTags.js +++ b/src/short-urls/reducers/shortUrlTags.js @@ -1,11 +1,15 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; import { curry } from 'ramda'; 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_ERROR = 'shlink/shortUrlTags/EDIT_SHORT_URL_TAGS_ERROR'; + 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 SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED'; 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 }); try { // Update short URL tags - await ShlinkApiClient.updateShortUrlTags(shortCode, tags); + await shlinkApiClient.updateShortUrlTags(shortCode, tags); dispatch({ tags, shortCode, type: EDIT_SHORT_URL_TAGS }); } catch (e) { dispatch({ type: EDIT_SHORT_URL_TAGS_ERROR }); + throw e; } }; -export const editShortUrlTags = curry(_editShortUrlTags)(ShlinkApiClient); + +export const editShortUrlTags = curry(_editShortUrlTags)(shlinkApiClient); export const resetShortUrlsTags = () => ({ type: RESET_EDIT_SHORT_URL_TAGS }); diff --git a/src/short-urls/reducers/shortUrlVisits.js b/src/short-urls/reducers/shortUrlVisits.js index a247fa2a..38281e38 100644 --- a/src/short-urls/reducers/shortUrlVisits.js +++ b/src/short-urls/reducers/shortUrlVisits.js @@ -1,50 +1,60 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; 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_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR'; 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 = { shortUrl: {}, visits: [], loading: false, - error: false + error: false, }; -export default function dispatch (state = initialState, action) { +export default function dispatch(state = initialState, action) { switch (action.type) { case GET_SHORT_URL_VISITS_START: return { ...state, - loading: true + loading: true, }; case GET_SHORT_URL_VISITS_ERROR: return { ...state, loading: false, - error: true + error: true, }; case GET_SHORT_URL_VISITS: return { shortUrl: action.shortUrl, visits: action.visits, loading: false, - error: false + error: false, }; default: return state; } } -export const _getShortUrlVisits = (ShlinkApiClient, shortCode, dates) => dispatch => { +export const _getShortUrlVisits = (shlinkApiClient, shortCode, dates) => (dispatch) => { dispatch({ type: GET_SHORT_URL_VISITS_START }); Promise.all([ - ShlinkApiClient.getShortUrlVisits(shortCode, dates), - ShlinkApiClient.getShortUrl(shortCode) + shlinkApiClient.getShortUrlVisits(shortCode, dates), + 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 })); }; -export const getShortUrlVisits = curry(_getShortUrlVisits)(ShlinkApiClient); + +export const getShortUrlVisits = curry(_getShortUrlVisits)(shlinkApiClient); diff --git a/src/short-urls/reducers/shortUrlsList.js b/src/short-urls/reducers/shortUrlsList.js index 93c1f453..9925357a 100644 --- a/src/short-urls/reducers/shortUrlsList.js +++ b/src/short-urls/reducers/shortUrlsList.js @@ -1,11 +1,19 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; -import { SHORT_URL_TAGS_EDITED } from './shortUrlTags'; 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_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR'; + 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 = { shortUrls: {}, loading: true, @@ -19,34 +27,36 @@ export default function reducer(state = initialState, action) { return { loading: false, error: false, - shortUrls: action.shortUrls + shortUrls: action.shortUrls, }; case LIST_SHORT_URLS_ERROR: return { loading: false, error: true, - shortUrls: [] + shortUrls: [], }; case SHORT_URL_TAGS_EDITED: const { data } = state.shortUrls; - return assocPath(['shortUrls', 'data'], data.map(shortUrl => + + return assocPath([ 'shortUrls', 'data' ], data.map((shortUrl) => shortUrl.shortCode === action.shortCode ? assoc('tags', action.tags, shortUrl) - : shortUrl - ), state); + : shortUrl), state); default: return state; } } -export const _listShortUrls = (ShlinkApiClient, params = {}) => async dispatch => { +export const _listShortUrls = (shlinkApiClient, params = {}) => async (dispatch) => { dispatch({ type: LIST_SHORT_URLS_START }); try { - const shortUrls = await ShlinkApiClient.listShortUrls(params); + const shortUrls = await shlinkApiClient.listShortUrls(params); + dispatch({ type: LIST_SHORT_URLS, shortUrls, params }); } catch (e) { dispatch({ type: LIST_SHORT_URLS_ERROR, params }); } }; -export const listShortUrls = (params = {}) => _listShortUrls(ShlinkApiClient, params); + +export const listShortUrls = (params = {}) => _listShortUrls(shlinkApiClient, params); diff --git a/src/short-urls/reducers/shortUrlsListParams.js b/src/short-urls/reducers/shortUrlsListParams.js index 29464264..7e5cfd6e 100644 --- a/src/short-urls/reducers/shortUrlsListParams.js +++ b/src/short-urls/reducers/shortUrlsListParams.js @@ -1,7 +1,14 @@ +import PropTypes from 'prop-types'; import { LIST_SHORT_URLS } from './shortUrlsList'; 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' }; export default function reducer(state = defaultState, action) { diff --git a/src/tags/TagCard.js b/src/tags/TagCard.js index f28b25e9..2ccfd21c 100644 --- a/src/tags/TagCard.js +++ b/src/tags/TagCard.js @@ -2,20 +2,21 @@ import { Card, CardBody } from 'reactstrap'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import deleteIcon from '@fortawesome/fontawesome-free-solid/faTrash'; import editIcon from '@fortawesome/fontawesome-free-solid/faPencilAlt'; -import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal'; import PropTypes from 'prop-types'; import React from 'react'; -import ColorGenerator, { colorGeneratorType } from '../utils/ColorGenerator'; -import './TagCard.scss'; 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'; const propTypes = { tag: PropTypes.string, + currentServerId: PropTypes.string, colorGenerator: colorGeneratorType, }; const defaultProps = { - colorGenerator: ColorGenerator, + colorGenerator, }; export default class TagCard extends React.Component { @@ -24,9 +25,9 @@ export default class TagCard extends React.Component { render() { const { tag, colorGenerator, currentServerId } = this.props; const toggleDelete = () => - this.setState({ isDeleteModalOpen: !this.state.isDeleteModalOpen }); + this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen })); const toggleEdit = () => - this.setState({ isEditModalOpen: !this.state.isEditModalOpen }); + this.setState(({ isEditModalOpen }) => ({ isEditModalOpen: !isEditModalOpen })); return ( @@ -35,17 +36,17 @@ export default class TagCard extends React.Component { className="btn btn-light btn-sm tag-card__btn tag-card__btn--last" onClick={toggleDelete} > - +
diff --git a/src/tags/TagsList.js b/src/tags/TagsList.js index 5288a02b..c87ddfbd 100644 --- a/src/tags/TagsList.js +++ b/src/tags/TagsList.js @@ -1,25 +1,35 @@ import React from 'react'; import { connect } from 'react-redux'; import { pick, splitEvery } from 'ramda'; -import { filterTags, listTags } from './reducers/tagsList'; +import PropTypes from 'prop-types'; import MuttedMessage from '../utils/MuttedMessage'; -import TagCard from './TagCard'; import SearchField from '../utils/SearchField'; +import { filterTags, listTags } from './reducers/tagsList'; +import TagCard from './TagCard'; 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 { - state = { isDeleteModalOpen: false }; - +export class TagsListComponent extends React.Component { componentDidMount() { const { listTags } = this.props; + listTags(); } renderContent() { const { tagsList, match } = this.props; + if (tagsList.loading) { - return Loading... + return Loading...; } if (tagsList.error) { @@ -31,17 +41,18 @@ export class TagsList extends React.Component { } const tagsCount = tagsList.filteredTags.length; + if (tagsCount < 1) { return No tags found; } - const tagsGroups = splitEvery(ceil(tagsCount / 4), tagsList.filteredTags); + const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUP_SIZE), tagsList.filteredTags); return ( {tagsGroups.map((group, index) => (
- {group.map(tag => ( + {group.map((tag) => ( {!this.props.tagsList.loading && ( )}
@@ -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; diff --git a/src/tags/helpers/DeleteTagConfirmModal.js b/src/tags/helpers/DeleteTagConfirmModal.js index 59a4cb33..0cb2b1d8 100644 --- a/src/tags/helpers/DeleteTagConfirmModal.js +++ b/src/tags/helpers/DeleteTagConfirmModal.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import PropTypes from 'prop-types'; @@ -11,22 +11,25 @@ const propTypes = { isOpen: PropTypes.bool.isRequired, deleteTag: PropTypes.func, tagDelete: tagDeleteType, + tagDeleted: PropTypes.func, }; -export class DeleteTagConfirmModal extends Component { +export class DeleteTagConfirmModalComponent extends React.Component { doDelete = () => { const { tag, toggle, deleteTag } = this.props; + deleteTag(tag).then(() => { this.tagWasDeleted = true; toggle(); }); }; - onClosed = () => { + handleOnClosed = () => { if (!this.tagWasDeleted) { return; } const { tagDeleted, tag } = this.props; + tagDeleted(tag); }; @@ -38,7 +41,7 @@ export class DeleteTagConfirmModal extends Component { const { tag, toggle, isOpen, tagDelete } = this.props; return ( - + Delete tag @@ -54,8 +57,8 @@ export class DeleteTagConfirmModal extends Component { @@ -65,9 +68,11 @@ export class DeleteTagConfirmModal extends Component { } } -DeleteTagConfirmModal.propTypes = propTypes; +DeleteTagConfirmModalComponent.propTypes = propTypes; -export default connect( - pick(['tagDelete']), +const DeleteTagConfirmModal = connect( + pick([ 'tagDelete' ]), { deleteTag, tagDeleted } -)(DeleteTagConfirmModal); +)(DeleteTagConfirmModalComponent); + +export default DeleteTagConfirmModal; diff --git a/src/tags/helpers/EditTagModal.js b/src/tags/helpers/EditTagModal.js index ce038998..5c12d67b 100644 --- a/src/tags/helpers/EditTagModal.js +++ b/src/tags/helpers/EditTagModal.js @@ -2,20 +2,32 @@ import React from 'react'; import { connect } from 'react-redux'; import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap'; import { pick } from 'ramda'; -import { editTag, tagEdited } from '../reducers/tagEdit'; import { ChromePicker } from 'react-color'; -import ColorGenerator from '../../utils/ColorGenerator'; -import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette' -import FontAwesomeIcon from '@fortawesome/react-fontawesome' +import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette'; +import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import PropTypes from 'prop-types'; +import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; +import { editTag, tagEdited } from '../reducers/tagEdit'; import './EditTagModal.scss'; +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 = { - colorGenerator: ColorGenerator, + colorGenerator, }; - -export class EditTagModal extends React.Component { - saveTag = e => { +export class EditTagModalComponent extends React.Component { + saveTag = (e) => { e.preventDefault(); const { tag: oldName, editTag, toggle } = this.props; const { tag: newName, color } = this.state; @@ -27,13 +39,14 @@ export class EditTagModal extends React.Component { }) .catch(() => {}); }; - onClosed = () => { + handleOnClosed = () => { if (!this.tagWasEdited) { return; } const { tag: oldName, tagEdited } = this.props; const { tag: newName, color } = this.state; + tagEdited(oldName, newName, color); }; @@ -41,11 +54,12 @@ export class EditTagModal extends React.Component { super(props); const { colorGenerator, tag } = props; + this.state = { showColorPicker: false, tag, - color: colorGenerator.getColorForKey(tag) - } + color: colorGenerator.getColorForKey(tag), + }; } componentDidMount() { @@ -56,11 +70,11 @@ export class EditTagModal extends React.Component { const { isOpen, toggle, tagEdit } = this.props; const { tag, color } = this.state; const toggleColorPicker = () => - this.setState({ showColorPicker: !this.state.showColorPicker }); + this.setState(({ showColorPicker }) => ({ showColorPicker: !showColorPicker })); return ( - - + + this.saveTag()}> Edit tag
@@ -87,17 +101,17 @@ export class EditTagModal extends React.Component { > this.setState({ color: color.hex })} disableAlpha + onChange={(color) => this.setState({ color: color.hex })} /> this.setState({ tag: e.target.value })} placeholder="Tag" required className="form-control" + onChange={(e) => this.setState({ tag: e.target.value })} />
@@ -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; diff --git a/src/tags/reducers/tagDelete.js b/src/tags/reducers/tagDelete.js index c80d43f7..379476c8 100644 --- a/src/tags/reducers/tagDelete.js +++ b/src/tags/reducers/tagDelete.js @@ -1,10 +1,11 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; import { curry } from 'ramda'; import PropTypes from 'prop-types'; +import shlinkApiClient from '../../api/ShlinkApiClient'; const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START'; const DELETE_TAG_ERROR = 'shlink/deleteTag/DELETE_TAG_ERROR'; const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG'; + export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED'; 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 }); try { - await ShlinkApiClient.deleteTags([tag]); + await shlinkApiClient.deleteTags([ tag ]); dispatch({ type: DELETE_TAG }); } catch (e) { dispatch({ type: DELETE_TAG_ERROR }); + 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 }); diff --git a/src/tags/reducers/tagEdit.js b/src/tags/reducers/tagEdit.js index f8d6aae0..0c97ea1a 100644 --- a/src/tags/reducers/tagEdit.js +++ b/src/tags/reducers/tagEdit.js @@ -1,10 +1,11 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; -import ColorGenerator from '../../utils/ColorGenerator'; 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_ERROR = 'shlink/editTag/EDIT_TAG_ERROR'; const EDIT_TAG = 'shlink/editTag/EDIT_TAG'; + export const TAG_EDITED = 'shlink/editTag/TAG_EDITED'; const defaultState = { @@ -30,7 +31,7 @@ export default function reducer(state = defaultState, action) { }; case EDIT_TAG: return { - ...pick(['oldName', 'newName'], action), + ...pick([ 'oldName', 'newName' ], action), editing: false, error: false, }; @@ -39,20 +40,22 @@ export default function reducer(state = defaultState, action) { } } -export const _editTag = (ShlinkApiClient, ColorGenerator, oldName, newName, color) => - async dispatch => { +export const _editTag = (shlinkApiClient, colorGenerator, oldName, newName, color) => + async (dispatch) => { dispatch({ type: EDIT_TAG_START }); try { - await ShlinkApiClient.editTag(oldName, newName); - ColorGenerator.setColorForKey(newName, color); + await shlinkApiClient.editTag(oldName, newName); + colorGenerator.setColorForKey(newName, color); dispatch({ type: EDIT_TAG, oldName, newName }); } catch (e) { dispatch({ type: EDIT_TAG_ERROR }); + throw e; } }; -export const editTag = curry(_editTag)(ShlinkApiClient, ColorGenerator); + +export const editTag = curry(_editTag)(shlinkApiClient, colorGenerator); export const tagEdited = (oldName, newName, color) => ({ type: TAG_EDITED, diff --git a/src/tags/reducers/tagsList.js b/src/tags/reducers/tagsList.js index d2d7c819..b16ceda4 100644 --- a/src/tags/reducers/tagsList.js +++ b/src/tags/reducers/tagsList.js @@ -1,6 +1,6 @@ -import ShlinkApiClient from '../../api/ShlinkApiClient'; -import { TAG_DELETED } from './tagDelete'; import { reject } from 'ramda'; +import shlinkApiClient from '../../api/ShlinkApiClient'; +import { TAG_DELETED } from './tagDelete'; import { TAG_EDITED } from './tagEdit'; const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START'; @@ -16,7 +16,7 @@ const defaultState = { }; export default function reducer(state = defaultState, action) { - switch(action.type) { + switch (action.type) { case LIST_TAGS_START: return { ...state, @@ -39,14 +39,17 @@ export default function reducer(state = defaultState, action) { case TAG_DELETED: return { ...state, + // FIXME This should be optimized somehow... - tags: reject(tag => tag === action.tag, state.tags), - filteredTags: reject(tag => tag === action.tag, state.filteredTags), + tags: reject((tag) => tag === action.tag, state.tags), + filteredTags: reject((tag) => tag === action.tag, state.filteredTags), }; case TAG_EDITED: - const renameTag = tag => tag === action.oldName ? action.newName : tag; + const renameTag = (tag) => tag === action.oldName ? action.newName : tag; + return { ...state, + // FIXME This should be optimized somehow... tags: state.tags.map(renameTag).sort(), filteredTags: state.filteredTags.map(renameTag).sort(), @@ -55,7 +58,7 @@ export default function reducer(state = defaultState, action) { return { ...state, filteredTags: state.tags.filter( - tag => tag.toLowerCase().match(action.searchTerm), + (tag) => tag.toLowerCase().match(action.searchTerm), ), }; 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 }); try { - const tags = await ShlinkApiClient.listTags(); + const tags = await shlinkApiClient.listTags(); + dispatch({ tags, type: LIST_TAGS }); } catch (e) { 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, searchTerm, }); diff --git a/src/utils/ColorGenerator.js b/src/utils/ColorGenerator.js index 126df448..859608a2 100644 --- a/src/utils/ColorGenerator.js +++ b/src/utils/ColorGenerator.js @@ -1,13 +1,14 @@ -import Storage from './Storage'; -import PropTypes from 'prop-types'; import { range } from 'ramda'; +import PropTypes from 'prop-types'; +import storage from './Storage'; +const HEX_COLOR_LENGTH = 6; const { floor, random } = Math; const letters = '0123456789ABCDEF'; const buildRandomColor = () => `#${ - range(0, 6) - .map(() => letters[floor(random() * 16)]) + range(0, HEX_COLOR_LENGTH) + .map(() => letters[floor(random() * letters.length)]) .join('') }`; @@ -17,12 +18,13 @@ export class ColorGenerator { this.colors = this.storage.get('colors') || {}; } - getColorForKey = key => { + getColorForKey = (key) => { const color = this.colors[key]; // If a color has not been set yet, generate a random one and save it if (!color) { this.setColorForKey(key, buildRandomColor()); + return this.getColorForKey(key); } @@ -40,4 +42,6 @@ export const colorGeneratorType = PropTypes.shape({ setColorForKey: PropTypes.func, }); -export default new ColorGenerator(Storage); +const colorGenerator = new ColorGenerator(storage); + +export default colorGenerator; diff --git a/src/utils/ExternalLink.js b/src/utils/ExternalLink.js new file mode 100644 index 00000000..f926db7e --- /dev/null +++ b/src/utils/ExternalLink.js @@ -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 ( + + {children} + + ); +} + +ExternalLink.propTypes = propTypes; diff --git a/src/utils/MuttedMessage.js b/src/utils/MuttedMessage.js index e5619d0d..3b2e6cc6 100644 --- a/src/utils/MuttedMessage.js +++ b/src/utils/MuttedMessage.js @@ -1,8 +1,15 @@ import React from 'react'; import { Card } from 'reactstrap'; 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', { [`mt-${marginSize}`]: marginSize > 0, }); @@ -17,3 +24,5 @@ export default function MutedMessage({ children, marginSize = 4 }) {
); } + +MutedMessage.propTypes = propTypes; diff --git a/src/utils/SearchField.js b/src/utils/SearchField.js index b2474647..5ed91632 100644 --- a/src/utils/SearchField.js +++ b/src/utils/SearchField.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import './SearchField.scss'; +const DEFAULT_SEARCH_INTERVAL = 500; const propTypes = { onChange: PropTypes.func.isRequired, className: PropTypes.string, @@ -19,7 +20,7 @@ export default class SearchField extends React.Component { state = { showClearBtn: false, searchTerm: '' }; timer = null; - searchTermChanged(searchTerm, timeout = 500) { + searchTermChanged(searchTerm, timeout = DEFAULT_SEARCH_INTERVAL) { this.setState({ showClearBtn: searchTerm !== '', searchTerm, @@ -29,6 +30,7 @@ export default class SearchField extends React.Component { clearTimeout(this.timer); this.timer = null; }; + resetTimer(); this.timer = setTimeout(() => { @@ -46,15 +48,15 @@ export default class SearchField extends React.Component { type="text" className="form-control form-control-lg search-field__input" placeholder={placeholder} - onChange={e => this.searchTermChanged(e.target.value)} value={this.state.searchTerm} + onChange={(e) => this.searchTermChanged(e.target.value)} /> diff --git a/src/utils/Storage.js b/src/utils/Storage.js index 0b3efdaa..f7ba7d88 100644 --- a/src/utils/Storage.js +++ b/src/utils/Storage.js @@ -1,13 +1,14 @@ const PREFIX = 'shlink'; -const buildPath = path => `${PREFIX}.${path}`; +const buildPath = (path) => `${PREFIX}.${path}`; export class Storage { constructor(localStorage) { this.localStorage = localStorage; } - get = key => { + get = (key) => { const item = this.localStorage.getItem(buildPath(key)); + return item ? JSON.parse(item) : undefined; }; @@ -15,4 +16,5 @@ export class Storage { } const storage = typeof localStorage !== 'undefined' ? localStorage : {}; + export default new Storage(storage); diff --git a/src/utils/Tag.js b/src/utils/Tag.js index eeadfbcb..f22787ce 100644 --- a/src/utils/Tag.js +++ b/src/utils/Tag.js @@ -1,15 +1,28 @@ import React from 'react'; -import ColorGenerator from '../utils/ColorGenerator'; +import PropTypes from 'prop-types'; +import colorGenerator, { colorGeneratorType } from '../utils/ColorGenerator'; 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, text, children, clearable, onClick = () => ({}), - onClose = () => ({}) + onClose = () => ({}), } ) { return ( @@ -24,6 +37,5 @@ export default function Tag ( ); } -Tag.defaultProps = { - colorGenerator: ColorGenerator -}; +Tag.defaultProps = defaultProps; +Tag.propTypes = propTypes; diff --git a/src/utils/TagsSelector.js b/src/utils/TagsSelector.js index 9bf36af6..35b82a37 100644 --- a/src/utils/TagsSelector.js +++ b/src/utils/TagsSelector.js @@ -1,38 +1,41 @@ import React from 'react'; import TagsInput from 'react-tagsinput'; -import ColorGenerator, { colorGeneratorType } from './ColorGenerator'; import PropTypes from 'prop-types'; +import colorGenerator, { colorGeneratorType } from './ColorGenerator'; const defaultProps = { - colorGenerator: ColorGenerator, + colorGenerator, placeholder: 'Add tags to the URL', }; const propTypes = { tags: PropTypes.arrayOf(PropTypes.string).isRequired, onChange: PropTypes.func.isRequired, placeholder: PropTypes.string, - colorGenerator: colorGeneratorType + colorGenerator: colorGeneratorType, }; export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) { const renderTag = (props) => { const { tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other } = props; + return ( {getTagDisplayValue(tag)} {!disabled && onRemove(key)} />} - ) + ); }; return ( ); } diff --git a/src/visits/services/VisitsParser.js b/src/visits/services/VisitsParser.js index f74088ba..bb801d67 100644 --- a/src/visits/services/VisitsParser.js +++ b/src/visits/services/VisitsParser.js @@ -1,87 +1,90 @@ import { assoc, isNil, isEmpty, reduce } from 'ramda'; -const osFromUserAgent = userAgent => { +const osFromUserAgent = (userAgent) => { const lowerUserAgent = userAgent.toLowerCase(); switch (true) { - case (lowerUserAgent.indexOf('linux') >= 0): + case lowerUserAgent.indexOf('linux') >= 0: return 'Linux'; - case (lowerUserAgent.indexOf('windows') >= 0): + case lowerUserAgent.indexOf('windows') >= 0: return 'Windows'; - case (lowerUserAgent.indexOf('mac') >= 0): + case lowerUserAgent.indexOf('mac') >= 0: return 'MacOS'; - case (lowerUserAgent.indexOf('mobi') >= 0): + case lowerUserAgent.indexOf('mobi') >= 0: return 'Mobile'; default: return 'Others'; } }; -const browserFromUserAgent = userAgent => { +const browserFromUserAgent = (userAgent) => { const lowerUserAgent = userAgent.toLowerCase(); switch (true) { - case (lowerUserAgent.indexOf('opera') >= 0 || lowerUserAgent.indexOf('opr') >= 0): + case lowerUserAgent.indexOf('opera') >= 0 || lowerUserAgent.indexOf('opr') >= 0: return 'Opera'; - case (lowerUserAgent.indexOf('firefox') >= 0): + case lowerUserAgent.indexOf('firefox') >= 0: return 'Firefox'; - case (lowerUserAgent.indexOf('chrome') >= 0): + case lowerUserAgent.indexOf('chrome') >= 0: return 'Chrome'; - case (lowerUserAgent.indexOf('safari') >= 0): + case lowerUserAgent.indexOf('safari') >= 0: return 'Safari'; - case (lowerUserAgent.indexOf('msie') >= 0): + case lowerUserAgent.indexOf('msie') >= 0: return 'Internet Explorer'; default: return 'Others'; } }; -const extractDomain = url => { +const extractDomain = (url) => { const domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0]; + return domain.split(':')[0]; }; -export const processOsStats = visits => +export const processOsStats = (visits) => reduce( - (stats, visit) => { - const userAgent = visit.userAgent; + (stats, { userAgent }) => { const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent); + return assoc(os, (stats[os] || 0) + 1, stats); }, {}, visits, ); -export const processBrowserStats = visits => +export const processBrowserStats = (visits) => reduce( - (stats, visit) => { - const userAgent = visit.userAgent; + (stats, { userAgent }) => { const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent); + return assoc(browser, (stats[browser] || 0) + 1, stats); }, {}, visits, ); -export const processReferrersStats = visits => +export const processReferrersStats = (visits) => reduce( (stats, visit) => { const notHasDomain = isNil(visit.referer) || isEmpty(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, ); -export const processCountriesStats = visits => +export const processCountriesStats = (visits) => reduce( (stats, { visitLocation }) => { const notHasCountry = isNil(visitLocation) || isNil(visitLocation.countryName) || isEmpty(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, diff --git a/test/api/ShlinkApiClient.test.js b/test/api/ShlinkApiClient.test.js index fe524726..68894fb0 100644 --- a/test/api/ShlinkApiClient.test.js +++ b/test/api/ShlinkApiClient.test.js @@ -1,20 +1,20 @@ -import { ShlinkApiClient } from '../../src/api/ShlinkApiClient' import sinon from 'sinon'; import { head, last } from 'ramda'; +import { ShlinkApiClient } from '../../src/api/ShlinkApiClient'; describe('ShlinkApiClient', () => { - const createAxiosMock = extraData => () => + const createAxiosMock = (extraData) => () => Promise.resolve({ - headers: { authorization: 'Bearer abc123' }, + headers: { authorization: 'Bearer abc123' }, data: { token: 'abc123' }, ...extraData, }); - const createApiClient = extraData => + const createApiClient = (extraData) => new ShlinkApiClient(createAxiosMock(extraData)); describe('listShortUrls', () => { it('properly returns short URLs list', async () => { - const expectedList = ['foo', 'bar']; + const expectedList = [ 'foo', 'bar' ]; const apiClient = createApiClient({ data: { @@ -23,6 +23,7 @@ describe('ShlinkApiClient', () => { }); const actualList = await apiClient.listShortUrls(); + expect(expectedList).toEqual(actualList); }); }); @@ -35,6 +36,7 @@ describe('ShlinkApiClient', () => { it('returns create short URL', async () => { const apiClient = createApiClient({ data: shortUrl }); const result = await apiClient.createShortUrl({}); + expect(result).toEqual(shortUrl); }); @@ -54,7 +56,7 @@ describe('ShlinkApiClient', () => { describe('getShortUrlVisits', () => { it('properly returns short URL visits', async () => { - const expectedVisits = ['foo', 'bar']; + const expectedVisits = [ 'foo', 'bar' ]; const axiosSpy = sinon.spy(createAxiosMock({ data: { visits: { @@ -94,7 +96,7 @@ describe('ShlinkApiClient', () => { describe('updateShortUrlTags', () => { it('properly updates short URL tags', async () => { - const expectedTags = ['foo', 'bar']; + const expectedTags = [ 'foo', 'bar' ]; const axiosSpy = sinon.spy(createAxiosMock({ data: { tags: expectedTags }, })); @@ -112,10 +114,10 @@ describe('ShlinkApiClient', () => { describe('listTags', () => { it('properly returns list of tags', async () => { - const expectedTags = ['foo', 'bar']; + const expectedTags = [ 'foo', 'bar' ]; const axiosSpy = sinon.spy(createAxiosMock({ data: { - tags: { data: expectedTags } + tags: { data: expectedTags }, }, })); const apiClient = new ShlinkApiClient(axiosSpy); @@ -132,7 +134,7 @@ describe('ShlinkApiClient', () => { describe('deleteTags', () => { it('properly deletes provided tags', async () => { - const tags = ['foo', 'bar']; + const tags = [ 'foo', 'bar' ]; const axiosSpy = sinon.spy(createAxiosMock({})); const apiClient = new ShlinkApiClient(axiosSpy); diff --git a/test/common/AsideMenu.test.js b/test/common/AsideMenu.test.js index 141f20a9..febc65a8 100644 --- a/test/common/AsideMenu.test.js +++ b/test/common/AsideMenu.test.js @@ -1,7 +1,7 @@ -import { shallow } from 'enzyme' -import React from 'react' -import AsideMenu from '../../src/common/AsideMenu' +import { shallow } from 'enzyme'; +import React from 'react'; import { NavLink } from 'react-router-dom'; +import AsideMenu from '../../src/common/AsideMenu'; describe('', () => { let wrapped; @@ -15,9 +15,10 @@ describe('', () => { it('contains links to different sections', () => { const links = wrapped.find(NavLink); + const expectedLength = 3; - expect(links).toHaveLength(3); - links.forEach(link => expect(link.prop('to')).toContain('abc123')); + expect(links).toHaveLength(expectedLength); + links.forEach((link) => expect(link.prop('to')).toContain('abc123')); }); it('contains a button to delete server', () => { diff --git a/test/common/DateInput.test.js b/test/common/DateInput.test.js index a2df216e..6ec0bbbd 100644 --- a/test/common/DateInput.test.js +++ b/test/common/DateInput.test.js @@ -1,16 +1,18 @@ import React from 'react'; import { shallow } from 'enzyme'; -import DateInput from '../../src/common/DateInput'; import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import moment from 'moment'; +import DateInput from '../../src/common/DateInput'; describe('', () => { let wrapped; const createComponent = (props = {}) => { wrapped = shallow(); + return wrapped; }; + afterEach(() => { if (wrapped !== undefined) { wrapped.unmount(); diff --git a/test/common/Home.test.js b/test/common/Home.test.js index 1495604f..d44d9b0e 100644 --- a/test/common/Home.test.js +++ b/test/common/Home.test.js @@ -2,17 +2,21 @@ import { shallow } from 'enzyme'; import { values } from 'ramda'; import React from 'react'; import * as sinon from 'sinon'; -import { Home } from '../../src/common/Home'; +import { HomeComponent } from '../../src/common/Home'; describe('', () => { let wrapped; const defaultProps = { - resetSelectedServer: () => {}, + resetSelectedServer() { + return ''; + }, servers: {}, }; - const createComponent = props => { + const createComponent = (props) => { const actualProps = { ...defaultProps, ...props }; - wrapped = shallow(); + + wrapped = shallow(); + return wrapped; }; @@ -42,7 +46,7 @@ describe('', () => { const servers = { 1: { name: 'foo', id: '123' }, 2: { name: 'bar', id: '456' }, - } + }; const wrapped = createComponent({ servers }); expect(wrapped.find('Link')).toHaveLength(0); diff --git a/test/servers/CreateServer.test.js b/test/servers/CreateServer.test.js index f1b13c81..ecfde7ce 100644 --- a/test/servers/CreateServer.test.js +++ b/test/servers/CreateServer.test.js @@ -1,8 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { CreateServer } from '../../src/servers/CreateServer'; import { identity } from 'ramda'; import sinon from 'sinon'; +import { CreateServerComponent } from '../../src/servers/CreateServer'; import ImportServersBtn from '../../src/servers/helpers/ImportServersBtn'; describe('', () => { @@ -17,7 +17,7 @@ describe('', () => { historyMock.push.resetHistory(); wrapper = shallow( - ', () => { it('creates server and redirects to it when form is submitted', () => { const form = wrapper.find('form'); - form.simulate('submit', { preventDefault: () => {} }); + + form.simulate('submit', { preventDefault() { + return ''; + } }); expect(createServerMock.callCount).toEqual(1); expect(historyMock.push.callCount).toEqual(1); diff --git a/test/servers/DeleteServerButton.test.js b/test/servers/DeleteServerButton.test.js index d6fb142e..9da480a8 100644 --- a/test/servers/DeleteServerButton.test.js +++ b/test/servers/DeleteServerButton.test.js @@ -1,13 +1,14 @@ import React from 'react'; -import DeleteServerButton from '../../src/servers/DeleteServerButton'; import { shallow } from 'enzyme'; +import DeleteServerButton from '../../src/servers/DeleteServerButton'; import DeleteServerModal from '../../src/servers/DeleteServerModal'; describe('', () => { let wrapper; - beforeEach(() => - wrapper = shallow()); + beforeEach(() => { + wrapper = shallow(); + }); afterEach(() => wrapper.unmount()); it('renders a button and a modal', () => { diff --git a/test/servers/DeleteServerModal.test.js b/test/servers/DeleteServerModal.test.js index 23866f64..c5b879c2 100644 --- a/test/servers/DeleteServerModal.test.js +++ b/test/servers/DeleteServerModal.test.js @@ -1,8 +1,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import sinon from 'sinon'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { DeleteServerModalComponent } from '../../src/servers/DeleteServerModal'; describe('', () => { let wrapper; @@ -17,7 +17,7 @@ describe('', () => { historyMock.push.resetHistory(); wrapper = shallow( - ', () => { it('displays the name of the server as part of the content', () => { const modalBody = wrapper.find(ModalBody); + expect(modalBody.find('p').first().text()).toEqual( `Are you sure you want to delete server ${serverName}?` ); @@ -44,6 +45,7 @@ describe('', () => { it('toggles when clicking cancel button', () => { const cancelBtn = wrapper.find('button').first(); + cancelBtn.simulate('click'); expect(toggleMock.callCount).toEqual(1); @@ -53,6 +55,7 @@ describe('', () => { it('deletes server when clicking accept button', () => { const acceptBtn = wrapper.find('button').last(); + acceptBtn.simulate('click'); expect(toggleMock.callCount).toEqual(1); diff --git a/test/servers/ServersDropdown.test.js b/test/servers/ServersDropdown.test.js index 6c8a90eb..cbda33e7 100644 --- a/test/servers/ServersDropdown.test.js +++ b/test/servers/ServersDropdown.test.js @@ -1,34 +1,37 @@ -import { identity } from 'ramda'; +import { identity, values } from 'ramda'; import React from 'react'; -import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { shallow } from 'enzyme'; import { DropdownItem, DropdownToggle } from 'reactstrap'; +import { ServersDropdownComponent } from '../../src/servers/ServersDropdown'; describe('', () => { 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(() => { - wrapped = shallow(); + wrapped = shallow(); }); afterEach(() => wrapped.unmount()); 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', () => - expect(wrapped.find(DropdownToggle)).toHaveLength(1) - ); + expect(wrapped.find(DropdownToggle)).toHaveLength(1)); it('contains a button to export servers', () => { const items = wrapped.find(DropdownItem); + expect(items.filter('[divider]')).toHaveLength(1); expect(items.filter('.servers-dropdown__export-item')).toHaveLength(1); }); it('contains a message when no servers exist yet', () => { - wrapped = shallow(); + wrapped = shallow(); const item = wrapped.find(DropdownItem); expect(item).toHaveLength(1); diff --git a/test/servers/helpers/ImportServersBtn.test.js b/test/servers/helpers/ImportServersBtn.test.js index e57e8548..58d6c970 100644 --- a/test/servers/helpers/ImportServersBtn.test.js +++ b/test/servers/helpers/ImportServersBtn.test.js @@ -1,8 +1,8 @@ import React from 'react'; -import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { UncontrolledTooltip } from 'reactstrap'; +import { ImportServersBtnComponent } from '../../../src/servers/helpers/ImportServersBtn'; describe('', () => { let wrapper; @@ -12,7 +12,7 @@ describe('', () => { importServersFromFile: sinon.fake.returns(Promise.resolve([])), }; const fileRef = { - current: { click: sinon.fake() } + current: { click: sinon.fake() }, }; beforeEach(() => { @@ -22,11 +22,11 @@ describe('', () => { fileRef.current.click.resetHistory(); wrapper = shallow( - ); }); @@ -40,14 +40,16 @@ describe('', () => { it('triggers click on file ref when button is clicked', () => { const btn = wrapper.find('#importBtn'); + btn.simulate('click'); 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'); - file.simulate('change', { target: { files: [''] } }); + + file.simulate('change', { target: { files: [ '' ] } }); setImmediate(() => { expect(serversImporterMock.importServersFromFile.callCount).toEqual(1); diff --git a/test/servers/reducers/selectedServer.test.js b/test/servers/reducers/selectedServer.test.js index ca73ede4..74ea4b99 100644 --- a/test/servers/reducers/selectedServer.test.js +++ b/test/servers/reducers/selectedServer.test.js @@ -1,24 +1,23 @@ +import * as sinon from 'sinon'; import reduce, { _selectServer, RESET_SELECTED_SERVER, resetSelectedServer, SELECT_SERVER, } from '../../../src/servers/reducers/selectedServer'; -import * as sinon from 'sinon'; import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams'; describe('selectedServerReducer', () => { describe('reduce', () => { 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', () => - 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', () => { const selectedServer = { id: 'abc123' }; + expect(reduce(null, { type: SELECT_SERVER, selectedServer })).toEqual(selectedServer); }); }); @@ -31,14 +30,14 @@ describe('selectedServerReducer', () => { describe('selectServer', () => { const ShlinkApiClientMock = { - setConfig: sinon.spy() + setConfig: sinon.spy(), }; const serverId = 'abc123'; const selectedServer = { - id: serverId + id: serverId, }; const ServersServiceMock = { - findServerById: sinon.fake.returns(selectedServer) + findServerById: sinon.fake.returns(selectedServer), }; afterEach(() => { @@ -48,14 +47,15 @@ describe('selectedServerReducer', () => { it('dispatches proper actions', () => { const dispatch = sinon.spy(); + const expectedDispatchCalls = 2; _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.secondCall.calledWith({ type: SELECT_SERVER, - selectedServer + selectedServer, })).toEqual(true); }); diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index e0227081..b015603e 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -1,3 +1,5 @@ +import * as sinon from 'sinon'; +import { values } from 'ramda'; import reduce, { _createServer, _deleteServer, @@ -5,13 +7,11 @@ import reduce, { _createServers, FETCH_SERVERS, } from '../../../src/servers/reducers/server'; -import * as sinon from 'sinon'; -import { values } from 'ramda'; describe('serverReducer', () => { const servers = { abc123: { id: 'abc123' }, - def456: { id: 'def456' } + def456: { id: 'def456' }, }; const ServersServiceMock = { listServers: sinon.fake.returns(servers), @@ -22,12 +22,10 @@ describe('serverReducer', () => { describe('reduce', () => { 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', () => - expect(reduce({}, { type: 'unknown' })).toEqual({}) - ); + expect(reduce({}, { type: 'unknown' })).toEqual({})); }); describe('action creators', () => { diff --git a/test/servers/services/ServersExporter.test.js b/test/servers/services/ServersExporter.test.js index 04125bb1..eea6e19a 100644 --- a/test/servers/services/ServersExporter.test.js +++ b/test/servers/services/ServersExporter.test.js @@ -1,5 +1,5 @@ -import { ServersExporter } from '../../../src/servers/services/ServersExporter'; import sinon from 'sinon'; +import { ServersExporter } from '../../../src/servers/services/ServersExporter'; describe('ServersExporter', () => { const createLinkMock = () => ({ @@ -17,7 +17,7 @@ describe('ServersExporter', () => { appendChild: sinon.fake(), removeChild: sinon.fake(), }, - } + }, }); const serversServiceMock = { listServers: sinon.fake.returns({ @@ -41,11 +41,13 @@ describe('ServersExporter', () => { beforeEach(() => { originalConsole = global.console; global.console = { error: sinon.fake() }; - global.Blob = function Blob() {}; + global.Blob = class Blob {}; global.URL = { createObjectURL: () => '' }; serversServiceMock.listServers.resetHistory(); }); - afterEach(() => global.console = originalConsole); + afterEach(() => { + global.console = originalConsole; + }); it('logs an error if something fails', () => { const csvjsonMock = createCsvjsonMock(true); diff --git a/test/servers/services/ServersImporter.test.js b/test/servers/services/ServersImporter.test.js index 0d10df19..af1b5e8c 100644 --- a/test/servers/services/ServersImporter.test.js +++ b/test/servers/services/ServersImporter.test.js @@ -1,5 +1,5 @@ -import { ServersImporter } from '../../../src/servers/services/ServersImporter'; import sinon from 'sinon'; +import { ServersImporter } from '../../../src/servers/services/ServersImporter'; describe('ServersImporter', () => { const servers = [{ name: 'foo' }, { name: 'bar' }]; @@ -29,10 +29,13 @@ describe('ServersImporter', () => { it('reads file when a CSV is provided', async () => { const readAsText = sinon.fake.returns(''); - global.FileReader = function FileReader() { - this.readAsText = readAsText; - this.addEventListener = (eventName, listener) => - listener({ target: { result: '' } }); + + global.FileReader = class FileReader { + constructor() { + this.readAsText = readAsText; + this.addEventListener = (eventName, listener) => + listener({ target: { result: '' } }); + } }; await importer.importServersFromFile({ type: 'text/csv' }); diff --git a/test/servers/services/ServersService.test.js b/test/servers/services/ServersService.test.js index b07db206..a8fb4281 100644 --- a/test/servers/services/ServersService.test.js +++ b/test/servers/services/ServersService.test.js @@ -1,13 +1,13 @@ -import { ServersService } from '../../../src/servers/services/ServersService'; import sinon from 'sinon'; import { last } from 'ramda'; +import { ServersService } from '../../../src/servers/services/ServersService'; describe('ServersService', () => { const servers = { abc123: { id: 'abc123' }, def456: { id: 'def456' }, }; - const createStorageMock = returnValue => ({ + const createStorageMock = (returnValue) => ({ set: sinon.fake(), get: sinon.fake.returns(returnValue), }); diff --git a/test/short-urls/reducers/shortUrlsListParams.test.js b/test/short-urls/reducers/shortUrlsListParams.test.js index 7540f673..89b43aec 100644 --- a/test/short-urls/reducers/shortUrlsListParams.test.js +++ b/test/short-urls/reducers/shortUrlsListParams.test.js @@ -9,24 +9,20 @@ describe('shortUrlsListParamsReducer', () => { const defaultState = { page: '1' }; 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', () => expect(reduce(defaultState, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo' } })).toEqual({ ...defaultState, - searchTerm: 'foo' - }) - ); + searchTerm: 'foo', + })); 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', () => { it('returns proper action', () => - expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS }) - ); + expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS })); }); }); diff --git a/test/visits/services/VisitsParser.test.js b/test/visits/services/VisitsParser.test.js index 568ebb71..f74a9476 100644 --- a/test/visits/services/VisitsParser.test.js +++ b/test/visits/services/VisitsParser.test.js @@ -42,9 +42,9 @@ describe('VisitsParser', () => { describe('processOsStats', () => { it('properly parses OS stats', () => { expect(processOsStats(visits)).toEqual({ - 'Linux': 3, - 'Windows': 1, - 'MacOS': 1, + Linux: 3, + Windows: 1, + MacOS: 1, }); }); }); @@ -52,9 +52,9 @@ describe('VisitsParser', () => { describe('processBrowserStats', () => { it('properly parses browser stats', () => { expect(processBrowserStats(visits)).toEqual({ - 'Firefox': 2, - 'Chrome': 2, - 'Opera': 1, + Firefox: 2, + Chrome: 2, + Opera: 1, }); }); }); diff --git a/yarn.lock b/yarn.lock index 99096d01..a8084fce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,24 +198,24 @@ acorn-globals@^3.1.0: dependencies: acorn "^4.0.4" -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" +acorn-jsx@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" dependencies: - acorn "^3.0.4" - -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + acorn "^5.0.3" acorn@^4.0.3, acorn@^4.0.4: version "4.0.13" 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" 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: version "1.0.3" 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" 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" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9" dependencies: @@ -248,7 +248,7 @@ ajv@^4.9.1: co "^4.6.0" 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" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: @@ -363,13 +363,6 @@ argparse@^1.0.7: dependencies: 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: version "2.0.0" 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" 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: version "1.0.1" 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" 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: version "6.26.0" 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-preset-jest "^20.0.3" -babel-loader@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" +babel-loader@^7.1.2: + version "7.1.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -1468,10 +1451,6 @@ bser@^2.0.0: dependencies: 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: version "1.1.1" 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" isarray "^1.0.0" -builtin-modules@^1.0.0, builtin-modules@^1.1.1: +builtin-modules@^1.0.0: version "1.1.1" 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" 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: version "2.17.1" 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" 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: version "3.1.2" 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" 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" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: @@ -2100,6 +2066,16 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" 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: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -2248,10 +2224,6 @@ d@1: dependencies: 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: version "1.14.1" 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: 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" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -2459,7 +2431,7 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.0.0: +doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: @@ -2585,10 +2557,6 @@ elliptic@^6.0.0: minimalistic-assert "^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: version "2.1.0" 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" estraverse "^4.1.1" -eslint-config-react-app@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.1.0.tgz#23c909f71cbaff76b945b831d2d814b8bde169eb" +eslint-config-adidas-babel@^1.0.1: + version "1.0.1" + 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: version "0.3.2" @@ -2792,110 +2784,110 @@ eslint-loader@1.9.0: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-module-utils@^2.1.1: +eslint-module-utils@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" dependencies: debug "^2.6.8" pkg-dir "^1.0.0" -eslint-plugin-flowtype@2.39.1: - version "2.39.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.39.1.tgz#b5624622a0388bcd969f4351131232dcb9649cd5" +eslint-plugin-import@^2.8.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" 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" debug "^2.6.8" doctrine "1.5.0" eslint-import-resolver-node "^0.3.1" - eslint-module-utils "^2.1.1" + eslint-module-utils "^2.2.0" has "^1.0.1" - lodash.cond "^4.3.0" + lodash "^4.17.4" minimatch "^3.0.3" read-pkg-up "^2.0.0" + resolve "^1.6.0" -eslint-plugin-jsx-a11y@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-5.1.1.tgz#5c96bb5186ca14e94db1095ff59b3e2bd94069b1" +eslint-plugin-jest@^21.22.0: + version "21.22.0" + 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: - aria-query "^0.7.0" array-includes "^3.0.3" - ast-types-flow "0.0.7" - axobject-query "^0.1.0" - damerau-levenshtein "^1.0.0" - emoji-regex "^6.1.0" - jsx-ast-utils "^1.4.0" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + prop-types "^15.6.2" -eslint-plugin-react@7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.4.0.tgz#300a95861b9729c087d362dd64abcc351a74364a" - 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" +eslint-scope@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.10.0.tgz#f25d0d7955c81968c2309aa5c9a229e045176bb7" +eslint-utils@^1.3.1: + version "1.3.1" + 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: - ajv "^5.2.0" - babel-code-frame "^6.22.0" + ajv "^6.5.0" + babel-code-frame "^6.26.0" chalk "^2.1.0" - concat-stream "^1.6.0" - cross-spawn "^5.1.0" - debug "^3.0.1" - doctrine "^2.0.0" - eslint-scope "^3.7.1" - espree "^3.5.1" - esquery "^1.0.0" - estraverse "^4.2.0" + cross-spawn "^6.0.5" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^4.0.0" + eslint-utils "^1.3.1" + eslint-visitor-keys "^1.0.0" + espree "^4.0.0" + esquery "^1.0.1" esutils "^2.0.2" file-entry-cache "^2.0.0" functional-red-black-tree "^1.0.1" glob "^7.1.2" - globals "^9.17.0" - ignore "^3.3.3" + globals "^11.7.0" + ignore "^4.0.2" imurmurhash "^0.1.4" - inquirer "^3.0.6" - is-resolvable "^1.0.0" - js-yaml "^3.9.1" - json-stable-stringify "^1.0.1" + inquirer "^5.2.0" + is-resolvable "^1.1.0" + js-yaml "^3.11.0" + json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.17.4" - minimatch "^3.0.2" + lodash "^4.17.5" + minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" path-is-inside "^1.0.2" pluralize "^7.0.0" progress "^2.0.0" + regexpp "^2.0.0" require-uncached "^1.0.3" - semver "^5.3.0" + semver "^5.5.0" strip-ansi "^4.0.0" - strip-json-comments "~2.0.1" - table "^4.0.1" - text-table "~0.2.0" + strip-json-comments "^2.0.1" + table "^4.0.3" + text-table "^0.2.0" -espree@^3.5.1: - version "3.5.4" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" +espree@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" dependencies: - acorn "^5.5.0" - acorn-jsx "^3.0.0" + acorn "^5.6.0" + acorn-jsx "^4.1.1" esprima@^2.6.0: version "2.7.3" @@ -2909,7 +2901,7 @@ esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" -esquery@^1.0.0: +esquery@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" dependencies: @@ -3079,7 +3071,7 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.2" 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" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" dependencies: @@ -3546,11 +3538,11 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" -globals@^11.1.0: +globals@^11.1.0, globals@^11.7.0: version "11.7.0" 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" 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" kind-of "^4.0.0" -has@^1.0.1: +has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" dependencies: @@ -3967,11 +3959,11 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.3.3, ignore@^3.3.5: +ignore@^3.3.5: version "3.3.10" 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" 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" 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" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" dependencies: @@ -4054,6 +4046,24 @@ inquirer@3.3.0, inquirer@^3.0.6: strip-ansi "^4.0.0" 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: version "1.2.0" 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" 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" 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" 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" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -4806,6 +4816,10 @@ json-schema@0.2.3: version "0.2.3" 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: version "1.0.1" 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" verror "1.10.0" -jsx-ast-utils@^1.4.0: - 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: +jsx-ast-utils@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" dependencies: @@ -5007,10 +5017,6 @@ lodash.clonedeep@^4.3.2: version "4.5.0" 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: version "4.0.8" 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" 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: version "1.4.2" 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" 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" 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" 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" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: @@ -7091,6 +7101,10 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" 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: version "1.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" @@ -7331,7 +7345,7 @@ resolve@1.6.0: dependencies: 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" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" dependencies: @@ -7390,6 +7404,12 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" 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: version "5.1.1" 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" 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" 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" 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: version "1.2.0" 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" 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" resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" dependencies: @@ -8205,7 +8229,7 @@ text-encoding@^0.6.4: version "0.6.4" 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" 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" 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: version "0.7.18" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"