mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Merge pull request #188 from acelaya-forks/feature/visits-amount
Feature/visits amount
This commit is contained in:
commit
90751a09f7
14 changed files with 39 additions and 33 deletions
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { shortUrlTagsType } from '../reducers/shortUrlTags';
|
import { shortUrlTagsType } from '../reducers/shortUrlTags';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import './PreviewModal.scss';
|
import './PreviewModal.scss';
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
url: PropTypes.string,
|
url: PropTypes.string,
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlMetaType } from '../reducers/shortUrlsList';
|
||||||
import './ShortUrlVisitsCount.scss';
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
shortUrl: shortUrlType,
|
visitsCount: PropTypes.number.isRequired,
|
||||||
|
meta: shortUrlMetaType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShortUrlVisitsCount = ({ shortUrl }) => {
|
const ShortUrlVisitsCount = ({ visitsCount, meta }) => {
|
||||||
const { visitsCount, meta } = shortUrl;
|
|
||||||
const maxVisits = meta && meta.maxVisits;
|
const maxVisits = meta && meta.maxVisits;
|
||||||
|
|
||||||
if (!maxVisits) {
|
if (!maxVisits) {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { isEmpty } from 'ramda';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
|
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
|
||||||
import { serverType } from '../../servers/prop-types';
|
import { serverType } from '../../servers/prop-types';
|
||||||
import ExternalLink from '../../utils/ExternalLink';
|
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import { shortUrlType } from '../reducers/shortUrlsList';
|
||||||
import Tag from '../../tags/helpers/Tag';
|
import Tag from '../../tags/helpers/Tag';
|
||||||
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
|
import ShortUrlVisitsCount from './ShortUrlVisitsCount';
|
||||||
|
@ -58,7 +58,7 @@ const ShortUrlsRow = (
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
|
<td className="short-urls-row__cell" data-th="Tags: ">{this.renderTags(shortUrl.tags)}</td>
|
||||||
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">
|
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">
|
||||||
<ShortUrlVisitsCount shortUrl={shortUrl} />
|
<ShortUrlVisitsCount visitsCount={shortUrl.visitsCount} meta={shortUrl.meta} />
|
||||||
</td>
|
</td>
|
||||||
<td className="short-urls-row__cell short-urls-row__cell--relative">
|
<td className="short-urls-row__cell short-urls-row__cell--relative">
|
||||||
<small
|
<small
|
||||||
|
|
|
@ -10,16 +10,18 @@ export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR
|
||||||
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
|
export const shortUrlMetaType = PropTypes.shape({
|
||||||
|
validSince: PropTypes.string,
|
||||||
|
validUntil: PropTypes.string,
|
||||||
|
maxVisits: PropTypes.number,
|
||||||
|
});
|
||||||
|
|
||||||
export const shortUrlType = PropTypes.shape({
|
export const shortUrlType = PropTypes.shape({
|
||||||
shortCode: PropTypes.string,
|
shortCode: PropTypes.string,
|
||||||
shortUrl: PropTypes.string,
|
shortUrl: PropTypes.string,
|
||||||
longUrl: PropTypes.string,
|
longUrl: PropTypes.string,
|
||||||
visitsCount: PropTypes.number,
|
visitsCount: PropTypes.number,
|
||||||
meta: PropTypes.shape({
|
meta: shortUrlMetaType,
|
||||||
validSince: PropTypes.string,
|
|
||||||
validUntil: PropTypes.string,
|
|
||||||
maxVisits: PropTypes.number,
|
|
||||||
}),
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { ExternalLink } from 'react-external-link';
|
|
||||||
|
|
||||||
export default ExternalLink;
|
|
|
@ -132,7 +132,7 @@ const ShortUrlVisits = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="shlink-container">
|
<div className="shlink-container">
|
||||||
<VisitsHeader shortUrlDetail={shortUrlDetail} />
|
<VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />
|
||||||
|
|
||||||
<section className="mt-4">
|
<section className="mt-4">
|
||||||
<DateRangeRow
|
<DateRangeRow
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import { Card, UncontrolledTooltip } from 'reactstrap';
|
import { Card, UncontrolledTooltip } from 'reactstrap';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ExternalLink from '../utils/ExternalLink';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import ShortUrlVisitsCount from '../short-urls/helpers/ShortUrlVisitsCount';
|
import ShortUrlVisitsCount from '../short-urls/helpers/ShortUrlVisitsCount';
|
||||||
import { shortUrlDetailType } from './reducers/shortUrlDetail';
|
import { shortUrlDetailType } from './reducers/shortUrlDetail';
|
||||||
|
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||||
import './VisitsHeader.scss';
|
import './VisitsHeader.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
shortUrlDetail: shortUrlDetailType.isRequired,
|
shortUrlDetail: shortUrlDetailType.isRequired,
|
||||||
|
shortUrlVisits: shortUrlVisitsType.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function VisitsHeader({ shortUrlDetail }) {
|
export default function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
|
||||||
const { shortUrl, loading } = shortUrlDetail;
|
const { shortUrl, loading } = shortUrlDetail;
|
||||||
|
const { visits } = shortUrlVisits;
|
||||||
const shortLink = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
const shortLink = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';
|
||||||
const longLink = shortUrl && shortUrl.longUrl ? shortUrl.longUrl : '';
|
const longLink = shortUrl && shortUrl.longUrl ? shortUrl.longUrl : '';
|
||||||
|
|
||||||
|
@ -30,7 +33,7 @@ export default function VisitsHeader({ shortUrlDetail }) {
|
||||||
<h2>
|
<h2>
|
||||||
<span className="badge badge-main float-right">
|
<span className="badge badge-main float-right">
|
||||||
Visits:{' '}
|
Visits:{' '}
|
||||||
<ShortUrlVisitsCount shortUrl={shortUrl} />
|
<ShortUrlVisitsCount visitsCount={visits.length} meta={shortUrl.meta} />
|
||||||
</span>
|
</span>
|
||||||
Visit stats for <ExternalLink href={shortLink} />
|
Visit stats for <ExternalLink href={shortLink} />
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
||||||
import ExternalLink from '../../../src/utils/ExternalLink';
|
|
||||||
|
|
||||||
describe('<PreviewModal />', () => {
|
describe('<PreviewModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
import ExternalLink from '../../../src/utils/ExternalLink';
|
|
||||||
|
|
||||||
describe('<QrCodeModal />', () => {
|
describe('<QrCodeModal />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import each from 'jest-each';
|
||||||
import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount';
|
import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount';
|
||||||
|
|
||||||
describe('<ShortUrlVisitsCount />', () => {
|
describe('<ShortUrlVisitsCount />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const createWrapper = (shortUrl) => {
|
const createWrapper = (visitsCount, meta) => {
|
||||||
wrapper = shallow(<ShortUrlVisitsCount shortUrl={shortUrl} />);
|
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} meta={meta} />);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper && wrapper.unmount());
|
||||||
|
|
||||||
it('just returns visits when no maxVisits is provided', () => {
|
each([ undefined, {}]).it('just returns visits when no maxVisits is provided', (meta) => {
|
||||||
const visitsCount = 45;
|
const visitsCount = 45;
|
||||||
const wrapper = createWrapper({ visitsCount });
|
const wrapper = createWrapper(visitsCount, meta);
|
||||||
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ describe('<ShortUrlVisitsCount />', () => {
|
||||||
const visitsCount = 45;
|
const visitsCount = 45;
|
||||||
const maxVisits = 500;
|
const maxVisits = 500;
|
||||||
const meta = { maxVisits };
|
const meta = { maxVisits };
|
||||||
const wrapper = createWrapper({ visitsCount, meta });
|
const wrapper = createWrapper(visitsCount, meta);
|
||||||
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { shallow } from 'enzyme';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import { assoc, toString } from 'ramda';
|
import { assoc, toString } from 'ramda';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
|
import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
|
||||||
import ExternalLink from '../../../src/utils/ExternalLink';
|
|
||||||
import Tag from '../../../src/tags/helpers/Tag';
|
import Tag from '../../../src/tags/helpers/Tag';
|
||||||
|
|
||||||
describe('<ShortUrlsRow />', () => {
|
describe('<ShortUrlsRow />', () => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
|
import { ExternalLink } from 'react-external-link';
|
||||||
import VisitsHeader from '../../src/visits/VisitsHeader';
|
import VisitsHeader from '../../src/visits/VisitsHeader';
|
||||||
import ExternalLink from '../../src/utils/ExternalLink';
|
|
||||||
|
|
||||||
describe('<VisitsHeader />', () => {
|
describe('<VisitsHeader />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
@ -11,20 +11,22 @@ describe('<VisitsHeader />', () => {
|
||||||
shortUrl: 'https://doma.in/abc123',
|
shortUrl: 'https://doma.in/abc123',
|
||||||
longUrl: 'https://foo.bar/bar/foo',
|
longUrl: 'https://foo.bar/bar/foo',
|
||||||
dateCreated: '2018-01-01T10:00:00+01:00',
|
dateCreated: '2018-01-01T10:00:00+01:00',
|
||||||
visitsCount: 3,
|
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
const shortUrlVisits = {
|
||||||
|
visits: [{}, {}, {}],
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<VisitsHeader shortUrlDetail={shortUrlDetail} />);
|
wrapper = shallow(<VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
it('shows the amount of visits', () => {
|
it('shows the amount of visits', () => {
|
||||||
const visitsBadge = wrapper.find('.badge');
|
const visitsBadge = wrapper.find('.badge');
|
||||||
|
|
||||||
expect(visitsBadge.html()).toContain(`Visits: <span>${shortUrlDetail.shortUrl.visitsCount}</span>`);
|
expect(visitsBadge.html()).toContain(`Visits: <span>${shortUrlVisits.visits.length}</span>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows when the URL was created', () => {
|
it('shows when the URL was created', () => {
|
||||||
|
|
Loading…
Reference in a new issue