diff --git a/CHANGELOG.md b/CHANGELOG.md
index d51bf867..2c9b21e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Changed
* [#205](https://github.com/shlinkio/shlink-web-client/issues/205) Replaced `jest-each` package by jet's native `test.each` function.
+* [#209](https://github.com/shlinkio/shlink-web-client/issues/209) Replaced `Unknown` by `Direct` for visits from undetermined referrers.
+* [#212](https://github.com/shlinkio/shlink-web-client/issues/212) Moved copy-to-clipboard next to short URL.
#### Deprecated
diff --git a/src/index.scss b/src/index.scss
index b2a386ee..480dd907 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -10,10 +10,6 @@ body,
outline: none !important;
}
-.nowrap {
- white-space: nowrap;
-}
-
.bg-main {
background-color: $mainColor !important;
}
diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js
index ffc9cd21..94fb0b10 100644
--- a/src/short-urls/ShortUrlsList.js
+++ b/src/short-urls/ShortUrlsList.js
@@ -161,7 +161,7 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={this.orderByColumn('visits')}
>
- {this.renderOrderIcon('visits')} Visits
+ {this.renderOrderIcon('visits')} Visits
|
diff --git a/src/short-urls/helpers/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js
index a0cbf4a8..a058a92a 100644
--- a/src/short-urls/helpers/ShortUrlsRow.js
+++ b/src/short-urls/helpers/ShortUrlsRow.js
@@ -3,6 +3,9 @@ import React from 'react';
import Moment from 'react-moment';
import PropTypes from 'prop-types';
import { ExternalLink } from 'react-external-link';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
import { serverType } from '../../servers/prop-types';
import { shortUrlType } from '../reducers/shortUrlsList';
@@ -10,53 +13,57 @@ import Tag from '../../tags/helpers/Tag';
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
import './ShortUrlsRow.scss';
+const propTypes = {
+ refreshList: PropTypes.func,
+ shortUrlsListParams: shortUrlsListParamsType,
+ selectedServer: serverType,
+ shortUrl: shortUrlType,
+};
+
const ShortUrlsRow = (
ShortUrlsRowMenu,
colorGenerator,
- stateFlagTimeout
-) => class ShortUrlsRow extends React.Component {
- static propTypes = {
- refreshList: PropTypes.func,
- shortUrlsListParams: shortUrlsListParamsType,
- selectedServer: serverType,
- shortUrl: shortUrlType,
- };
+ useStateFlagTimeout
+) => {
+ const ShortUrlsRowComp = ({ shortUrl, selectedServer, refreshList, shortUrlsListParams }) => {
+ const [ copiedToClipboard, setCopiedToClipboard ] = useStateFlagTimeout(false);
+ const renderTags = (tags) => {
+ if (isEmpty(tags)) {
+ return No tags;
+ }
- state = { copiedToClipboard: false };
+ const selectedTags = shortUrlsListParams.tags || [];
- renderTags(tags) {
- if (isEmpty(tags)) {
- return No tags;
- }
-
- const { refreshList, shortUrlsListParams } = this.props;
- const selectedTags = shortUrlsListParams.tags || [];
-
- return tags.map((tag) => (
- refreshList({ tags: [ ...selectedTags, tag ] })}
- />
- ));
- }
-
- render() {
- const { shortUrl, selectedServer } = this.props;
+ return tags.map((tag) => (
+ refreshList({ tags: [ ...selectedTags, tag ] })}
+ />
+ ));
+ };
return (
-
+ |
{shortUrl.dateCreated}
|
-
+
+
+
+
+
+
+ Copied short URL!
+
+
|
|
- {this.renderTags(shortUrl.tags)} |
+ {renderTags(shortUrl.tags)} |
|
-
-
- Copied short URL!
-
- stateFlagTimeout(this.setState.bind(this), 'copiedToClipboard')}
- />
+
+
|
|
);
- }
+ };
+
+ ShortUrlsRowComp.propTypes = propTypes;
+
+ return ShortUrlsRowComp;
};
export default ShortUrlsRow;
diff --git a/src/short-urls/helpers/ShortUrlsRow.scss b/src/short-urls/helpers/ShortUrlsRow.scss
index da6e8f86..7a888cf3 100644
--- a/src/short-urls/helpers/ShortUrlsRow.scss
+++ b/src/short-urls/helpers/ShortUrlsRow.scss
@@ -43,11 +43,16 @@
position: relative;
}
+.short-urls-row__copy-btn {
+ cursor: pointer;
+ font-size: 1.2rem;
+}
+
.short-urls-row__copy-hint {
- @include vertical-align();
- right: 100%;
+ @include vertical-align(translateX(10px));
+ box-shadow: 0 3px 15px rgba(0, 0, 0, .25);
@media (max-width: $smMax) {
- right: calc(100% + 10px);
+ @include vertical-align(translateX(calc(-100% - 20px)));
}
}
diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js
index 1915883a..0e8a149d 100644
--- a/src/short-urls/helpers/ShortUrlsRowMenu.js
+++ b/src/short-urls/helpers/ShortUrlsRowMenu.js
@@ -1,4 +1,4 @@
-import { faCopy as copyIcon, faImage as pictureIcon } from '@fortawesome/free-regular-svg-icons';
+import { faImage as pictureIcon } from '@fortawesome/free-regular-svg-icons';
import {
faTags as tagsIcon,
faChartPie as pieChartIcon,
@@ -9,9 +9,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
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';
@@ -26,7 +24,6 @@ const ShortUrlsRowMenu = (
ForServerVersion
) => class ShortUrlsRowMenu extends React.Component {
static propTypes = {
- onCopyToClipboard: PropTypes.func,
selectedServer: serverType,
shortUrl: shortUrlType,
};
@@ -42,7 +39,7 @@ const ShortUrlsRowMenu = (
toggle = () => this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
render() {
- const { onCopyToClipboard, shortUrl, selectedServer } = this.props;
+ const { shortUrl, selectedServer } = this.props;
const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
const toggleModal = (prop) => () => this.setState((prevState) => ({ [prop]: !prevState[prop] }));
const toggleQrCode = toggleModal('isQrModalOpen');
@@ -73,12 +70,10 @@ const ShortUrlsRowMenu = (
-
- Delete short URL
+
+ QR code
-
-
-
+
@@ -87,20 +82,12 @@ const ShortUrlsRowMenu = (
-
- QR code
+
+
+
+ Delete short URL
-
-
-
-
-
-
-
-
- Copy to clipboard
-
-
+
);
diff --git a/src/short-urls/services/provideServices.js b/src/short-urls/services/provideServices.js
index 014ecd74..3c3c258f 100644
--- a/src/short-urls/services/provideServices.js
+++ b/src/short-urls/services/provideServices.js
@@ -33,7 +33,7 @@ const provideServices = (bottle, connect) => {
[ 'listShortUrls', 'resetShortUrlParams' ]
));
- bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'stateFlagTimeout');
+ bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
bottle.serviceFactory(
'ShortUrlsRowMenu',
diff --git a/src/utils/mixins/vertical-align.scss b/src/utils/mixins/vertical-align.scss
index d9fd0a0b..5af5038c 100644
--- a/src/utils/mixins/vertical-align.scss
+++ b/src/utils/mixins/vertical-align.scss
@@ -1,5 +1,5 @@
-@mixin vertical-align {
+@mixin vertical-align($extraTransforms: '') {
position: absolute;
top: 50%;
- transform: translateY(-50%);
+ transform: translateY(-50%) $extraTransforms;
}
diff --git a/src/utils/services/provideServices.js b/src/utils/services/provideServices.js
index 4c165f37..0efe06cd 100644
--- a/src/utils/services/provideServices.js
+++ b/src/utils/services/provideServices.js
@@ -1,5 +1,5 @@
import axios from 'axios';
-import { stateFlagTimeout } from '../utils';
+import { stateFlagTimeout, useStateFlagTimeout } from '../utils';
import Storage from './Storage';
import ColorGenerator from './ColorGenerator';
import buildShlinkApiClient from './ShlinkApiClientBuilder';
@@ -14,6 +14,7 @@ const provideServices = (bottle) => {
bottle.constant('setTimeout', global.setTimeout);
bottle.serviceFactory('stateFlagTimeout', stateFlagTimeout, 'setTimeout');
+ bottle.serviceFactory('useStateFlagTimeout', useStateFlagTimeout, 'setTimeout');
};
export default provideServices;
diff --git a/src/utils/utils.js b/src/utils/utils.js
index d3118236..78d50daa 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -19,6 +19,16 @@ export const stateFlagTimeout = (setTimeout) => (
setTimeout(() => setState({ [flagName]: !initialValue }), delay);
};
+export const useStateFlagTimeout = (setTimeout) => (initialValue = true, delay = DEFAULT_TIMEOUT_DELAY) => {
+ const [ flag, setFlag ] = useState(initialValue);
+ const callback = () => {
+ setFlag(!initialValue);
+ setTimeout(() => setFlag(initialValue), delay);
+ };
+
+ return [ flag, callback ];
+};
+
export const determineOrderDir = (clickedField, currentOrderField, currentOrderDir) => {
if (currentOrderField !== clickedField) {
return 'ASC';
diff --git a/src/visits/services/VisitsParser.js b/src/visits/services/VisitsParser.js
index 8aef797f..004f8e30 100644
--- a/src/visits/services/VisitsParser.js
+++ b/src/visits/services/VisitsParser.js
@@ -61,7 +61,7 @@ const updateBrowsersStatsForVisit = (browsersStats, { userAgent }) => {
const updateReferrersStatsForVisit = (referrersStats, { referer }) => {
const notHasDomain = isNil(referer) || isEmpty(referer);
- const domain = notHasDomain ? 'Unknown' : extractDomain(referer);
+ const domain = notHasDomain ? 'Direct' : extractDomain(referer);
referrersStats[domain] = (referrersStats[domain] || 0) + 1;
};
diff --git a/test/short-urls/helpers/ShortUrlsRow.test.js b/test/short-urls/helpers/ShortUrlsRow.test.js
index cb6d9a77..4c0d022c 100644
--- a/test/short-urls/helpers/ShortUrlsRow.test.js
+++ b/test/short-urls/helpers/ShortUrlsRow.test.js
@@ -4,6 +4,7 @@ import moment from 'moment';
import Moment from 'react-moment';
import { assoc, toString } from 'ramda';
import { ExternalLink } from 'react-external-link';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
import Tag from '../../../src/tags/helpers/Tag';
@@ -11,7 +12,8 @@ describe('', () => {
let wrapper;
const mockFunction = () => '';
const ShortUrlsRowMenu = mockFunction;
- const stateFlagTimeout = jest.fn();
+ const stateFlagTimeout = jest.fn(() => true);
+ const useStateFlagTimeout = jest.fn(() => [ false, stateFlagTimeout ]);
const colorGenerator = {
getColorForKey: mockFunction,
setColorForKey: mockFunction,
@@ -29,7 +31,7 @@ describe('', () => {
};
beforeEach(() => {
- const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, stateFlagTimeout);
+ const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout);
wrapper = shallow(
@@ -87,20 +89,12 @@ describe('', () => {
});
it('updates state when copied to clipboard', () => {
- const col = wrapper.find('td').at(5);
- const menu = col.find(ShortUrlsRowMenu);
+ const col = wrapper.find('td').at(1);
+ const menu = col.find(CopyToClipboard);
expect(menu).toHaveLength(1);
expect(stateFlagTimeout).not.toHaveBeenCalled();
- menu.simulate('copyToClipboard');
+ menu.simulate('copy');
expect(stateFlagTimeout).toHaveBeenCalledTimes(1);
});
-
- it('shows copy hint when state prop is true', () => {
- const isHidden = () => wrapper.find('td').at(5).find('.short-urls-row__copy-hint').prop('hidden');
-
- expect(isHidden()).toEqual(true);
- wrapper.setState({ copiedToClipboard: true });
- expect(isHidden()).toEqual(false);
- });
});
diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.js
index cfcee90d..8ffd0d38 100644
--- a/test/short-urls/helpers/ShortUrlsRowMenu.test.js
+++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.js
@@ -49,8 +49,8 @@ describe('', () => {
const wrapper = createWrapper();
const items = wrapper.find(DropdownItem);
- expect(items).toHaveLength(9);
- expect(items.find('[divider]')).toHaveLength(2);
+ expect(items).toHaveLength(7);
+ expect(items.find('[divider]')).toHaveLength(1);
});
describe('toggles state when toggling modal windows', () => {
diff --git a/test/visits/services/VisitsParser.test.js b/test/visits/services/VisitsParser.test.js
index c6dfd717..01041e53 100644
--- a/test/visits/services/VisitsParser.test.js
+++ b/test/visits/services/VisitsParser.test.js
@@ -74,7 +74,7 @@ describe('VisitsParser', () => {
const { referrers } = stats;
expect(referrers).toEqual({
- 'Unknown': 2,
+ 'Direct': 2,
'google.com': 2,
'm.facebook.com': 1,
});