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} - + + + + + + + - {this.renderTags(shortUrl.tags)} + {renderTags(shortUrl.tags)} - - - 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, });