Migrated all remaining short-url elements to TS

This commit is contained in:
Alejandro Celaya 2020-08-30 19:45:17 +02:00
parent 4b33d39d44
commit 8a9c694fbc
24 changed files with 555 additions and 595 deletions

View file

@ -1,171 +0,0 @@
import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, isNil, pipe, replace, trim } from 'ramda';
import React, { useState } from 'react';
import { Collapse, FormGroup, Input } from 'reactstrap';
import * as PropTypes from 'prop-types';
import DateInput from '../utils/DateInput';
import Checkbox from '../utils/Checkbox';
import { serverType } from '../servers/prop-types';
import { versionMatch } from '../utils/helpers/version';
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import { useToggle } from '../utils/helpers/hooks';
import { createShortUrlResultType } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
const normalizeTag = pipe(trim, replace(/ /g, '-'));
const formatDate = (date) => isNil(date) ? date : date.format();
const propTypes = {
createShortUrl: PropTypes.func,
shortUrlCreationResult: createShortUrlResultType,
resetCreateShortUrl: PropTypes.func,
selectedServer: serverType,
};
const initialState = {
longUrl: '',
tags: [],
customSlug: '',
shortCodeLength: '',
domain: '',
validSince: undefined,
validUntil: undefined,
maxVisits: '',
findIfExists: false,
};
const CreateShortUrl = (TagsSelector, CreateShortUrlResult, ForServerVersion) => {
const CreateShortUrlComp = ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }) => {
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle();
const changeTags = (tags) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlCreation(initialState);
const save = handleEventPreventingDefault(() => {
const shortUrlData = {
...shortUrlCreation,
validSince: formatDate(shortUrlCreation.validSince),
validUntil: formatDate(shortUrlCreation.validUntil),
};
createShortUrl(shortUrlData).then(reset).catch(() => {});
});
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => (
<FormGroup>
<Input
id={id}
type={type}
placeholder={placeholder}
value={shortUrlCreation[id]}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })}
{...props}
/>
</FormGroup>
);
const renderDateInput = (id, placeholder, props = {}) => (
<div className="form-group">
<DateInput
selected={shortUrlCreation[id]}
placeholderText={placeholder}
isClearable
onChange={(date) => setShortUrlCreation({ ...shortUrlCreation, [id]: date })}
{...props}
/>
</div>
);
const currentServerVersion = selectedServer && selectedServer.version;
const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' });
const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
return (
<form onSubmit={save}>
<div className="form-group">
<input
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={shortUrlCreation.longUrl}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })}
/>
</div>
<Collapse isOpen={moreOptionsVisible}>
<div className="form-group">
<TagsSelector tags={shortUrlCreation.tags} onChange={changeTags} />
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('customSlug', 'Custom slug')}
</div>
<div className="col-sm-4">
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
min: 4,
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
...disableShortCodeLength && {
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
},
})}
</div>
<div className="col-sm-4">
{renderOptionalInput('domain', 'Domain', 'text', {
disabled: disableDomain,
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
})}
</div>
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-4">
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil })}
</div>
<div className="col-sm-4">
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 text-right">
<Checkbox
className="mr-2"
checked={shortUrlCreation.findIfExists}
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForServerVersion>
</Collapse>
<div>
<button type="button" className="btn btn-outline-secondary" onClick={toggleMoreOptionsVisible}>
<FontAwesomeIcon icon={moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary float-right"
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
>
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
</button>
</div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
</form>
);
};
CreateShortUrlComp.propTypes = propTypes;
return CreateShortUrlComp;
};
export default CreateShortUrl;

View file

@ -0,0 +1,175 @@
import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, pipe, replace, trim } from 'ramda';
import React, { FC, useState } from 'react';
import { Collapse, FormGroup, Input } from 'reactstrap';
import { InputType } from 'reactstrap/lib/Input';
import * as m from 'moment';
import DateInput, { DateInputProps } from '../utils/DateInput';
import Checkbox from '../utils/Checkbox';
import { versionMatch, Versions } from '../utils/helpers/version';
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
import { useToggle } from '../utils/helpers/hooks';
import { isReachableServer, SelectedServer } from '../servers/data';
import { formatIsoDate } from '../utils/helpers/date';
import { ShortUrlData } from './data';
import { ShortUrlCreation } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
const normalizeTag = pipe(trim, replace(/ /g, '-'));
interface CreateShortUrlProps {
shortUrlCreationResult: ShortUrlCreation;
selectedServer: SelectedServer;
createShortUrl: Function;
resetCreateShortUrl: () => void;
}
const initialState: ShortUrlData = {
longUrl: '',
tags: [],
customSlug: '',
shortCodeLength: undefined,
domain: '',
validSince: undefined,
validUntil: undefined,
maxVisits: undefined,
findIfExists: false,
};
type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits';
type DateFields = 'validSince' | 'validUntil';
const CreateShortUrl = (
TagsSelector: FC<any>,
CreateShortUrlResult: FC<CreateShortUrlResultProps>,
ForServerVersion: FC<Versions>,
) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }: CreateShortUrlProps) => {
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle();
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlCreation(initialState);
const save = handleEventPreventingDefault(() => {
const shortUrlData = {
...shortUrlCreation,
validSince: formatIsoDate(shortUrlCreation.validSince),
validUntil: formatIsoDate(shortUrlCreation.validUntil),
};
createShortUrl(shortUrlData).then(reset).catch(() => {});
});
const renderOptionalInput = (id: NonDateFields, placeholder: string, type: InputType = 'text', props = {}) => (
<FormGroup>
<Input
id={id}
type={type}
placeholder={placeholder}
value={shortUrlCreation[id]}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })}
{...props}
/>
</FormGroup>
);
const renderDateInput = (id: DateFields, placeholder: string, props: Partial<DateInputProps> = {}) => (
<div className="form-group">
<DateInput
selected={shortUrlCreation[id] as m.Moment | null}
placeholderText={placeholder}
isClearable
onChange={(date) => setShortUrlCreation({ ...shortUrlCreation, [id]: date })}
{...props}
/>
</div>
);
const currentServerVersion = isReachableServer(selectedServer) ? selectedServer.version : '';
const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' });
const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
return (
<form onSubmit={save}>
<div className="form-group">
<input
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={shortUrlCreation.longUrl}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })}
/>
</div>
<Collapse isOpen={moreOptionsVisible}>
<div className="form-group">
<TagsSelector tags={shortUrlCreation.tags} onChange={changeTags} />
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('customSlug', 'Custom slug')}
</div>
<div className="col-sm-4">
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
min: 4,
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
...disableShortCodeLength && {
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
},
})}
</div>
<div className="col-sm-4">
{renderOptionalInput('domain', 'Domain', 'text', {
disabled: disableDomain,
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
})}
</div>
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-4">
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
</div>
<div className="col-sm-4">
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 text-right">
<Checkbox
className="mr-2"
checked={shortUrlCreation.findIfExists}
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForServerVersion>
</Collapse>
<div>
<button type="button" className="btn btn-outline-secondary" onClick={toggleMoreOptionsVisible}>
<FontAwesomeIcon icon={moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary float-right"
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
>
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
</button>
</div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
</form>
);
};
export default CreateShortUrl;

View file

@ -1,20 +1,17 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import PropTypes from 'prop-types';
import { pageIsEllipsis, keyForPage, progressivePagination } from '../utils/helpers/pagination';
import { ShlinkPaginator } from '../utils/services/types';
import './Paginator.scss';
const propTypes = {
serverId: PropTypes.string.isRequired,
paginator: PropTypes.shape({
currentPage: PropTypes.number,
pagesCount: PropTypes.number,
}),
};
interface PaginatorProps {
paginator?: ShlinkPaginator;
serverId: string;
}
const Paginator = ({ paginator = {}, serverId }) => {
const { currentPage, pagesCount = 0 } = paginator;
const Paginator = ({ paginator, serverId }: PaginatorProps) => {
const { currentPage = 0, pagesCount = 0 } = paginator ?? {};
if (pagesCount <= 1) {
return null;
@ -57,6 +54,4 @@ const Paginator = ({ paginator = {}, serverId }) => {
);
};
Paginator.propTypes = propTypes;
export default Paginator;

View file

@ -1,81 +0,0 @@
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import { isEmpty, pipe } from 'ramda';
import PropTypes from 'prop-types';
import moment from 'moment';
import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag';
import DateRangeRow from '../utils/DateRangeRow';
import { formatDate } from '../utils/helpers/date';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './SearchBar.scss';
const propTypes = {
listShortUrls: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
};
const dateOrUndefined = (date) => date ? moment(date) : undefined;
const SearchBar = (colorGenerator, ForServerVersion) => {
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
const selectedTags = shortUrlsListParams.tags || [];
const setDate = (dateName) => pipe(
formatDate(),
(date) => listShortUrls({ ...shortUrlsListParams, [dateName]: date }),
);
return (
<div className="search-bar-container">
<SearchField
onChange={
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
}
/>
<ForServerVersion minVersion="1.21.0">
<div className="mt-3">
<div className="row">
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
<DateRangeRow
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
onStartDateChange={setDate('startDate')}
onEndDateChange={setDate('endDate')}
/>
</div>
</div>
</div>
</ForServerVersion>
{!isEmpty(selectedTags) && (
<h4 className="search-bar__selected-tag mt-3">
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
&nbsp;
{selectedTags.map((tag) => (
<Tag
colorGenerator={colorGenerator}
key={tag}
text={tag}
clearable
onClose={() => listShortUrls(
{
...shortUrlsListParams,
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
},
)}
/>
))}
</h4>
)}
</div>
);
};
SearchBar.propTypes = propTypes;
return SearchBar;
};
export default SearchBar;

View file

@ -0,0 +1,78 @@
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { FC } from 'react';
import { isEmpty, pipe } from 'ramda';
import moment from 'moment';
import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag';
import DateRangeRow from '../utils/DateRangeRow';
import { formatDate } from '../utils/helpers/date';
import ColorGenerator from '../utils/services/ColorGenerator';
import { Versions } from '../utils/helpers/version';
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';
import './SearchBar.scss';
interface SearchBarProps {
listShortUrls: (params: ShortUrlsListParams) => void;
shortUrlsListParams: ShortUrlsListParams;
}
const dateOrUndefined = (date?: string) => date ? moment(date) : undefined;
const SearchBar = (colorGenerator: ColorGenerator, ForServerVersion: FC<Versions>) => (
{ listShortUrls, shortUrlsListParams }: SearchBarProps,
) => {
const selectedTags = shortUrlsListParams.tags ?? [];
const setDate = (dateName: 'startDate' | 'endDate') => pipe(
formatDate(),
(date) => listShortUrls({ ...shortUrlsListParams, [dateName]: date }),
);
return (
<div className="search-bar-container">
<SearchField
onChange={
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
}
/>
<ForServerVersion minVersion="1.21.0">
<div className="mt-3">
<div className="row">
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
<DateRangeRow
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
onStartDateChange={setDate('startDate')}
onEndDateChange={setDate('endDate')}
/>
</div>
</div>
</div>
</ForServerVersion>
{!isEmpty(selectedTags) && (
<h4 className="search-bar__selected-tag mt-3">
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
&nbsp;
{selectedTags.map((tag) => (
<Tag
colorGenerator={colorGenerator}
key={tag}
text={tag}
clearable
onClose={() => listShortUrls(
{
...shortUrlsListParams,
tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
},
)}
/>
))}
</h4>
)}
</div>
);
};
export default SearchBar;

View file

@ -1,41 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Paginator from './Paginator';
const ShortUrls = (SearchBar, ShortUrlsList) => {
const propTypes = {
match: PropTypes.shape({
params: PropTypes.object,
}),
shortUrlsList: PropTypes.object,
};
const ShortUrlsComponent = (props) => {
const { match: { params }, shortUrlsList } = props;
const { page, serverId } = params;
const { data = [], pagination } = shortUrlsList;
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
// Using a key on a component makes react to create a new instance every time the key changes
// Without it, pagination on the URL will not make the component to be refreshed
useEffect(() => {
setUrlsListKey(`${serverId}_${page}`);
}, [ serverId, page ]);
return (
<React.Fragment>
<div className="form-group"><SearchBar /></div>
<div>
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</div>
</React.Fragment>
);
};
ShortUrlsComponent.propTypes = propTypes;
return ShortUrlsComponent;
};
export default ShortUrls;

View file

@ -0,0 +1,33 @@
import React, { FC, useEffect, useState } from 'react';
import { ShlinkShortUrlsResponse } from '../utils/services/types';
import Paginator from './Paginator';
import { ShortUrlsListProps, WithList } from './ShortUrlsList';
interface ShortUrlsProps extends ShortUrlsListProps {
shortUrlsList?: ShlinkShortUrlsResponse;
}
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps & WithList>) => (props: ShortUrlsProps) => {
const { match, shortUrlsList } = props;
const { page = '1', serverId = '' } = match?.params ?? {};
const { data = [], pagination } = shortUrlsList ?? {};
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
// Using a key on a component makes react to create a new instance every time the key changes
// Without it, pagination on the URL will not make the component to be refreshed
useEffect(() => {
setUrlsListKey(`${serverId}_${page}`);
}, [ serverId, page ]);
return (
<React.Fragment>
<div className="form-group"><SearchBar /></div>
<div>
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</div>
</React.Fragment>
);
};
export default ShortUrls;

View file

@ -1,178 +0,0 @@
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { head, isEmpty, keys, values } from 'ramda';
import React, { useState, useEffect } from 'react';
import qs from 'qs';
import PropTypes from 'prop-types';
import { serverType } from '../servers/prop-types';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir } from '../utils/utils';
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
import { useMercureTopicBinding } from '../mercure/helpers';
import { shortUrlType } from './reducers/shortUrlsList';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './ShortUrlsList.scss';
export const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
longUrl: 'Long URL',
visits: 'Visits',
};
const propTypes = {
listShortUrls: PropTypes.func,
resetShortUrlParams: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
match: PropTypes.object,
location: PropTypes.object,
loading: PropTypes.bool,
error: PropTypes.bool,
shortUrlsList: PropTypes.arrayOf(shortUrlType),
selectedServer: serverType,
createNewVisit: PropTypes.func,
loadMercureInfo: PropTypes.func,
mercureInfo: MercureInfoType,
};
// FIXME Replace with typescript: (ShortUrlsRow component)
const ShortUrlsList = (ShortUrlsRow) => {
const ShortUrlsListComp = ({
listShortUrls,
resetShortUrlParams,
shortUrlsListParams,
match,
location,
loading,
error,
shortUrlsList,
selectedServer,
createNewVisit,
loadMercureInfo,
mercureInfo,
}) => {
const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState({
orderField: orderBy && head(keys(orderBy)),
orderDir: orderBy && head(values(orderBy)),
});
const refreshList = (extraParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (orderField, orderDir) => {
setOrder({ orderField, orderDir });
refreshList({ orderBy: { [orderField]: orderDir } });
};
const orderByColumn = (columnName) => () =>
handleOrderBy(columnName, determineOrderDir(columnName, order.orderField, order.orderDir));
const renderOrderIcon = (field) => {
if (order.orderField !== field) {
return null;
}
if (!order.orderDir) {
return null;
}
return (
<FontAwesomeIcon
icon={order.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
className="short-urls-list__header-icon"
/>
);
};
const renderShortUrls = () => {
if (error) {
return (
<tr>
<td colSpan="6" className="text-center table-danger">Something went wrong while loading short URLs :(</td>
</tr>
);
}
if (loading) {
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
}
if (!loading && isEmpty(shortUrlsList)) {
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
}
return shortUrlsList.map((shortUrl) => (
<ShortUrlsRow
key={shortUrl.shortUrl}
shortUrl={shortUrl}
selectedServer={selectedServer}
refreshList={refreshList}
shortUrlsListParams={shortUrlsListParams}
/>
));
};
useEffect(() => {
const { params } = match;
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
const tags = query.tag ? [ query.tag ] : shortUrlsListParams.tags;
refreshList({ page: params.page, tags });
return resetShortUrlParams;
}, []);
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
return (
<React.Fragment>
<div className="d-block d-md-none mb-3">
<SortingDropdown
items={SORTABLE_FIELDS}
orderField={order.orderField}
orderDir={order.orderDir}
onChange={handleOrderBy}
/>
</div>
<table className="table table-striped table-hover">
<thead className="short-urls-list__header">
<tr>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('dateCreated')}
>
{renderOrderIcon('dateCreated')}
Created at
</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('shortCode')}
>
{renderOrderIcon('shortCode')}
Short URL
</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('longUrl')}
>
{renderOrderIcon('longUrl')}
Long URL
</th>
<th className="short-urls-list__header-cell">Tags</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('visits')}
>
<span className="indivisible">{renderOrderIcon('visits')} Visits</span>
</th>
<th className="short-urls-list__header-cell">&nbsp;</th>
</tr>
</thead>
<tbody>
{renderShortUrls()}
</tbody>
</table>
</React.Fragment>
);
};
ShortUrlsListComp.propTypes = propTypes;
return ShortUrlsListComp;
};
export default ShortUrlsList;

View file

