mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Migrated remaining tags-related elements to TS
This commit is contained in:
parent
18883caa6d
commit
f8ea1ae3d5
8 changed files with 198 additions and 225 deletions
|
@ -1,21 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
const regularServerType = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
apiKey: PropTypes.string,
|
||||
version: PropTypes.string,
|
||||
printableVersion: PropTypes.string,
|
||||
serverNotReachable: PropTypes.bool,
|
||||
});
|
||||
|
||||
const notFoundServerType = PropTypes.shape({
|
||||
serverNotFound: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
/** @deprecated Use SelectedServer type instead */
|
||||
export const serverType = PropTypes.oneOfType([
|
||||
regularServerType,
|
||||
notFoundServerType,
|
||||
]);
|
|
@ -1,84 +0,0 @@
|
|||
import { Card, CardHeader, CardBody, Button, Collapse } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrash as deleteIcon, faPencilAlt as editIcon, faLink, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
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 './TagCard.scss';
|
||||
|
||||
const propTypes = {
|
||||
tag: PropTypes.string,
|
||||
tagStats: PropTypes.shape({
|
||||
shortUrlsCount: PropTypes.number,
|
||||
visitsCount: PropTypes.number,
|
||||
}),
|
||||
selectedServer: serverType,
|
||||
displayed: PropTypes.bool,
|
||||
toggle: PropTypes.func,
|
||||
};
|
||||
|
||||
const TagCard = (DeleteTagConfirmModal, EditTagModal, ForServerVersion, colorGenerator) => {
|
||||
const TagCardComp = ({ tag, tagStats, selectedServer, displayed, toggle }) => {
|
||||
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
||||
const [ isEditModalOpen, toggleEdit ] = useToggle();
|
||||
|
||||
const { id } = selectedServer;
|
||||
const shortUrlsLink = `/server/${id}/list-short-urls/1?tag=${tag}`;
|
||||
|
||||
return (
|
||||
<Card className="tag-card">
|
||||
<CardHeader className="tag-card__header">
|
||||
<Button color="light" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
||||
<FontAwesomeIcon icon={deleteIcon} />
|
||||
</Button>
|
||||
<Button color="light" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
||||
<FontAwesomeIcon icon={editIcon} />
|
||||
</Button>
|
||||
<h5 className="tag-card__tag-title text-ellipsis">
|
||||
<TagBullet tag={tag} colorGenerator={colorGenerator} />
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
||||
{tagStats && (
|
||||
<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}/tag/${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>
|
||||
);
|
||||
};
|
||||
|
||||
TagCardComp.propTypes = propTypes;
|
||||
|
||||
return TagCardComp;
|
||||
};
|
||||
|
||||
export default TagCard;
|
82
src/tags/TagCard.tsx
Normal file
82
src/tags/TagCard.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { Card, CardHeader, CardBody, Button, Collapse } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrash as deleteIcon, faPencilAlt as editIcon, faLink, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { prettify } from '../utils/helpers/numbers';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { Versions } from '../utils/helpers/version';
|
||||
import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { isServerWithId, SelectedServer } from '../servers/data';
|
||||
import TagBullet from './helpers/TagBullet';
|
||||
import { TagModalProps, TagStats } from './data';
|
||||
import './TagCard.scss';
|
||||
|
||||
export interface TagCardProps {
|
||||
tag: string;
|
||||
tagStats?: TagStats;
|
||||
selectedServer: SelectedServer;
|
||||
displayed: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const TagCard = (
|
||||
DeleteTagConfirmModal: FC<TagModalProps>,
|
||||
EditTagModal: FC<TagModalProps>,
|
||||
ForServerVersion: FC<Versions>,
|
||||
colorGenerator: ColorGenerator,
|
||||
) => ({ tag, tagStats, selectedServer, displayed, toggle }: TagCardProps) => {
|
||||
const [ isDeleteModalOpen, toggleDelete ] = useToggle();
|
||||
const [ isEditModalOpen, toggleEdit ] = useToggle();
|
||||
|
||||
const serverId = isServerWithId(selectedServer) ? selectedServer.id : '';
|
||||
const shortUrlsLink = `/server/${serverId}/list-short-urls/1?tag=${tag}`;
|
||||
|
||||
return (
|
||||
<Card className="tag-card">
|
||||
<CardHeader className="tag-card__header">
|
||||
<Button color="light" size="sm" className="tag-card__btn tag-card__btn--last" onClick={toggleDelete}>
|
||||
<FontAwesomeIcon icon={deleteIcon} />
|
||||
</Button>
|
||||
<Button color="light" size="sm" className="tag-card__btn" onClick={toggleEdit}>
|
||||
<FontAwesomeIcon icon={editIcon} />
|
||||
</Button>
|
||||
<h5 className="tag-card__tag-title text-ellipsis">
|
||||
<TagBullet tag={tag} colorGenerator={colorGenerator} />
|
||||
<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>
|
||||
</CardHeader>
|
||||
|
||||
{tagStats && (
|
||||
<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/${serverId}/tag/${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>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagCard;
|
|
@ -1,91 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { splitEvery } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import Message from '../utils/Message';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { serverType } from '../servers/prop-types';
|
||||
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
|
||||
import { useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { TagsListType } from './reducers/tagsList';
|
||||
|
||||
const { ceil } = Math;
|
||||
const TAGS_GROUPS_AMOUNT = 4;
|
||||
|
||||
const propTypes = {
|
||||
filterTags: PropTypes.func,
|
||||
forceListTags: PropTypes.func,
|
||||
tagsList: TagsListType,
|
||||
selectedServer: serverType,
|
||||
createNewVisit: PropTypes.func,
|
||||
loadMercureInfo: PropTypes.func,
|
||||
mercureInfo: MercureInfoType,
|
||||
};
|
||||
|
||||
const TagsList = (TagCard) => {
|
||||
const TagListComp = (
|
||||
{ filterTags, forceListTags, tagsList, selectedServer, createNewVisit, loadMercureInfo, mercureInfo },
|
||||
) => {
|
||||
const [ displayedTag, setDisplayedTag ] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
forceListTags();
|
||||
}, []);
|
||||
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
|
||||
|
||||
const renderContent = () => {
|
||||
if (tagsList.loading) {
|
||||
return <Message noMargin loading />;
|
||||
}
|
||||
|
||||
if (tagsList.error) {
|
||||
return (
|
||||
<div className="col-12">
|
||||
<div className="bg-danger p-2 text-white text-center">Error loading tags :(</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tagsCount = tagsList.filteredTags.length;
|
||||
|
||||
if (tagsCount < 1) {
|
||||
return <Message>No tags found</Message>;
|
||||
}
|
||||
|
||||
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{tagsGroups.map((group, index) => (
|
||||
<div key={index} className="col-md-6 col-xl-3">
|
||||
{group.map((tag) => (
|
||||
<TagCard
|
||||
key={tag}
|
||||
tag={tag}
|
||||
tagStats={tagsList.stats[tag]}
|
||||
selectedServer={selectedServer}
|
||||
displayed={displayedTag === tag}
|
||||
toggle={() => setDisplayedTag(displayedTag !== tag ? tag : undefined)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!tagsList.loading && <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />}
|
||||
<div className="row">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
TagListComp.propTypes = propTypes;
|
||||
|
||||
return TagListComp;
|
||||
};
|
||||
|
||||
export default TagsList;
|
85
src/tags/TagsList.tsx
Normal file
85
src/tags/TagsList.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { splitEvery } from 'ramda';
|
||||
import Message from '../utils/Message';
|
||||
import SearchField from '../utils/SearchField';
|
||||
import { MercureInfo } from '../mercure/reducers/mercureInfo';
|
||||
import { useMercureTopicBinding } from '../mercure/helpers';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { TagsList as TagsListState } from './reducers/tagsList';
|
||||
import { TagCardProps } from './TagCard';
|
||||
|
||||
const { ceil } = Math;
|
||||
const TAGS_GROUPS_AMOUNT = 4;
|
||||
|
||||
export interface TagsListProps {
|
||||
filterTags: (searchTerm: string) => void;
|
||||
forceListTags: Function;
|
||||
tagsList: TagsListState;
|
||||
selectedServer: SelectedServer;
|
||||
createNewVisit: () => void;
|
||||
loadMercureInfo: Function;
|
||||
mercureInfo: MercureInfo;
|
||||
}
|
||||
|
||||
const TagsList = (TagCard: FC<TagCardProps>) => (
|
||||
{ filterTags, forceListTags, tagsList, selectedServer, createNewVisit, loadMercureInfo, mercureInfo }: TagsListProps,
|
||||
) => {
|
||||
const [ displayedTag, setDisplayedTag ] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
forceListTags();
|
||||
}, []);
|
||||
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
|
||||
|
||||
const renderContent = () => {
|
||||
if (tagsList.loading) {
|
||||
return <Message noMargin loading />;
|
||||
}
|
||||
|
||||
if (tagsList.error) {
|
||||
return (
|
||||
<div className="col-12">
|
||||
<div className="bg-danger p-2 text-white text-center">Error loading tags :(</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tagsCount = tagsList.filteredTags.length;
|
||||
|
||||
if (tagsCount < 1) {
|
||||
return <Message>No tags found</Message>;
|
||||
}
|
||||
|
||||
const tagsGroups = splitEvery(ceil(tagsCount / TAGS_GROUPS_AMOUNT), tagsList.filteredTags);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{tagsGroups.map((group, index) => (
|
||||
<div key={index} className="col-md-6 col-xl-3">
|
||||
{group.map((tag) => (
|
||||
<TagCard
|
||||
key={tag}
|
||||
tag={tag}
|
||||
tagStats={tagsList.stats[tag]}
|
||||
selectedServer={selectedServer}
|
||||
displayed={displayedTag === tag}
|
||||
toggle={() => setDisplayedTag(displayedTag !== tag ? tag : undefined)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!tagsList.loading && <SearchField className="mb-3" placeholder="Search tags..." onChange={filterTags} />}
|
||||
<div className="row">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsList;
|
|
@ -1,5 +1,4 @@
|
|||
import { isEmpty, reject } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCreation';
|
||||
import { buildReducer } from '../../utils/helpers/redux';
|
||||
|
@ -17,18 +16,6 @@ export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
|||
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
/** @deprecated Use TagsList interface instead */
|
||||
export const TagsListType = PropTypes.shape({
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
filteredTags: PropTypes.arrayOf(PropTypes.string),
|
||||
stats: PropTypes.objectOf(PropTypes.shape({
|
||||
shortUrlsCount: PropTypes.number,
|
||||
visitsCount: PropTypes.number,
|
||||
})), // Record
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
type TagsStatsMap = Record<string, TagStats>;
|
||||
|
||||
export interface TagsList {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import createTagCard from '../../src/tags/TagCard';
|
||||
import TagBullet from '../../src/tags/helpers/TagBullet';
|
||||
import ColorGenerator from '../../src/utils/services/ColorGenerator';
|
||||
import { ReachableServer } from '../../src/servers/data';
|
||||
|
||||
describe('<TagCard />', () => {
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper;
|
||||
const tagStats = {
|
||||
shortUrlsCount: 48,
|
||||
visitsCount: 23257,
|
||||
|
@ -14,9 +17,17 @@ describe('<TagCard />', () => {
|
|||
const EditTagModal = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => '', {});
|
||||
const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => null, Mock.all<ColorGenerator>());
|
||||
|
||||
wrapper = shallow(<TagCard tag="ssr" selectedServer={{ id: 1, serverNotFound: false }} tagStats={tagStats} />);
|
||||
wrapper = shallow(
|
||||
<TagCard
|
||||
tag="ssr"
|
||||
selectedServer={Mock.of<ReachableServer>({ id: '1' })}
|
||||
tagStats={tagStats}
|
||||
displayed={true}
|
||||
toggle={() => {}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => wrapper.unmount());
|
|
@ -1,30 +1,34 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { identity } from 'ramda';
|
||||
import createTagsList from '../../src/tags/TagsList';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import createTagsList, { TagsListProps } from '../../src/tags/TagsList';
|
||||
import Message from '../../src/utils/Message';
|
||||
import SearchField from '../../src/utils/SearchField';
|
||||
import { rangeOf } from '../../src/utils/utils';
|
||||
import { TagsList } from '../../src/tags/reducers/tagsList';
|
||||
|
||||
describe('<TagsList />', () => {
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper;
|
||||
const filterTags = jest.fn();
|
||||
const TagCard = () => '';
|
||||
const createWrapper = (tagsList) => {
|
||||
const params = { serverId: '1' };
|
||||
const TagsList = createTagsList(TagCard);
|
||||
const TagCard = () => null;
|
||||
const createWrapper = (tagsList: Partial<TagsList>) => {
|
||||
const TagsListComp = createTagsList(TagCard);
|
||||
|
||||
wrapper = shallow(
|
||||
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} />,
|
||||
<TagsListComp
|
||||
{...Mock.all<TagsListProps>()}
|
||||
forceListTags={identity}
|
||||
filterTags={filterTags}
|
||||
tagsList={Mock.of<TagsList>(tagsList)}
|
||||
/>,
|
||||
);
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.unmount();
|
||||
filterTags.mockReset();
|
||||
});
|
||||
afterEach(() => wrapper?.unmount());
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('shows a loading message when tags are being loaded', () => {
|
||||
const wrapper = createWrapper({ loading: true });
|
Loading…
Reference in a new issue