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
-
+
@@ -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 (
@@ -95,7 +97,7 @@ export class CreateShortUrl extends React.Component {