mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-17 21:13:48 +00:00
Merge pull request #266 from acelaya-forks/feature/tags-list-improvements
Feature/tags list improvements
This commit is contained in:
@@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
* [#253](https://github.com/shlinkio/shlink-web-client/issues/253) Created new settings page that will be used to define customizations in the app.
|
* [#253](https://github.com/shlinkio/shlink-web-client/issues/253) Created new settings page that will be used to define customizations in the app.
|
||||||
|
|
||||||
|
* [#265](https://github.com/shlinkio/shlink-web-client/issues/265) Updated tags section to allow displaying number of short URLs using every tag and number of visits for all short URLs using the tag.
|
||||||
|
|
||||||
|
This will work only when using Shlink v2.2.0 or above. For previous versions, the tags page will continue behaving the same.
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
|
||||||
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
|
* [#218](https://github.com/shlinkio/shlink-web-client/issues/218) Added back button to sections not displayed in left menu.
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ body,
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paddingless {
|
.indivisible {
|
||||||
padding: 0;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indivisible {
|
.text-ellipsis {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,84 @@
|
|||||||
import { Card, CardBody } from 'reactstrap';
|
import { Card, CardHeader, CardBody, Button, Collapse } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrash as deleteIcon, faPencilAlt as editIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash as deleteIcon, faPencilAlt as editIcon, faLink, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
|
import { useToggle } from '../utils/helpers/hooks';
|
||||||
import TagBullet from './helpers/TagBullet';
|
import TagBullet from './helpers/TagBullet';
|
||||||
import './TagCard.scss';
|
import './TagCard.scss';
|
||||||
|
|
||||||
const TagCard = (DeleteTagConfirmModal, EditTagModal, colorGenerator) => class TagCard extends React.Component {
|
const propTypes = {
|
||||||
static propTypes = {
|
tag: PropTypes.string,
|
||||||
tag: PropTypes.string,
|
tagStats: PropTypes.shape({
|
||||||
currentServerId: PropTypes.string,
|
shortUrlsCount: PropTypes.number,
|
||||||
};
|
visitsCount: PropTypes.number,
|
||||||
|
}),
|
||||||
|
selectedServer: serverType,
|
||||||
|
displayed: PropTypes.bool,
|
||||||
|
toggle: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
state = { isDeleteModalOpen: false, isEditModalOpen: false };
|
const TagCard = (DeleteTagConfirmModal, EditTagModal, ForServerVersion, colorGenerator) => {
|
||||||
|
const TagCardComp = ({ tag, tagStats, selectedServer, displayed, toggle }) => {
|
||||||
|
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
||||||
|
const [ isEditModalOpen, toggleEdit ] = useToggle();
|
||||||
|
|
||||||
render() {
|
const { id } = selectedServer;
|
||||||
const { tag, currentServerId } = this.props;
|
const shortUrlsLink = `/server/${id}/list-short-urls/1?tag=${tag}`;
|
||||||
const toggleDelete = () =>
|
|
||||||
this.setState(({ isDeleteModalOpen }) => ({ isDeleteModalOpen: !isDeleteModalOpen }));
|
|
||||||
const toggleEdit = () =>
|
|
||||||
this.setState(({ isEditModalOpen }) => ({ isEditModalOpen: !isEditModalOpen }));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="tag-card">
|
<Card className="tag-card">
|
||||||
<CardBody className="tag-card__body">
|
<CardHeader className="tag-card__header">
|
||||||
<button className="btn btn-light btn-sm tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
<Button color="light" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
||||||
<FontAwesomeIcon icon={deleteIcon} />
|
<FontAwesomeIcon icon={deleteIcon} />
|
||||||
</button>
|
</Button>
|
||||||
<button className="btn btn-light btn-sm tag-card__btn" onClick={toggleEdit}>
|
<Button color="light" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
||||||
<FontAwesomeIcon icon={editIcon} />
|
<FontAwesomeIcon icon={editIcon} />
|
||||||
</button>
|
</Button>
|
||||||
<h5 className="tag-card__tag-title">
|
<h5 className="tag-card__tag-title text-ellipsis">
|
||||||
<TagBullet tag={tag} colorGenerator={colorGenerator} />
|
<TagBullet tag={tag} colorGenerator={colorGenerator} />
|
||||||
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>{tag}</Link>
|
<ForServerVersion minVersion="2.2.0">
|
||||||
|
<span className="tag-card__tag-name" onClick={toggle}>{tag}</span>
|
||||||
|
</ForServerVersion>
|
||||||
|
<ForServerVersion maxVersion="2.1.*">
|
||||||
|
<Link to={shortUrlsLink}>{tag}</Link>
|
||||||
|
</ForServerVersion>
|
||||||
</h5>
|
</h5>
|
||||||
</CardBody>
|
</CardHeader>
|
||||||
|
|
||||||
<DeleteTagConfirmModal tag={tag} toggle={toggleDelete} isOpen={this.state.isDeleteModalOpen} />
|
{tagStats && (
|
||||||
<EditTagModal tag={tag} toggle={toggleEdit} isOpen={this.state.isEditModalOpen} />
|
<Collapse isOpen={displayed}>
|
||||||
|
<CardBody className="tag-card__body">
|
||||||
|
<Link
|
||||||
|
to={shortUrlsLink}
|
||||||
|
className="btn btn-light btn-block d-flex justify-content-between align-items-center mb-1"
|
||||||
|
>
|
||||||
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faLink} className="mr-2" />Short URLs</span>
|
||||||
|
<b>{prettify(tagStats.shortUrlsCount)}</b>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={`/server/${id}/tags/${tag}/visits`}
|
||||||
|
className="btn btn-light btn-block d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<span className="text-ellipsis"><FontAwesomeIcon icon={faEye} className="mr-2" />Visits</span>
|
||||||
|
<b>{prettify(tagStats.visitsCount)}</b>
|
||||||
|
</Link>
|
||||||
|
</CardBody>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DeleteTagConfirmModal tag={tag} toggle={toggleDelete} isOpen={isDeleteModalOpen} />
|
||||||
|
<EditTagModal tag={tag} toggle={toggleEdit} isOpen={isEditModalOpen} />
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
TagCardComp.propTypes = propTypes;
|
||||||
|
|
||||||
|
return TagCardComp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagCard;
|
export default TagCard;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
.tag-card.tag-card {
|
.tag-card.tag-card {
|
||||||
background-color: #eee;
|
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-card__header.tag-card__header {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-card__header.tag-card__header,
|
||||||
.tag-card__body.tag-card__body {
|
.tag-card__body.tag-card__body {
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
}
|
}
|
||||||
@@ -10,9 +14,6 @@
|
|||||||
.tag-card__tag-title {
|
.tag-card__tag-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 31px;
|
line-height: 31px;
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,3 +24,17 @@
|
|||||||
.tag-card__btn--last {
|
.tag-card__btn--last {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-card__table-cell.tag-card__table-cell {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-card__tag-name {
|
||||||
|
color: #007bff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-card__tag-name:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { splitEvery } from 'ramda';
|
import { splitEvery } from 'ramda';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Message from '../utils/Message';
|
import Message from '../utils/Message';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
|
import { serverType } from '../servers/prop-types';
|
||||||
|
import { TagsListType } from './reducers/tagsList';
|
||||||
|
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
const TAGS_GROUPS_AMOUNT = 4;
|
const TAGS_GROUPS_AMOUNT = 4;
|
||||||
@@ -10,16 +12,14 @@ const TAGS_GROUPS_AMOUNT = 4;
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
filterTags: PropTypes.func,
|
filterTags: PropTypes.func,
|
||||||
forceListTags: PropTypes.func,
|
forceListTags: PropTypes.func,
|
||||||
tagsList: PropTypes.shape({
|
tagsList: TagsListType,
|
||||||
loading: PropTypes.bool,
|
selectedServer: serverType,
|
||||||
error: PropTypes.bool,
|
|
||||||
filteredTags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
}),
|
|
||||||
match: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TagsList = (TagCard) => {
|
const TagsList = (TagCard) => {
|
||||||
const TagListComp = ({ filterTags, forceListTags, tagsList, match }) => {
|
const TagListComp = ({ filterTags, forceListTags, tagsList, selectedServer }) => {
|
||||||
|
const [ displayedTag, setDisplayedTag ] = useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
forceListTags();
|
forceListTags();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -53,7 +53,10 @@ const TagsList = (TagCard) => {
|
|||||||
<TagCard
|
<TagCard
|
||||||
key={tag}
|
key={tag}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
currentServerId={match.params.serverId}
|
tagStats={tagsList.stats[tag]}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
displayed={displayedTag === tag}
|
||||||
|
toggle={() => setDisplayedTag(displayedTag !== tag ? tag : undefined)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
import { isEmpty, reject } from 'ramda';
|
import { isEmpty, reject } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { TAG_DELETED } from './tagDelete';
|
import { TAG_DELETED } from './tagDelete';
|
||||||
import { TAG_EDITED } from './tagEdit';
|
import { TAG_EDITED } from './tagEdit';
|
||||||
|
|
||||||
@@ -10,9 +11,18 @@ export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
|||||||
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
|
export const TagsListType = PropTypes.shape({
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
filteredTags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
stats: PropTypes.object, // Record
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
});
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
tags: [],
|
tags: [],
|
||||||
filteredTags: [],
|
filteredTags: [],
|
||||||
|
stats: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
@@ -23,7 +33,7 @@ const rejectTag = (tags, tagToReject) => reject((tag) => tag === tagToReject, ta
|
|||||||
export default handleActions({
|
export default handleActions({
|
||||||
[LIST_TAGS_START]: (state) => ({ ...state, loading: true, error: false }),
|
[LIST_TAGS_START]: (state) => ({ ...state, loading: true, error: false }),
|
||||||
[LIST_TAGS_ERROR]: (state) => ({ ...state, loading: false, error: true }),
|
[LIST_TAGS_ERROR]: (state) => ({ ...state, loading: false, error: true }),
|
||||||
[LIST_TAGS]: (state, { tags }) => ({ tags, filteredTags: tags, loading: false, error: false }),
|
[LIST_TAGS]: (state, { tags, stats }) => ({ stats, tags, filteredTags: tags, loading: false, error: false }),
|
||||||
[TAG_DELETED]: (state, { tag }) => ({
|
[TAG_DELETED]: (state, { tag }) => ({
|
||||||
...state,
|
...state,
|
||||||
tags: rejectTag(state.tags, tag),
|
tags: rejectTag(state.tags, tag),
|
||||||
@@ -51,9 +61,14 @@ export const listTags = (buildShlinkApiClient, force = true) => () => async (dis
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { listTags } = buildShlinkApiClient(getState);
|
const { listTags } = buildShlinkApiClient(getState);
|
||||||
const tags = await listTags();
|
const { tags, stats = [] } = await listTags();
|
||||||
|
const processedStats = stats.reduce((acc, { tag, shortUrlsCount, visitsCount }) => {
|
||||||
|
acc[tag] = { shortUrlsCount, visitsCount };
|
||||||
|
|
||||||
dispatch({ tags, type: LIST_TAGS });
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
dispatch({ tags, stats: processedStats, type: LIST_TAGS });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: LIST_TAGS_ERROR });
|
dispatch({ type: LIST_TAGS_ERROR });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ const provideServices = (bottle, connect) => {
|
|||||||
bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
|
bottle.serviceFactory('TagsSelector', TagsSelector, 'ColorGenerator');
|
||||||
bottle.decorator('TagsSelector', connect([ 'tagsList' ], [ 'listTags' ]));
|
bottle.decorator('TagsSelector', connect([ 'tagsList' ], [ 'listTags' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
|
bottle.serviceFactory(
|
||||||
|
'TagCard',
|
||||||
|
TagCard,
|
||||||
|
'DeleteTagConfirmModal',
|
||||||
|
'EditTagModal',
|
||||||
|
'ForServerVersion',
|
||||||
|
'ColorGenerator'
|
||||||
|
);
|
||||||
|
|
||||||
bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
|
bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
|
||||||
bottle.decorator('DeleteTagConfirmModal', connect([ 'tagDelete' ], [ 'deleteTag', 'tagDeleted' ]));
|
bottle.decorator('DeleteTagConfirmModal', connect([ 'tagDelete' ], [ 'deleteTag', 'tagDeleted' ]));
|
||||||
@@ -21,7 +28,7 @@ const provideServices = (bottle, connect) => {
|
|||||||
bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ]));
|
bottle.decorator('EditTagModal', connect([ 'tagEdit' ], [ 'editTag', 'tagEdited' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('TagsList', TagsList, 'TagCard');
|
bottle.serviceFactory('TagsList', TagsList, 'TagCard');
|
||||||
bottle.decorator('TagsList', connect([ 'tagsList' ], [ 'forceListTags', 'filterTags' ]));
|
bottle.decorator('TagsList', connect([ 'tagsList', 'selectedServer' ], [ 'forceListTags', 'filterTags' ]));
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const listTagsActionFactory = (force) => ({ buildShlinkApiClient }) => listTags(buildShlinkApiClient, force);
|
const listTagsActionFactory = (force) => ({ buildShlinkApiClient }) => listTags(buildShlinkApiClient, force);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const SortingDropdown = ({ items, orderField, orderDir, onChange, isButton, righ
|
|||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
caret
|
caret
|
||||||
color={isButton ? 'secondary' : 'link'}
|
color={isButton ? 'secondary' : 'link'}
|
||||||
className={classNames({ 'btn-block': isButton, 'btn-sm paddingless': !isButton })}
|
className={classNames({ 'btn-block': isButton, 'btn-sm p-0': !isButton })}
|
||||||
>
|
>
|
||||||
Order by
|
Order by
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ export default class ShlinkApiClient {
|
|||||||
.then(() => meta);
|
.then(() => meta);
|
||||||
|
|
||||||
listTags = () =>
|
listTags = () =>
|
||||||
this._performRequest('/tags', 'GET')
|
this._performRequest('/tags', 'GET', { withStats: 'true' })
|
||||||
.then((resp) => resp.data.tags.data);
|
.then((resp) => resp.data.tags)
|
||||||
|
.then(({ data, stats }) => ({ tags: data, stats }));
|
||||||
|
|
||||||
deleteTags = (tags) =>
|
deleteTags = (tags) =>
|
||||||
this._performRequest('/tags', 'DELETE', { tags })
|
this._performRequest('/tags', 'DELETE', { tags })
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default class SortableBarGraph extends React.Component {
|
|||||||
{withPagination && keys(stats).length > 50 && (
|
{withPagination && keys(stats).length > 50 && (
|
||||||
<div className="float-right">
|
<div className="float-right">
|
||||||
<PaginationDropdown
|
<PaginationDropdown
|
||||||
toggleClassName="btn-sm paddingless mr-3"
|
toggleClassName="btn-sm p-0 mr-3"
|
||||||
ranges={[ 50, 100, 200, 500 ]}
|
ranges={[ 50, 100, 200, 500 ]}
|
||||||
value={this.state.itemsPerPage}
|
value={this.state.itemsPerPage}
|
||||||
setValue={(itemsPerPage) => this.setState({ itemsPerPage, currentPage: 1 })}
|
setValue={(itemsPerPage) => this.setState({ itemsPerPage, currentPage: 1 })}
|
||||||
|
|||||||
@@ -6,43 +6,52 @@ import TagBullet from '../../src/tags/helpers/TagBullet';
|
|||||||
|
|
||||||
describe('<TagCard />', () => {
|
describe('<TagCard />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
const tagStats = {
|
||||||
|
shortUrlsCount: 48,
|
||||||
|
visitsCount: 23257,
|
||||||
|
};
|
||||||
|
const DeleteTagConfirmModal = jest.fn();
|
||||||
|
const EditTagModal = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const TagCard = createTagCard(() => '', () => '', {});
|
const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => '', {});
|
||||||
|
|
||||||
wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />);
|
wrapper = shallow(<TagCard tag="ssr" selectedServer={{ id: 1, serverNotFound: false }} tagStats={tagStats} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
afterEach(jest.resetAllMocks);
|
||||||
|
|
||||||
it('shows a TagBullet and a link to the list filtering by the tag', () => {
|
it('shows a TagBullet and a link to the list filtering by the tag', () => {
|
||||||
const link = wrapper.find(Link);
|
const links = wrapper.find(Link);
|
||||||
const bullet = wrapper.find(TagBullet);
|
const bullet = wrapper.find(TagBullet);
|
||||||
|
|
||||||
expect(link.prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr');
|
expect(links.at(0).prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr');
|
||||||
expect(bullet.prop('tag')).toEqual('ssr');
|
expect(bullet.prop('tag')).toEqual('ssr');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays delete modal when delete btn is clicked', (done) => {
|
it('displays delete modal when delete btn is clicked', () => {
|
||||||
const delBtn = wrapper.find('.tag-card__btn').at(0);
|
const delBtn = wrapper.find('.tag-card__btn').at(0);
|
||||||
|
|
||||||
expect(wrapper.state('isDeleteModalOpen')).toEqual(false);
|
expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(false);
|
||||||
delBtn.simulate('click');
|
delBtn.simulate('click');
|
||||||
|
expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(true);
|
||||||
setImmediate(() => {
|
|
||||||
expect(wrapper.state('isDeleteModalOpen')).toEqual(true);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays edit modal when edit btn is clicked', (done) => {
|
it('displays edit modal when edit btn is clicked', () => {
|
||||||
const editBtn = wrapper.find('.tag-card__btn').at(1);
|
const editBtn = wrapper.find('.tag-card__btn').at(1);
|
||||||
|
|
||||||
expect(wrapper.state('isEditModalOpen')).toEqual(false);
|
expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(false);
|
||||||
editBtn.simulate('click');
|
editBtn.simulate('click');
|
||||||
|
expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
setImmediate(() => {
|
it('shows expected tag stats', () => {
|
||||||
expect(wrapper.state('isEditModalOpen')).toEqual(true);
|
const links = wrapper.find(Link);
|
||||||
done();
|
|
||||||
});
|
expect(links.at(1).prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr');
|
||||||
|
expect(links.at(1).text()).toContain('48');
|
||||||
|
expect(links.at(2).prop('to')).toEqual('/server/1/tags/ssr/visits');
|
||||||
|
expect(links.at(2).text()).toContain('23,257');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('<TagsList />', () => {
|
|||||||
it('renders the proper amount of groups and cards based on the amount of tags', () => {
|
it('renders the proper amount of groups and cards based on the amount of tags', () => {
|
||||||
const amountOfTags = 10;
|
const amountOfTags = 10;
|
||||||
const amountOfGroups = 4;
|
const amountOfGroups = 4;
|
||||||
const wrapper = createWrapper({ filteredTags: rangeOf(amountOfTags, (i) => `tag_${i}`) });
|
const wrapper = createWrapper({ filteredTags: rangeOf(amountOfTags, (i) => `tag_${i}`), stats: {} });
|
||||||
const cards = wrapper.find(TagCard);
|
const cards = wrapper.find(TagCard);
|
||||||
const groups = wrapper.find('.col-md-6');
|
const groups = wrapper.find('.col-md-6');
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ describe('tagsListReducer', () => {
|
|||||||
it('dispatches loaded lists when no error occurs', async () => {
|
it('dispatches loaded lists when no error occurs', async () => {
|
||||||
const tags = [ 'foo', 'bar', 'baz' ];
|
const tags = [ 'foo', 'bar', 'baz' ];
|
||||||
|
|
||||||
listTagsMock.mockResolvedValue(tags);
|
listTagsMock.mockResolvedValue({ tags, stats: [] });
|
||||||
buildShlinkApiClient.mockReturnValue({ listTags: listTagsMock });
|
buildShlinkApiClient.mockReturnValue({ listTags: listTagsMock });
|
||||||
|
|
||||||
await listTags(buildShlinkApiClient, true)()(dispatch, getState);
|
await listTags(buildShlinkApiClient, true)()(dispatch, getState);
|
||||||
@@ -112,7 +112,7 @@ describe('tagsListReducer', () => {
|
|||||||
expect(getState).toHaveBeenCalledTimes(1);
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_TAGS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_TAGS_START });
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_TAGS, tags });
|
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_TAGS, tags, stats: {} });
|
||||||
});
|
});
|
||||||
|
|
||||||
const assertErrorResult = async () => {
|
const assertErrorResult = async () => {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ describe('ShlinkApiClient', () => {
|
|||||||
|
|
||||||
const result = await listTags();
|
const result = await listTags();
|
||||||
|
|
||||||
expect(expectedTags).toEqual(result);
|
expect({ tags: expectedTags }).toEqual(result);
|
||||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({ url: '/tags', method: 'GET' }));
|
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({ url: '/tags', method: 'GET' }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user