diff --git a/package-lock.json b/package-lock.json index f7749882..5e1b06d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18293,9 +18293,9 @@ "dev": true }, "react-external-link": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/react-external-link/-/react-external-link-1.0.0.tgz", - "integrity": "sha512-KkEozBNo4OI+zdNgGX6ua5+w68wEu2RLdnMGF7KIod6+heDMLfK52Xeqtb0GBO/JvC+HTcj5Kdz8ol0oORYIPA==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-external-link/-/react-external-link-1.1.1.tgz", + "integrity": "sha512-e2WnTWkg81cuqxmDfjOalliAE20+Y/uD+lserN4uuwkwu+ciGLB3BMz4m7GnXh2+TowIi4sLtCL7zr7aDnIaqA==" }, "react-is": { "version": "16.7.0", diff --git a/package.json b/package.json index 95cd3fad..7d8e84be 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "react-copy-to-clipboard": "^5.0.1", "react-datepicker": "~1.5.0", "react-dom": "^16.13.1", - "react-external-link": "^1.0.0", + "react-external-link": "^1.1.1", "react-leaflet": "^2.4.0", "react-moment": "^0.9.5", "react-redux": "^7.1.1", diff --git a/src/common/ShlinkVersions.js b/src/common/ShlinkVersions.tsx similarity index 50% rename from src/common/ShlinkVersions.js rename to src/common/ShlinkVersions.tsx index ae083806..547b447e 100644 --- a/src/common/ShlinkVersions.js +++ b/src/common/ShlinkVersions.tsx @@ -1,45 +1,43 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { pipe } from 'ramda'; import { ExternalLink } from 'react-external-link'; -import { serverType } from '../servers/prop-types'; import { versionToPrintable, versionToSemVer } from '../utils/helpers/version'; +import { isReachableServer, SelectedServer } from '../servers/data'; const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%'; const normalizeVersion = pipe(versionToSemVer(), versionToPrintable); -const propTypes = { - selectedServer: serverType, - className: PropTypes.string, - clientVersion: PropTypes.string, -}; +export interface ShlinkVersionsProps { + selectedServer: SelectedServer; + clientVersion?: string; + className?: string; +} -const versionLinkPropTypes = { - project: PropTypes.oneOf([ 'shlink', 'shlink-web-client' ]).isRequired, - version: PropTypes.string.isRequired, -}; +interface VersionLinkProps { + project: 'shlink' | 'shlink-web-client'; + version: string; +} -const VersionLink = ({ project, version }) => ( +const VersionLink = ({ project, version }: VersionLinkProps) => ( {version} ); -VersionLink.propTypes = versionLinkPropTypes; - -const ShlinkVersions = ({ selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => { - const { printableVersion: serverVersion } = selectedServer; +const ShlinkVersions = ( + { selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps, +) => { const normalizedClientVersion = normalizeVersion(clientVersion); return ( - Client: - - Server: + {isReachableServer(selectedServer) && + Server: - + } + Client: ); }; -ShlinkVersions.propTypes = propTypes; - export default ShlinkVersions; diff --git a/src/common/SimplePaginator.js b/src/common/SimplePaginator.tsx similarity index 64% rename from src/common/SimplePaginator.js rename to src/common/SimplePaginator.tsx index 0859e4ff..2b648962 100644 --- a/src/common/SimplePaginator.js +++ b/src/common/SimplePaginator.tsx @@ -1,23 +1,22 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FC } from 'react'; import classNames from 'classnames'; import { Pagination, PaginationItem, PaginationLink } from 'reactstrap'; -import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination'; +import { pageIsEllipsis, keyForPage, NumberOrEllipsis, progressivePagination } from '../utils/helpers/pagination'; import './SimplePaginator.scss'; -const propTypes = { - pagesCount: PropTypes.number.isRequired, - currentPage: PropTypes.number.isRequired, - setCurrentPage: PropTypes.func.isRequired, - centered: PropTypes.bool, -}; +interface SimplePaginatorProps { + pagesCount: number; + currentPage: number; + setCurrentPage: (currentPage: number) => void; + centered?: boolean; +} -const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { +const SimplePaginator: FC = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { if (pagesCount < 2) { return null; } - const onClick = (page) => () => setCurrentPage(page); + const onClick = (page: NumberOrEllipsis) => () => !pageIsEllipsis(page) && setCurrentPage(page); return ( @@ -27,7 +26,7 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = t {progressivePagination(currentPage, pagesCount).map((pageNumber, index) => ( {pageNumber} @@ -40,6 +39,4 @@ const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = t ); }; -SimplePaginator.propTypes = propTypes; - export default SimplePaginator; diff --git a/src/servers/data/index.ts b/src/servers/data/index.ts index 57dbf52a..3c918cbb 100644 --- a/src/servers/data/index.ts +++ b/src/servers/data/index.ts @@ -27,3 +27,6 @@ export type SelectedServer = RegularServer | NotFoundServer | null; export const hasServerData = (server: ServerData | NotFoundServer | null): server is ServerData => !!(server as ServerData)?.url && !!(server as ServerData)?.apiKey; + +export const isReachableServer = (server: SelectedServer): server is ReachableServer => + !!server?.hasOwnProperty('printableVersion'); diff --git a/src/short-urls/Paginator.js b/src/short-urls/Paginator.js index dd976954..4b051811 100644 --- a/src/short-urls/Paginator.js +++ b/src/short-urls/Paginator.js @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Pagination, PaginationItem, PaginationLink } from 'reactstrap'; import PropTypes from 'prop-types'; -import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination'; +import { pageIsEllipsis, keyForPage, progressivePagination } from '../utils/helpers/pagination'; import './Paginator.scss'; const propTypes = { @@ -24,7 +24,7 @@ const Paginator = ({ paginator = {}, serverId }) => { progressivePagination(currentPage, pagesCount).map((pageNumber, index) => ( { - const delta = 2; const pages: NumberOrEllipsis[] = range( - max(delta, currentPage - delta), - min(pageCount - 1, currentPage + delta) + 1, + max(DELTA, currentPage - DELTA), + min(pageCount - 1, currentPage + DELTA) + 1, ); - if (currentPage - delta > delta) { + if (currentPage - DELTA > DELTA) { pages.unshift(ELLIPSIS); } - if (currentPage + delta < pageCount - 1) { + if (currentPage + DELTA < pageCount - 1) { pages.push(ELLIPSIS); } @@ -24,6 +27,6 @@ export const progressivePagination = (currentPage: number, pageCount: number): N return pages; }; -export const keyForPage = (pageNumber: NumberOrEllipsis, index: number) => pageNumber !== ELLIPSIS ? pageNumber : `${pageNumber}_${index}`; +export const pageIsEllipsis = (pageNumber: NumberOrEllipsis): pageNumber is Ellipsis => pageNumber === ELLIPSIS; -export const isPageDisabled = (pageNumber: NumberOrEllipsis) => pageNumber === ELLIPSIS; +export const keyForPage = (pageNumber: NumberOrEllipsis, index: number) => !pageIsEllipsis(pageNumber) ? `${pageNumber}` : `${pageNumber}_${index}`; diff --git a/test/common/ShlinkVersions.test.js b/test/common/ShlinkVersions.test.js deleted file mode 100644 index 020f50dd..00000000 --- a/test/common/ShlinkVersions.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import ShlinkVersions from '../../src/common/ShlinkVersions'; - -describe('', () => { - let wrapper; - const createWrapper = (props) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper && wrapper.unmount()); - - it.each([ - [ '1.2.3', 'foo', 'v1.2.3', 'foo' ], - [ 'foo', '1.2.3', 'latest', '1.2.3' ], - [ 'latest', 'latest', 'latest', 'latest' ], - [ '5.5.0', '0.2.8', 'v5.5.0', '0.2.8' ], - [ 'not-semver', 'something', 'latest', 'something' ], - ])('displays expected versions', (clientVersion, printableVersion, expectedClientVersion, expectedServerVersion) => { - const wrapper = createWrapper({ clientVersion, selectedServer: { printableVersion } }); - const links = wrapper.find('VersionLink'); - const clientLink = links.at(0); - const serverLink = links.at(1); - - expect(clientLink.prop('project')).toEqual('shlink-web-client'); - expect(clientLink.prop('version')).toEqual(expectedClientVersion); - expect(serverLink.prop('project')).toEqual('shlink'); - expect(serverLink.prop('version')).toEqual(expectedServerVersion); - }); -}); diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx new file mode 100644 index 00000000..75d0600a --- /dev/null +++ b/test/common/ShlinkVersions.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import ShlinkVersions, { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; +import { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; + +describe('', () => { + let wrapper: ShallowWrapper; + const createWrapper = (props: ShlinkVersionsProps) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + + it.each([ + [ '1.2.3', Mock.of({ printableVersion: 'foo' }), 'v1.2.3', 'foo' ], + [ 'foo', Mock.of({ printableVersion: '1.2.3' }), 'latest', '1.2.3' ], + [ 'latest', Mock.of({ printableVersion: 'latest' }), 'latest', 'latest' ], + [ '5.5.0', Mock.of({ printableVersion: '0.2.8' }), 'v5.5.0', '0.2.8' ], + [ 'not-semver', Mock.of({ printableVersion: 'something' }), 'latest', 'something' ], + ])( + 'displays expected versions when selected server is reachable', + (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { + const wrapper = createWrapper({ clientVersion, selectedServer }); + const links = wrapper.find('VersionLink'); + const serverLink = links.at(0); + const clientLink = links.at(1); + + expect(serverLink.prop('project')).toEqual('shlink'); + expect(serverLink.prop('version')).toEqual(expectedServerVersion); + expect(clientLink.prop('project')).toEqual('shlink-web-client'); + expect(clientLink.prop('version')).toEqual(expectedClientVersion); + }, + ); + + it.each([ + [ '1.2.3', null ], + [ '1.2.3', Mock.of({ serverNotFound: true }) ], + [ '1.2.3', Mock.of({ serverNotReachable: true }) ], + ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { + const wrapper = createWrapper({ clientVersion, selectedServer }); + const links = wrapper.find('VersionLink'); + + expect(links).toHaveLength(1); + expect(links.at(0).prop('project')).toEqual('shlink-web-client'); + }); +}); diff --git a/test/common/SimplePaginator.test.js b/test/common/SimplePaginator.test.tsx similarity index 83% rename from test/common/SimplePaginator.test.js rename to test/common/SimplePaginator.test.tsx index ac91dd01..a8408f56 100644 --- a/test/common/SimplePaginator.test.js +++ b/test/common/SimplePaginator.test.tsx @@ -1,29 +1,30 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { identity } from 'ramda'; import { PaginationItem } from 'reactstrap'; import SimplePaginator from '../../src/common/SimplePaginator'; import { ELLIPSIS } from '../../src/utils/helpers/pagination'; describe('', () => { - let wrapper; - const createWrapper = (pagesCount, currentPage = 1) => { + let wrapper: ShallowWrapper; + const createWrapper = (pagesCount: number, currentPage = 1) => { + // @ts-expect-error wrapper = shallow(); return wrapper; }; - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it.each([ -3, -2, 0, 1 ])('renders empty when the amount of pages is smaller than 2', (pagesCount) => { expect(createWrapper(pagesCount).text()).toEqual(''); }); describe('ELLIPSIS are rendered where expected', () => { - const getItemsForPages = (pagesCount, currentPage) => { + const getItemsForPages = (pagesCount: number, currentPage: number) => { const paginator = createWrapper(pagesCount, currentPage); const items = paginator.find(PaginationItem); - const itemsWithEllipsis = items.filterWhere((item) => item.key() && item.key().includes(ELLIPSIS)); + const itemsWithEllipsis = items.filterWhere((item) => item?.key()?.includes(ELLIPSIS)); return { items, itemsWithEllipsis }; };