@ -0,0 +1,177 @@
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { head, isEmpty, keys, values } from 'ramda';
import React, { useState, useEffect, FC } from 'react';
import qs from 'qs';
import { RouteChildrenProps } from 'react-router';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, OrderDir } from '../utils/utils';
import { MercureInfo } from '../mercure/reducers/mercureInfo';
import { useMercureTopicBinding } from '../mercure/helpers';
import { SelectedServer } from '../servers/data';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
import { ShortUrl } from './data';
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';
import './ShortUrlsList.scss';
export const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
longUrl: 'Long URL',
visits: 'Visits',
};
type OrderableFields = keyof typeof SORTABLE_FIELDS;
interface RouteParams {
page: string;
serverId: string;
}
export interface WithList {
shortUrlsList: ShortUrl[];
}
export interface ShortUrlsListProps extends ShortUrlsListState, RouteChildrenProps<RouteParams> {
selectedServer: SelectedServer;
listShortUrls: (params: ShortUrlsListParams) => void;
shortUrlsListParams: ShortUrlsListParams;
resetShortUrlParams: () => void;
createNewVisit: (message: any) => void;
loadMercureInfo: Function;
mercureInfo: MercureInfo;
}
const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
listShortUrls,
resetShortUrlParams,
shortUrlsListParams,
match,
location,
loading,
error,
shortUrlsList,
selectedServer,
createNewVisit,
loadMercureInfo,
mercureInfo,
}: ShortUrlsListProps & WithList) => {
const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState<{ orderField?: OrderableFields; orderDir?: OrderDir }>({
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
orderDir: orderBy && head(values(orderBy)),
});
const refreshList = (extraParams: ShortUrlsListParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
const handleOrderBy = (orderField: OrderableFields, orderDir: OrderDir) => {
setOrder({ orderField, orderDir });
refreshList({ orderBy: { [orderField]: orderDir } });
};
const orderByColumn = (field: OrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.orderField, order.orderDir));
const renderOrderIcon = (field: OrderableFields) => {
if (order.orderField !== field) {
return null;
}
if (!order.orderDir) {
return null;
}
return (
<FontAwesomeIcon
icon={order.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
className="short-urls-list__header-icon"
/>
);
};
const renderShortUrls = () => {
if (error) {
return (
<tr>
<td colSpan={6} className="text-center table-danger">Something went wrong while loading short URLs :(</td>
</tr>
);
}
if (loading) {
return <tr><td colSpan={6} className="text-center">Loading...</td></tr>;
}
if (!loading && isEmpty(shortUrlsList)) {
return <tr><td colSpan={6} className="text-center">No results found</td></tr>;
}
return shortUrlsList.map((shortUrl) => (
<ShortUrlsRow
key={shortUrl.shortUrl}
shortUrl={shortUrl}
selectedServer={selectedServer}
refreshList={refreshList}
shortUrlsListParams={shortUrlsListParams}
/>
));
};
useEffect(() => {
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
const tags = query.tag ? [ query.tag as string ] : shortUrlsListParams.tags;
refreshList({ page: match?.params.page, tags });
return resetShortUrlParams;
}, []);
useMercureTopicBinding(mercureInfo, 'https://shlink.io/new-visit', createNewVisit, loadMercureInfo);
return (
<React.Fragment>
<div className="d-block d-md-none mb-3">
<SortingDropdown
items={SORTABLE_FIELDS}
orderField={order.orderField}
orderDir={order.orderDir}
onChange={handleOrderBy}
/>
</div>
<table className="table table-striped table-hover">
<thead className="short-urls-list__header">
<tr>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('dateCreated')}
>
{renderOrderIcon('dateCreated')}
Created at
</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('shortCode')}
>
{renderOrderIcon('shortCode')}
Short URL
</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('longUrl')}
>
{renderOrderIcon('longUrl')}
Long URL
</th>
<th className="short-urls-list__header-cell">Tags</th>
<th
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={orderByColumn('visits')}
>
<span className="indivisible">{renderOrderIcon('visits')} Visits</span>
</th>
<th className="short-urls-list__header-cell">&nbsp;</th>
</tr>
</thead>
<tbody>
{renderShortUrls()}
</tbody>
</table>
</React.Fragment>
);
};
export default ShortUrlsList;

View file

@ -5,7 +5,7 @@ import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import './UseExistingIfFoundInfoIcon.scss';
import { useToggle } from '../utils/helpers/hooks';
const renderInfoModal = (isOpen, toggle) => (
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (
<Modal isOpen={isOpen} toggle={toggle} centered size="lg">
<ModalHeader toggle={toggle}>Info</ModalHeader>
<ModalBody>
@ -45,7 +45,7 @@ const UseExistingIfFoundInfoIcon = () => {
<span title="What does this mean?">
<FontAwesomeIcon icon={infoIcon} style={{ cursor: 'pointer' }} onClick={toggleModal} />
</span>
{renderInfoModal(isModalOpen, toggleModal)}
<InfoModal isOpen={isModalOpen} toggle={toggleModal} />
</React.Fragment>
);
};

View file

@ -1,3 +1,4 @@
import * as m from 'moment';
import { Nullable, OptionalString } from '../../utils/utils';
export interface ShortUrlData {
@ -6,8 +7,8 @@ export interface ShortUrlData {
customSlug?: string;
shortCodeLength?: number;
domain?: string;
validSince?: string;
validUntil?: string;
validSince?: m.Moment | string;
validUntil?: m.Moment | string;
maxVisits?: number;
findIfExists?: boolean;
}

View file

@ -5,11 +5,11 @@ import React, { useEffect } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { Card, CardBody, Tooltip } from 'reactstrap';
import { ShortUrlCreation } from '../reducers/shortUrlCreation';
import './CreateShortUrlResult.scss';
import { StateFlagTimeout } from '../../utils/helpers/hooks';
import './CreateShortUrlResult.scss';
interface CreateShortUrlResultProps extends ShortUrlCreation {
resetCreateShortUrl: Function;
export interface CreateShortUrlResultProps extends ShortUrlCreation {
resetCreateShortUrl: () => void;
}
const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => (

View file

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import { Action, Dispatch } from 'redux';
import { GetState } from '../../container/types';
import { ShortUrl, ShortUrlData } from '../data';
@ -12,15 +11,6 @@ export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
export const RESET_CREATE_SHORT_URL = 'shlink/createShortUrl/RESET_CREATE_SHORT_URL';
/* eslint-enable padding-line-between-statements */
/** @deprecated Use ShortUrlCreation interface instead */
export const createShortUrlResultType = PropTypes.shape({
result: PropTypes.shape({
shortUrl: PropTypes.string,
}),
saving: PropTypes.bool,
error: PropTypes.bool,
});
export interface ShortUrlCreation {
result: ShortUrl | null;
saving: boolean;

View file

@ -7,6 +7,7 @@ import { ShortUrl, ShortUrlIdentifier } from '../data';
import { buildReducer } from '../../utils/helpers/redux';
import { GetState } from '../../container/types';
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
import { ShlinkShortUrlsResponse } from '../../utils/services/types';
import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags';
import { SHORT_URL_DELETED } from './shortUrlDeletion';
import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction, shortUrlMetaType } from './shortUrlMeta';
@ -30,18 +31,14 @@ export const shortUrlType = PropTypes.shape({
domain: PropTypes.string,
});
interface ShortUrlsData {
data: ShortUrl[];
}
export interface ShortUrlsList {
shortUrls: ShortUrlsData;
shortUrls?: ShlinkShortUrlsResponse;
loading: boolean;
error: boolean;
}
export interface ListShortUrlsAction extends Action<string> {
shortUrls: ShortUrlsData;
shortUrls: ShlinkShortUrlsResponse;
params: ShortUrlsListParams;
}
@ -50,9 +47,6 @@ export type ListShortUrlsCombinedAction = (
);
const initialState: ShortUrlsList = {
shortUrls: {
data: [],
},
loading: true,
error: false,
};
@ -60,7 +54,7 @@ const initialState: ShortUrlsList = {
const setPropFromActionOnMatchingShortUrl = <T extends ShortUrlIdentifier>(prop: keyof T) => (
state: ShortUrlsList,
{ shortCode, domain, [prop]: propValue }: T,
): ShortUrlsList => assocPath(
): ShortUrlsList => !state.shortUrls ? state : assocPath(
[ 'shortUrls', 'data' ],
state.shortUrls.data.map(
(shortUrl: ShortUrl) =>
@ -71,9 +65,9 @@ const setPropFromActionOnMatchingShortUrl = <T extends ShortUrlIdentifier>(prop:
export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }),
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true, shortUrls: { data: [] } }),
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
[SHORT_URL_DELETED]: (state, { shortCode, domain }) => assocPath(
[SHORT_URL_DELETED]: (state, { shortCode, domain }) => !state.shortUrls ? state : assocPath(
[ 'shortUrls', 'data' ],
reject((shortUrl) => shortUrlMatches(shortUrl, shortCode, domain), state.shortUrls.data),
state,

View file

@ -1,26 +1,16 @@
import PropTypes from 'prop-types';
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { OrderDir } from '../../utils/utils';
import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList';
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
/** @deprecated Use ShortUrlsListParams interface instead */
export const shortUrlsListParamsType = PropTypes.shape({
page: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
searchTerm: PropTypes.string,
startDate: PropTypes.string,
endDate: PropTypes.string,
orderBy: PropTypes.object,
});
export interface ShortUrlsListParams {
page?: string;
tags?: string[];
searchTerm?: string;
startDate?: string;
endDate?: string;
orderBy?: string | Record<string, 'ASC' | 'DESC'>;
orderBy?: Record<string, OrderDir>;
}
const initialState: ShortUrlsListParams = { page: '1' };

View file

@ -19,13 +19,13 @@ import { editShortUrlTags, resetShortUrlsTags } from '../reducers/shortUrlTags';
import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta';
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
import { editShortUrl } from '../reducers/shortUrlEdition';
import { ConnectDecorator } from '../../container/types';
import { ConnectDecorator, ShlinkState } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
bottle.decorator('ShortUrls', reduxConnect(
(state: any) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList),
(state: ShlinkState) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList),
));
// Services

View file

@ -3,7 +3,7 @@ import { SyntheticEvent } from 'react';
export type OrderDir = 'ASC' | 'DESC' | undefined;
export const determineOrderDir = (currentField: string, newField: string, currentOrderDir?: OrderDir): OrderDir => {
export const determineOrderDir = (currentField: string, newField?: string, currentOrderDir?: OrderDir): OrderDir => {
if (currentField !== newField) {
return 'ASC';
}

View file

@ -1,29 +1,32 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import moment from 'moment';
import { identity } from 'ramda';
import { Mock } from 'ts-mockery';
import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl';
import DateInput from '../../src/utils/DateInput';
import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
describe('<CreateShortUrl />', () => {
let wrapper;
const TagsSelector = () => '';
const shortUrlCreationResult = {
loading: false,
};
const createShortUrl = jest.fn(() => Promise.resolve());
let wrapper: ShallowWrapper;
const TagsSelector = () => null;
const shortUrlCreationResult = Mock.all<ShortUrlCreation>();
const createShortUrl = jest.fn(async () => Promise.resolve());
beforeEach(() => {
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '', () => '');
const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => null, () => null);
wrapper = shallow(
<CreateShortUrl shortUrlCreationResult={shortUrlCreationResult} createShortUrl={createShortUrl} />,
<CreateShortUrl
shortUrlCreationResult={shortUrlCreationResult}
createShortUrl={createShortUrl}
selectedServer={null}
resetCreateShortUrl={() => {}}
/>,
);
});
afterEach(() => {
wrapper.unmount();
createShortUrl.mockClear();
});
afterEach(() => wrapper.unmount());
afterEach(jest.clearAllMocks);
it('saves short URL with data set in form controls', () => {
const validSince = moment('2017-01-01');

View file

@ -1,12 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { PaginationItem } from 'reactstrap';
import Paginator from '../../src/short-urls/Paginator';
describe('<Paginator />', () => {
let wrapper;
let wrapper: ShallowWrapper;
afterEach(() => wrapper && wrapper.unmount());
afterEach(() => wrapper?.unmount());
it('renders nothing if the number of pages is below 2', () => {
wrapper = shallow(<Paginator serverId="abc123" />);

View file

@ -1,34 +1,34 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import searchBarCreator from '../../src/short-urls/SearchBar';
import SearchField from '../../src/utils/SearchField';
import Tag from '../../src/tags/helpers/Tag';
import DateRangeRow from '../../src/utils/DateRangeRow';
import ColorGenerator from '../../src/utils/services/ColorGenerator';
describe('<SearchBar />', () => {
let wrapper;
let wrapper: ShallowWrapper;
const listShortUrlsMock = jest.fn();
const SearchBar = searchBarCreator({}, () => '');
const SearchBar = searchBarCreator(Mock.all<ColorGenerator>(), () => null);
afterEach(() => {
listShortUrlsMock.mockReset();
wrapper && wrapper.unmount();
});
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders a SearchField', () => {
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
expect(wrapper.find(SearchField)).toHaveLength(1);
});
it('renders a DateRangeRow', () => {
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
expect(wrapper.find(DateRangeRow)).toHaveLength(1);
});
it('renders no tags when the list of tags is empty', () => {
wrapper = shallow(<SearchBar shortUrlsListParams={{}} />);
wrapper = shallow(<SearchBar shortUrlsListParams={{}} listShortUrls={listShortUrlsMock} />);
expect(wrapper.find(Tag)).toHaveLength(0);
});
@ -36,7 +36,7 @@ describe('<SearchBar />', () => {
it('renders the proper amount of tags', () => {
const tags = [ 'foo', 'bar', 'baz' ];
wrapper = shallow(<SearchBar shortUrlsListParams={{ tags }} />);
wrapper = shallow(<SearchBar shortUrlsListParams={{ tags }} listShortUrls={listShortUrlsMock} />);
expect(wrapper.find(Tag)).toHaveLength(tags.length);
});

View file

@ -1,22 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import shortUrlsCreator from '../../src/short-urls/ShortUrls';
import Paginator from '../../src/short-urls/Paginator';
import { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
describe('<ShortUrls />', () => {
let wrapper;
const SearchBar = () => '';
const ShortUrlsList = () => '';
let wrapper: ShallowWrapper;
const SearchBar = () => null;
const ShortUrlsList = () => null;
beforeEach(() => {
const params = {
serverId: '1',
page: '1',
};
const ShortUrls = shortUrlsCreator(SearchBar, ShortUrlsList);
wrapper = shallow(<ShortUrls match={{ params }} shortUrlsList={{ data: [] }} />);
wrapper = shallow(
<ShortUrls {...Mock.all<ShortUrlsListProps>()} />,
);
});
afterEach(() => wrapper.unmount());

View file

@ -1,12 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
import shortUrlsListCreator, { SORTABLE_FIELDS } from '../../src/short-urls/ShortUrlsList';
import { Mock } from 'ts-mockery';
import shortUrlsListCreator, { ShortUrlsListProps, SORTABLE_FIELDS } from '../../src/short-urls/ShortUrlsList';
import { ShortUrl } from '../../src/short-urls/data';
describe('<ShortUrlsList />', () => {
let wrapper;
const ShortUrlsRow = () => '';
let wrapper: ShallowWrapper;
const ShortUrlsRow = () => null;
const listShortUrlsMock = jest.fn();
const resetShortUrlParamsMock = jest.fn();
@ -15,6 +17,7 @@ describe('<ShortUrlsList />', () => {
beforeEach(() => {
wrapper = shallow(
<ShortUrlsList
{...Mock.all<ShortUrlsListProps>()}
listShortUrls={listShortUrlsMock}
resetShortUrlParams={resetShortUrlParamsMock}
shortUrlsListParams={{
@ -22,29 +25,27 @@ describe('<ShortUrlsList />', () => {
tags: [ 'test tag' ],
searchTerm: 'example.com',
}}
match={{ params: {} }}
location={{}}
match={{ params: {} } as any}
location={{} as any}
loading={false}
error={false}
shortUrlsList={
[
{
Mock.of<ShortUrl>({
shortCode: 'testShortCode',
shortUrl: 'https://www.example.com/testShortUrl',
longUrl: 'https://www.example.com/testLongUrl',
tags: [ 'test tag' ],
},
}),
]
}
mercureInfo={{ loading: true }}
mercureInfo={{ loading: true } as any}
/>,
);
});
afterEach(() => {
jest.resetAllMocks();
wrapper && wrapper.unmount();
});
afterEach(jest.resetAllMocks);
afterEach(() => wrapper?.unmount());
it('wraps a ShortUrlsList with 1 ShortUrlsRow', () => {
expect(wrapper.find(ShortUrlsRow)).toHaveLength(1);
@ -71,11 +72,11 @@ describe('<ShortUrlsList />', () => {
});
it('should render 6 table header cells with conditional order by icon', () => {
const getThElementForSortableField = (sortableField) => wrapper.find('table')
const getThElementForSortableField = (sortableField: string) => wrapper.find('table')
.find('thead')
.find('tr')
.find('th')
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField]));
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField as keyof typeof SORTABLE_FIELDS]));
Object.keys(SORTABLE_FIELDS).forEach((sortableField) => {
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0);

View file

@ -1,11 +1,11 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, ReactWrapper } from 'enzyme';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Modal } from 'reactstrap';
import UseExistingIfFoundInfoIcon from '../../src/short-urls/UseExistingIfFoundInfoIcon';
describe('<UseExistingIfFoundInfoIcon />', () => {
let wrapped;
let wrapped: ReactWrapper;
beforeEach(() => {
wrapped = mount(<UseExistingIfFoundInfoIcon />);

View file

@ -11,14 +11,12 @@ import { SHORT_URL_META_EDITED } from '../../../src/short-urls/reducers/shortUrl
import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation';
import { ShortUrl } from '../../../src/short-urls/data';
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
import { ShlinkShortUrlsResponse } from '../../../src/utils/services/types';
describe('shortUrlsListReducer', () => {
describe('reducer', () => {
it('returns loading on LIST_SHORT_URLS_START', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS_START } as any)).toEqual({
shortUrls: {
data: [],
},
loading: true,
error: false,
}));
@ -32,9 +30,6 @@ describe('shortUrlsListReducer', () => {
it('returns error on LIST_SHORT_URLS_ERROR', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS_ERROR } as any)).toEqual({
shortUrls: {
data: [],
},
loading: false,
error: true,
}));
@ -43,13 +38,13 @@ describe('shortUrlsListReducer', () => {
const shortCode = 'abc123';
const tags = [ 'foo', 'bar', 'baz' ];
const state = {
shortUrls: {
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
data: [
Mock.of<ShortUrl>({ shortCode, tags: [] }),
Mock.of<ShortUrl>({ shortCode, tags: [], domain: 'example.com' }),
Mock.of<ShortUrl>({ shortCode: 'foo', tags: [] }),
],
},
}),
loading: false,
error: false,
};
@ -75,13 +70,13 @@ describe('shortUrlsListReducer', () => {
validSince: '2020-05-05',
};
const state = {
shortUrls: {
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
data: [
Mock.of<ShortUrl>({ shortCode, meta: { maxVisits: 10 }, domain }),
Mock.of<ShortUrl>({ shortCode, meta: { maxVisits: 50 } }),
Mock.of<ShortUrl>({ shortCode: 'foo', meta: {} }),
],
},
}),
loading: false,
error: false,
};
@ -102,13 +97,13 @@ describe('shortUrlsListReducer', () => {
it('removes matching URL on SHORT_URL_DELETED', () => {
const shortCode = 'abc123';
const state = {
shortUrls: {
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
data: [
Mock.of<ShortUrl>({ shortCode }),
Mock.of<ShortUrl>({ shortCode, domain: 'example.com' }),
Mock.of<ShortUrl>({ shortCode: 'foo' }),
],
},
}),
loading: false,
error: false,
};
@ -129,13 +124,13 @@ describe('shortUrlsListReducer', () => {
visitsCount: 11,
};
const state = {
shortUrls: {
shortUrls: Mock.of<ShlinkShortUrlsResponse>({
data: [
Mock.of<ShortUrl>({ shortCode, domain: 'example.com', visitsCount: 5 }),
Mock.of<ShortUrl>({ shortCode, visitsCount: 10 }),
Mock.of<ShortUrl>({ shortCode: 'foo', visitsCount: 8 }),
],
},
}),
loading: false,
error: false,
};