diff --git a/src/short-urls/CreateShortUrl.js b/src/short-urls/CreateShortUrl.js
deleted file mode 100644
index e1591fd1..00000000
--- a/src/short-urls/CreateShortUrl.js
+++ /dev/null
@@ -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 = {}) => (
-
- setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })}
- {...props}
- />
-
- );
- const renderDateInput = (id, placeholder, props = {}) => (
-
- setShortUrlCreation({ ...shortUrlCreation, [id]: date })}
- {...props}
- />
-
- );
-
- const currentServerVersion = selectedServer && selectedServer.version;
- const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' });
- const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
-
- return (
-
- );
- };
-
- CreateShortUrlComp.propTypes = propTypes;
-
- return CreateShortUrlComp;
-};
-
-export default CreateShortUrl;
diff --git a/src/short-urls/CreateShortUrl.tsx b/src/short-urls/CreateShortUrl.tsx
new file mode 100644
index 00000000..93f8ee0f
--- /dev/null
+++ b/src/short-urls/CreateShortUrl.tsx
@@ -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,
+ CreateShortUrlResult: FC,
+ ForServerVersion: FC,
+) => ({ 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 = {}) => (
+
+ setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })}
+ {...props}
+ />
+
+ );
+ const renderDateInput = (id: DateFields, placeholder: string, props: Partial = {}) => (
+
+ setShortUrlCreation({ ...shortUrlCreation, [id]: date })}
+ {...props}
+ />
+
+ );
+
+ 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 (
+
+ );
+};
+
+export default CreateShortUrl;
diff --git a/src/short-urls/Paginator.js b/src/short-urls/Paginator.tsx
similarity index 80%
rename from src/short-urls/Paginator.js
rename to src/short-urls/Paginator.tsx
index 4b051811..eeccb727 100644
--- a/src/short-urls/Paginator.js
+++ b/src/short-urls/Paginator.tsx
@@ -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;
diff --git a/src/short-urls/SearchBar.js b/src/short-urls/SearchBar.js
deleted file mode 100644
index 53232f4e..00000000
--- a/src/short-urls/SearchBar.js
+++ /dev/null
@@ -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 (
-
-
listShortUrls({ ...shortUrlsListParams, searchTerm })
- }
- />
-
-
-
-
-
- {!isEmpty(selectedTags) && (
-
-
-
- {selectedTags.map((tag) => (
- listShortUrls(
- {
- ...shortUrlsListParams,
- tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
- },
- )}
- />
- ))}
-
- )}
-
- );
- };
-
- SearchBar.propTypes = propTypes;
-
- return SearchBar;
-};
-
-export default SearchBar;
diff --git a/src/short-urls/SearchBar.tsx b/src/short-urls/SearchBar.tsx
new file mode 100644
index 00000000..85150608
--- /dev/null
+++ b/src/short-urls/SearchBar.tsx
@@ -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) => (
+ { listShortUrls, shortUrlsListParams }: SearchBarProps,
+) => {
+ const selectedTags = shortUrlsListParams.tags ?? [];
+ const setDate = (dateName: 'startDate' | 'endDate') => pipe(
+ formatDate(),
+ (date) => listShortUrls({ ...shortUrlsListParams, [dateName]: date }),
+ );
+
+ return (
+
+
listShortUrls({ ...shortUrlsListParams, searchTerm })
+ }
+ />
+
+
+
+
+
+ {!isEmpty(selectedTags) && (
+
+
+
+ {selectedTags.map((tag) => (
+ listShortUrls(
+ {
+ ...shortUrlsListParams,
+ tags: selectedTags.filter((selectedTag) => selectedTag !== tag),
+ },
+ )}
+ />
+ ))}
+
+ )}
+
+ );
+};
+
+export default SearchBar;
diff --git a/src/short-urls/ShortUrls.js b/src/short-urls/ShortUrls.js
deleted file mode 100644
index a4cc23c4..00000000
--- a/src/short-urls/ShortUrls.js
+++ /dev/null
@@ -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 (
-
-
-
-
- );
- };
-
- ShortUrlsComponent.propTypes = propTypes;
-
- return ShortUrlsComponent;
-};
-
-export default ShortUrls;
diff --git a/src/short-urls/ShortUrls.tsx b/src/short-urls/ShortUrls.tsx
new file mode 100644
index 00000000..0a283bb1
--- /dev/null
+++ b/src/short-urls/ShortUrls.tsx
@@ -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) => (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 (
+
+
+
+
+ );
+};
+
+export default ShortUrls;
diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js
deleted file mode 100644
index a7b275e9..00000000
--- a/src/short-urls/ShortUrlsList.js
+++ /dev/null
@@ -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 (
-
- );
- };
- const renderShortUrls = () => {
- if (error) {
- return (
-
- Something went wrong while loading short URLs :( |
-
- );
- }
-
- if (loading) {
- return Loading... |
;
- }
-
- if (!loading && isEmpty(shortUrlsList)) {
- return No results found |
;
- }
-
- return shortUrlsList.map((shortUrl) => (
-
- ));
- };
-
- 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 (
-
-
-
-
-
-
-
-
- {renderOrderIcon('dateCreated')}
- Created at
- |
-
- {renderOrderIcon('shortCode')}
- Short URL
- |
-
- {renderOrderIcon('longUrl')}
- Long URL
- |
- Tags |
-
- {renderOrderIcon('visits')} Visits
- |
- |
-
-
-
- {renderShortUrls()}
-
-
-
- );
- };
-
- ShortUrlsListComp.propTypes = propTypes;
-
- return ShortUrlsListComp;
-};
-
-export default ShortUrlsList;
diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx
new file mode 100644
index 00000000..1273894d
--- /dev/null
+++ b/src/short-urls/ShortUrlsList.tsx
@@ -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 {
+ selectedServer: SelectedServer;
+ listShortUrls: (params: ShortUrlsListParams) => void;
+ shortUrlsListParams: ShortUrlsListParams;
+ resetShortUrlParams: () => void;
+ createNewVisit: (message: any) => void;
+ loadMercureInfo: Function;
+ mercureInfo: MercureInfo;
+}
+
+const ShortUrlsList = (ShortUrlsRow: FC) => ({
+ 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 (
+
+ );
+ };
+ const renderShortUrls = () => {
+ if (error) {
+ return (
+
+ Something went wrong while loading short URLs :( |
+
+ );
+ }
+
+ if (loading) {
+ return Loading... |
;
+ }
+
+ if (!loading && isEmpty(shortUrlsList)) {
+ return No results found |
;
+ }
+
+ return shortUrlsList.map((shortUrl) => (
+
+ ));
+ };
+
+ 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 (
+
+
+
+
+
+
+
+
+ {renderOrderIcon('dateCreated')}
+ Created at
+ |
+
+ {renderOrderIcon('shortCode')}
+ Short URL
+ |
+
+ {renderOrderIcon('longUrl')}
+ Long URL
+ |
+ Tags |
+
+ {renderOrderIcon('visits')} Visits
+ |
+ |
+
+
+
+ {renderShortUrls()}
+
+
+
+ );
+};
+
+export default ShortUrlsList;
diff --git a/src/short-urls/UseExistingIfFoundInfoIcon.js b/src/short-urls/UseExistingIfFoundInfoIcon.tsx
similarity index 93%
rename from src/short-urls/UseExistingIfFoundInfoIcon.js
rename to src/short-urls/UseExistingIfFoundInfoIcon.tsx
index 803d4b23..e1c13fa1 100644
--- a/src/short-urls/UseExistingIfFoundInfoIcon.js
+++ b/src/short-urls/UseExistingIfFoundInfoIcon.tsx
@@ -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 }) => (
Info
@@ -45,7 +45,7 @@ const UseExistingIfFoundInfoIcon = () => {
- {renderInfoModal(isModalOpen, toggleModal)}
+
);
};
diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts
index f0045e83..ba7d7c6e 100644
--- a/src/short-urls/data/index.ts
+++ b/src/short-urls/data/index.ts
@@ -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;
}
diff --git a/src/short-urls/helpers/CreateShortUrlResult.tsx b/src/short-urls/helpers/CreateShortUrlResult.tsx
index 83701cf8..8a561d28 100644
--- a/src/short-urls/helpers/CreateShortUrlResult.tsx
+++ b/src/short-urls/helpers/CreateShortUrlResult.tsx
@@ -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) => (
diff --git a/src/short-urls/reducers/shortUrlCreation.ts b/src/short-urls/reducers/shortUrlCreation.ts
index f062b694..3f066655 100644
--- a/src/short-urls/reducers/shortUrlCreation.ts
+++ b/src/short-urls/reducers/shortUrlCreation.ts
@@ -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;
diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts
index de4a2370..71a153e5 100644
--- a/src/short-urls/reducers/shortUrlsList.ts
+++ b/src/short-urls/reducers/shortUrlsList.ts
@@ -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 {
- 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 = (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 = (prop:
export default buildReducer({
[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,
diff --git a/src/short-urls/reducers/shortUrlsListParams.ts b/src/short-urls/reducers/shortUrlsListParams.ts
index e5ed91d5..db9ba232 100644
--- a/src/short-urls/reducers/shortUrlsListParams.ts
+++ b/src/short-urls/reducers/shortUrlsListParams.ts
@@ -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;
+ orderBy?: Record;
}
const initialState: ShortUrlsListParams = { page: '1' };
diff --git a/src/short-urls/services/provideServices.ts b/src/short-urls/services/provideServices.ts
index a5f43f4c..8e86e7c9 100644
--- a/src/short-urls/services/provideServices.ts
+++ b/src/short-urls/services/provideServices.ts
@@ -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
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 45592f26..e3a061e4 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -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';
}
diff --git a/test/short-urls/CreateShortUrl.test.js b/test/short-urls/CreateShortUrl.test.tsx
similarity index 72%
rename from test/short-urls/CreateShortUrl.test.js
rename to test/short-urls/CreateShortUrl.test.tsx
index e9ca10cb..18081131 100644
--- a/test/short-urls/CreateShortUrl.test.js
+++ b/test/short-urls/CreateShortUrl.test.tsx
@@ -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('', () => {
- let wrapper;
- const TagsSelector = () => '';
- const shortUrlCreationResult = {
- loading: false,
- };
- const createShortUrl = jest.fn(() => Promise.resolve());
+ let wrapper: ShallowWrapper;
+ const TagsSelector = () => null;
+ const shortUrlCreationResult = Mock.all();
+ const createShortUrl = jest.fn(async () => Promise.resolve());
beforeEach(() => {
- const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => '', () => '');
+ const CreateShortUrl = createShortUrlsCreator(TagsSelector, () => null, () => null);
wrapper = shallow(
- ,
+ {}}
+ />,
);
});
- 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');
diff --git a/test/short-urls/Paginator.test.js b/test/short-urls/Paginator.test.tsx
similarity index 85%
rename from test/short-urls/Paginator.test.js
rename to test/short-urls/Paginator.test.tsx
index f292dea9..7e63d972 100644
--- a/test/short-urls/Paginator.test.js
+++ b/test/short-urls/Paginator.test.tsx
@@ -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('', () => {
- 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();
diff --git a/test/short-urls/SearchBar.test.js b/test/short-urls/SearchBar.test.tsx
similarity index 75%
rename from test/short-urls/SearchBar.test.js
rename to test/short-urls/SearchBar.test.tsx
index efbdddc1..97f155f6 100644
--- a/test/short-urls/SearchBar.test.js
+++ b/test/short-urls/SearchBar.test.tsx
@@ -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('', () => {
- let wrapper;
+ let wrapper: ShallowWrapper;
const listShortUrlsMock = jest.fn();
- const SearchBar = searchBarCreator({}, () => '');
+ const SearchBar = searchBarCreator(Mock.all(), () => null);
- afterEach(() => {
- listShortUrlsMock.mockReset();
- wrapper && wrapper.unmount();
- });
+ afterEach(jest.clearAllMocks);
+ afterEach(() => wrapper?.unmount());
it('renders a SearchField', () => {
- wrapper = shallow();
+ wrapper = shallow();
expect(wrapper.find(SearchField)).toHaveLength(1);
});
it('renders a DateRangeRow', () => {
- wrapper = shallow();
+ wrapper = shallow();
expect(wrapper.find(DateRangeRow)).toHaveLength(1);
});
it('renders no tags when the list of tags is empty', () => {
- wrapper = shallow();
+ wrapper = shallow();
expect(wrapper.find(Tag)).toHaveLength(0);
});
@@ -36,7 +36,7 @@ describe('', () => {
it('renders the proper amount of tags', () => {
const tags = [ 'foo', 'bar', 'baz' ];
- wrapper = shallow();
+ wrapper = shallow();
expect(wrapper.find(Tag)).toHaveLength(tags.length);
});
diff --git a/test/short-urls/ShortUrls.test.js b/test/short-urls/ShortUrls.test.tsx
similarity index 61%
rename from test/short-urls/ShortUrls.test.js
rename to test/short-urls/ShortUrls.test.tsx
index d2327a31..45d845b8 100644
--- a/test/short-urls/ShortUrls.test.js
+++ b/test/short-urls/ShortUrls.test.tsx
@@ -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('', () => {
- 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();
+ wrapper = shallow(
+ ()} />,
+ );
});
afterEach(() => wrapper.unmount());
diff --git a/test/short-urls/ShortUrlsList.test.js b/test/short-urls/ShortUrlsList.test.tsx
similarity index 80%
rename from test/short-urls/ShortUrlsList.test.js
rename to test/short-urls/ShortUrlsList.test.tsx
index 65246833..60161780 100644
--- a/test/short-urls/ShortUrlsList.test.js
+++ b/test/short-urls/ShortUrlsList.test.tsx
@@ -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('', () => {
- let wrapper;
- const ShortUrlsRow = () => '';
+ let wrapper: ShallowWrapper;
+ const ShortUrlsRow = () => null;
const listShortUrlsMock = jest.fn();
const resetShortUrlParamsMock = jest.fn();
@@ -15,6 +17,7 @@ describe('', () => {
beforeEach(() => {
wrapper = shallow(
()}
listShortUrls={listShortUrlsMock}
resetShortUrlParams={resetShortUrlParamsMock}
shortUrlsListParams={{
@@ -22,29 +25,27 @@ describe('', () => {
tags: [ 'test tag' ],
searchTerm: 'example.com',
}}
- match={{ params: {} }}
- location={{}}
+ match={{ params: {} } as any}
+ location={{} as any}
loading={false}
error={false}
shortUrlsList={
[
- {
+ Mock.of({
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('', () => {
});
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);
diff --git a/test/short-urls/UseExistingIfFoundInfoIcon.test.js b/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx
similarity index 89%
rename from test/short-urls/UseExistingIfFoundInfoIcon.test.js
rename to test/short-urls/UseExistingIfFoundInfoIcon.test.tsx
index 12f25a0d..4cac6e05 100644
--- a/test/short-urls/UseExistingIfFoundInfoIcon.test.js
+++ b/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx
@@ -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('', () => {
- let wrapped;
+ let wrapped: ReactWrapper;
beforeEach(() => {
wrapped = mount();
diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts
index 00aa1740..ea77b662 100644
--- a/test/short-urls/reducers/shortUrlsList.test.ts
+++ b/test/short-urls/reducers/shortUrlsList.test.ts
@@ -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({
data: [
Mock.of({ shortCode, tags: [] }),
Mock.of({ shortCode, tags: [], domain: 'example.com' }),
Mock.of({ shortCode: 'foo', tags: [] }),
],
- },
+ }),
loading: false,
error: false,
};
@@ -75,13 +70,13 @@ describe('shortUrlsListReducer', () => {
validSince: '2020-05-05',
};
const state = {
- shortUrls: {
+ shortUrls: Mock.of({
data: [
Mock.of({ shortCode, meta: { maxVisits: 10 }, domain }),
Mock.of({ shortCode, meta: { maxVisits: 50 } }),
Mock.of({ 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({
data: [
Mock.of({ shortCode }),
Mock.of({ shortCode, domain: 'example.com' }),
Mock.of({ shortCode: 'foo' }),
],
- },
+ }),
loading: false,
error: false,
};
@@ -129,13 +124,13 @@ describe('shortUrlsListReducer', () => {
visitsCount: 11,
};
const state = {
- shortUrls: {
+ shortUrls: Mock.of({
data: [
Mock.of({ shortCode, domain: 'example.com', visitsCount: 5 }),
Mock.of({ shortCode, visitsCount: 10 }),
Mock.of({ shortCode: 'foo', visitsCount: 8 }),
],
- },
+ }),
loading: false,
error: false,
};