mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 01:37:24 +03:00
Migrated all remaining short-url elements to TS
This commit is contained in:
parent
4b33d39d44
commit
8a9c694fbc
24 changed files with 555 additions and 595 deletions
|
@ -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} />
|
||||
|
||||
{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;
|
175
src/short-urls/CreateShortUrl.tsx
Normal file
175
src/short-urls/CreateShortUrl.tsx
Normal 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} />
|
||||
|
||||
{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;
|
|
@ -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;
|
|
@ -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" />
|
||||
|
||||
{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;
|
78
src/short-urls/SearchBar.tsx
Normal file
78
src/short-urls/SearchBar.tsx
Normal 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" />
|
||||
|
||||
{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;
|
|
@ -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;
|
33
src/short-urls/ShortUrls.tsx
Normal file
33
src/short-urls/ShortUrls.tsx
Normal 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;
|
|
@ -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"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{renderShortUrls()}
|
||||
</tbody>
|
||||
</table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ShortUrlsListComp.propTypes = propTypes;
|
||||
|
||||
return ShortUrlsListComp;
|
||||
};
|
||||
|
||||
export default ShortUrlsList;
|
177
src/short-urls/ShortUrlsList.tsx
Normal file
177
src/short-urls/ShortUrlsList.tsx
Normal 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"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{renderShortUrls()}
|
||||
</tbody>
|
||||
</table>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortUrlsList;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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');
|
|
@ -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" />);
|
|
@ -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);
|
||||
});
|
|
@ -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());
|
||||
|
|
@ -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);
|
|
@ -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 />);
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue