diff --git a/package-lock.json b/package-lock.json index 26892ac8..ee22ba94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3384,6 +3384,15 @@ "csstype": "^3.0.2" } }, + "@types/react-copy-to-clipboard": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.3.0.tgz", + "integrity": "sha512-iideNPRyroENqsOFh1i2Dv3zkviYS9r/9qD9Uh3Z9NNoAAqqa2x53i7iGndGNnJFIo20wIu7Hgh77tx1io8bgw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-datepicker": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-1.8.0.tgz", diff --git a/package.json b/package.json index b4ec1ce3..4139b384 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@types/qs": "^6.9.4", "@types/ramda": "^0.27.14", "@types/react": "^16.9.46", + "@types/react-copy-to-clipboard": "^4.3.0", "@types/react-datepicker": "~1.8.0", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", diff --git a/src/servers/data/index.ts b/src/servers/data/index.ts index 0a89ae2c..22294143 100644 --- a/src/servers/data/index.ts +++ b/src/servers/data/index.ts @@ -27,14 +27,14 @@ export type SelectedServer = RegularServer | NotFoundServer | null; export type ServersMap = Record; -export const hasServerData = (server: ServerData | NotFoundServer | null): server is ServerData => +export const hasServerData = (server: SelectedServer | ServerData): server is ServerData => !!(server as ServerData)?.url && !!(server as ServerData)?.apiKey; -export const isReachableServer = (server: SelectedServer): server is ReachableServer => - !!server?.hasOwnProperty('printableVersion'); - export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId => !!server?.hasOwnProperty('id'); +export const isReachableServer = (server: SelectedServer): server is ReachableServer => + !!server?.hasOwnProperty('printableVersion'); + export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer => !!server?.hasOwnProperty('serverNotFound'); diff --git a/src/servers/prop-types/index.js b/src/servers/prop-types/index.js index 221d946a..f86d1744 100644 --- a/src/servers/prop-types/index.js +++ b/src/servers/prop-types/index.js @@ -14,6 +14,7 @@ const notFoundServerType = PropTypes.shape({ serverNotFound: PropTypes.bool.isRequired, }); +/** @deprecated Use SelectedServer type instead */ export const serverType = PropTypes.oneOfType([ regularServerType, notFoundServerType, diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts index 4997ce44..f0045e83 100644 --- a/src/short-urls/data/index.ts +++ b/src/short-urls/data/index.ts @@ -16,6 +16,7 @@ export interface ShortUrl { shortCode: string; shortUrl: string; longUrl: string; + dateCreated: string; visitsCount: number; meta: Required>; tags: string[]; diff --git a/src/short-urls/helpers/CreateShortUrlResult.js b/src/short-urls/helpers/CreateShortUrlResult.js deleted file mode 100644 index f7cd3f5d..00000000 --- a/src/short-urls/helpers/CreateShortUrlResult.js +++ /dev/null @@ -1,67 +0,0 @@ -import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isNil } from 'ramda'; -import React, { useEffect } from 'react'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { Card, CardBody, Tooltip } from 'reactstrap'; -import PropTypes from 'prop-types'; -import { createShortUrlResultType } from '../reducers/shortUrlCreation'; -import './CreateShortUrlResult.scss'; - -const propTypes = { - resetCreateShortUrl: PropTypes.func, - error: PropTypes.bool, - result: createShortUrlResultType, -}; - -const CreateShortUrlResult = (useStateFlagTimeout) => { - const CreateShortUrlResultComp = ({ error, result, resetCreateShortUrl }) => { - const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout(); - - useEffect(() => { - resetCreateShortUrl(); - }, []); - - if (error) { - return ( - - An error occurred while creating the URL :( - - ); - } - - if (isNil(result)) { - return null; - } - - const { shortUrl } = result; - - return ( - - - Great! The short URL is {shortUrl} - - - - - - - Copied! - - - - ); - }; - - CreateShortUrlResultComp.propTypes = propTypes; - - return CreateShortUrlResultComp; -}; - -export default CreateShortUrlResult; diff --git a/src/short-urls/helpers/CreateShortUrlResult.tsx b/src/short-urls/helpers/CreateShortUrlResult.tsx new file mode 100644 index 00000000..83701cf8 --- /dev/null +++ b/src/short-urls/helpers/CreateShortUrlResult.tsx @@ -0,0 +1,61 @@ +import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { isNil } from 'ramda'; +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'; + +interface CreateShortUrlResultProps extends ShortUrlCreation { + resetCreateShortUrl: Function; +} + +const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => ( + { error, result, resetCreateShortUrl }: CreateShortUrlResultProps, +) => { + const [ showCopyTooltip, setShowCopyTooltip ] = useStateFlagTimeout(); + + useEffect(() => { + resetCreateShortUrl(); + }, []); + + if (error) { + return ( + + An error occurred while creating the URL :( + + ); + } + + if (isNil(result)) { + return null; + } + + const { shortUrl } = result; + + return ( + + + Great! The short URL is {shortUrl} + + + + + + + Copied! + + + + ); +}; + +export default CreateShortUrlResult; diff --git a/src/short-urls/helpers/EditTagsModal.js b/src/short-urls/helpers/EditTagsModal.js deleted file mode 100644 index ed4c5055..00000000 --- a/src/short-urls/helpers/EditTagsModal.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; -import PropTypes from 'prop-types'; -import { ExternalLink } from 'react-external-link'; -import { shortUrlTagsType } from '../reducers/shortUrlTags'; -import { shortUrlType } from '../reducers/shortUrlsList'; - -const propTypes = { - isOpen: PropTypes.bool.isRequired, - toggle: PropTypes.func.isRequired, - shortUrl: shortUrlType.isRequired, - shortUrlTags: shortUrlTagsType, - editShortUrlTags: PropTypes.func, - resetShortUrlsTags: PropTypes.func, -}; - -const EditTagsModal = (TagsSelector) => { - const EditTagsModalComp = ({ isOpen, toggle, shortUrl, shortUrlTags, editShortUrlTags, resetShortUrlsTags }) => { - const [ selectedTags, setSelectedTags ] = useState(shortUrl.tags || []); - - useEffect(() => resetShortUrlsTags, []); - - const url = shortUrl && (shortUrl.shortUrl || ''); - const saveTags = () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags) - .then(toggle) - .catch(() => {}); - - return ( - - - Edit tags for - - - setSelectedTags(tags)} /> - {shortUrlTags.error && ( -
- Something went wrong while saving the tags :( -
- )} -
- - - - -
- ); - }; - - EditTagsModalComp.propTypes = propTypes; - - return EditTagsModalComp; -}; - -export default EditTagsModal; diff --git a/src/short-urls/helpers/EditTagsModal.tsx b/src/short-urls/helpers/EditTagsModal.tsx new file mode 100644 index 00000000..be7cf4ee --- /dev/null +++ b/src/short-urls/helpers/EditTagsModal.tsx @@ -0,0 +1,49 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { ExternalLink } from 'react-external-link'; +import { ShortUrlTags } from '../reducers/shortUrlTags'; +import { ShortUrlModalProps } from '../data'; +import { OptionalString } from '../../utils/utils'; + +interface EditTagsModalProps extends ShortUrlModalProps { + shortUrlTags: ShortUrlTags; + editShortUrlTags: (shortCode: string, domain: OptionalString, tags: string[]) => Promise; + resetShortUrlsTags: () => void; +} + +const EditTagsModal = (TagsSelector: FC) => ( // TODO Use TagsSelector type when available + { isOpen, toggle, shortUrl, shortUrlTags, editShortUrlTags, resetShortUrlsTags }: EditTagsModalProps, +) => { + const [ selectedTags, setSelectedTags ] = useState(shortUrl.tags || []); + + useEffect(() => resetShortUrlsTags, []); + + const url = shortUrl?.shortUrl ?? ''; + const saveTags = async () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags) + .then(toggle) + .catch(() => {}); + + return ( + + + Edit tags for + + + + {shortUrlTags.error && ( +
+ Something went wrong while saving the tags :( +
+ )} +
+ + + + +
+ ); +}; + +export default EditTagsModal; diff --git a/src/short-urls/helpers/PreviewModal.js b/src/short-urls/helpers/PreviewModal.tsx similarity index 55% rename from src/short-urls/helpers/PreviewModal.js rename to src/short-urls/helpers/PreviewModal.tsx index 9570ec75..b4c85878 100644 --- a/src/short-urls/helpers/PreviewModal.js +++ b/src/short-urls/helpers/PreviewModal.tsx @@ -1,29 +1,21 @@ import React from 'react'; import { Modal, ModalBody, ModalHeader } from 'reactstrap'; -import PropTypes from 'prop-types'; import { ExternalLink } from 'react-external-link'; +import { ShortUrlModalProps } from '../data'; import './PreviewModal.scss'; -const propTypes = { - url: PropTypes.string, - toggle: PropTypes.func, - isOpen: PropTypes.bool, -}; - -const PreviewModal = ({ url, toggle, isOpen }) => ( +const PreviewModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => ( - Preview for {url} + Preview for {shortUrl}

Loading...

- Preview + Preview
); -PreviewModal.propTypes = propTypes; - export default PreviewModal; diff --git a/src/short-urls/helpers/QrCodeModal.js b/src/short-urls/helpers/QrCodeModal.tsx similarity index 51% rename from src/short-urls/helpers/QrCodeModal.js rename to src/short-urls/helpers/QrCodeModal.tsx index dcc8b323..f050f662 100644 --- a/src/short-urls/helpers/QrCodeModal.js +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -1,28 +1,20 @@ import React from 'react'; import { Modal, ModalBody, ModalHeader } from 'reactstrap'; -import PropTypes from 'prop-types'; import { ExternalLink } from 'react-external-link'; +import { ShortUrlModalProps } from '../data'; import './QrCodeModal.scss'; -const propTypes = { - url: PropTypes.string, - toggle: PropTypes.func, - isOpen: PropTypes.bool, -}; - -const QrCodeModal = ({ url, toggle, isOpen }) => ( +const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => ( - QR code for {url} + QR code for {shortUrl}
- QR code + QR code
); -QrCodeModal.propTypes = propTypes; - export default QrCodeModal; diff --git a/src/short-urls/helpers/ShortUrlVisitsCount.js b/src/short-urls/helpers/ShortUrlVisitsCount.tsx similarity index 70% rename from src/short-urls/helpers/ShortUrlVisitsCount.js rename to src/short-urls/helpers/ShortUrlVisitsCount.tsx index e33ae1c0..b87af090 100644 --- a/src/short-urls/helpers/ShortUrlVisitsCount.js +++ b/src/short-urls/helpers/ShortUrlVisitsCount.tsx @@ -1,24 +1,19 @@ import React, { useRef } from 'react'; -import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { UncontrolledTooltip } from 'reactstrap'; import classNames from 'classnames'; -import { serverType } from '../../servers/prop-types'; import { prettify } from '../../utils/helpers/numbers'; -import { shortUrlType } from '../reducers/shortUrlsList'; -import VisitStatsLink from './VisitStatsLink'; +import VisitStatsLink, { VisitStatsLinkProps } from './VisitStatsLink'; import './ShortUrlVisitsCount.scss'; -const propTypes = { - visitsCount: PropTypes.number.isRequired, - shortUrl: shortUrlType, - selectedServer: serverType, - active: PropTypes.bool, -}; +export interface ShortUrlVisitsCount extends VisitStatsLinkProps { + visitsCount: number; + active?: boolean; +} -const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }) => { - const maxVisits = shortUrl && shortUrl.meta && shortUrl.meta.maxVisits; +const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCount) => { + const maxVisits = shortUrl?.meta?.maxVisits; const visitsLink = ( (); return ( @@ -52,13 +47,11 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f - tooltipRef.current} placement="bottom"> + tooltipRef.current) as any} placement="bottom"> This short URL will not accept more than {prettifiedMaxVisits} visits. ); }; -ShortUrlVisitsCount.propTypes = propTypes; - export default ShortUrlVisitsCount; diff --git a/src/short-urls/helpers/ShortUrlsRow.js b/src/short-urls/helpers/ShortUrlsRow.js deleted file mode 100644 index f2762a9f..00000000 --- a/src/short-urls/helpers/ShortUrlsRow.js +++ /dev/null @@ -1,98 +0,0 @@ -import { isEmpty } from 'ramda'; -import React, { useEffect, useRef } from 'react'; -import Moment from 'react-moment'; -import PropTypes from 'prop-types'; -import { ExternalLink } from 'react-external-link'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams'; -import { serverType } from '../../servers/prop-types'; -import { shortUrlType } from '../reducers/shortUrlsList'; -import Tag from '../../tags/helpers/Tag'; -import ShortUrlVisitsCount from './ShortUrlVisitsCount'; -import './ShortUrlsRow.scss'; - -const propTypes = { - refreshList: PropTypes.func, - shortUrlsListParams: shortUrlsListParamsType, - selectedServer: serverType, - shortUrl: shortUrlType, -}; - -const ShortUrlsRow = ( - ShortUrlsRowMenu, - colorGenerator, - useStateFlagTimeout, -) => { - const ShortUrlsRowComp = ({ shortUrl, selectedServer, refreshList, shortUrlsListParams }) => { - const [ copiedToClipboard, setCopiedToClipboard ] = useStateFlagTimeout(); - const [ active, setActive ] = useStateFlagTimeout(false, 500); - const isFirstRun = useRef(true); - - const renderTags = (tags) => { - if (isEmpty(tags)) { - return No tags; - } - - const selectedTags = shortUrlsListParams.tags || []; - - return tags.map((tag) => ( - refreshList({ tags: [ ...selectedTags, tag ] })} - /> - )); - }; - - useEffect(() => { - if (isFirstRun.current) { - isFirstRun.current = false; - } else { - setActive(true); - } - }, [ shortUrl.visitsCount ]); - - return ( - - - {shortUrl.dateCreated} - - - - - - - - - - - - - - {renderTags(shortUrl.tags)} - - - - - - - - ); - }; - - ShortUrlsRowComp.propTypes = propTypes; - - return ShortUrlsRowComp; -}; - -export default ShortUrlsRow; diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx new file mode 100644 index 00000000..be524892 --- /dev/null +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -0,0 +1,94 @@ +import { isEmpty } from 'ramda'; +import React, { FC, useEffect, useRef } from 'react'; +import Moment from 'react-moment'; +import { ExternalLink } from 'react-external-link'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { ShortUrlsListParams } from '../reducers/shortUrlsListParams'; +import ColorGenerator from '../../utils/services/ColorGenerator'; +import { StateFlagTimeout } from '../../utils/helpers/hooks'; +import Tag from '../../tags/helpers/Tag'; +import { SelectedServer } from '../../servers/data'; +import { ShortUrl } from '../data'; +import ShortUrlVisitsCount from './ShortUrlVisitsCount'; +import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu'; +import './ShortUrlsRow.scss'; + +export interface ShortUrlsRowProps { + refreshList: Function; + shortUrlsListParams: ShortUrlsListParams; + selectedServer: SelectedServer; + shortUrl: ShortUrl; +} + +const ShortUrlsRow = ( + ShortUrlsRowMenu: FC, + colorGenerator: ColorGenerator, + useStateFlagTimeout: StateFlagTimeout, +) => ({ shortUrl, selectedServer, refreshList, shortUrlsListParams }: ShortUrlsRowProps) => { + const [ copiedToClipboard, setCopiedToClipboard ] = useStateFlagTimeout(); + const [ active, setActive ] = useStateFlagTimeout(false, 500); + const isFirstRun = useRef(true); + + const renderTags = (tags: string[]) => { + if (isEmpty(tags)) { + return No tags; + } + + const selectedTags = shortUrlsListParams.tags ?? []; + + return tags.map((tag) => ( + refreshList({ tags: [ ...selectedTags, tag ] })} + /> + )); + }; + + useEffect(() => { + if (isFirstRun.current) { + isFirstRun.current = false; + } else { + setActive(); + } + }, [ shortUrl.visitsCount ]); + + return ( + + + {shortUrl.dateCreated} + + + + + + + + + + + + + + {renderTags(shortUrl.tags)} + + + + + + + + ); +}; + +export default ShortUrlsRow; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.js b/src/short-urls/helpers/ShortUrlsRowMenu.js deleted file mode 100644 index ae6dc1e7..00000000 --- a/src/short-urls/helpers/ShortUrlsRowMenu.js +++ /dev/null @@ -1,95 +0,0 @@ -import { faImage as pictureIcon } from '@fortawesome/free-regular-svg-icons'; -import { - faTags as tagsIcon, - faChartPie as pieChartIcon, - faEllipsisV as menuIcon, - faQrcode as qrIcon, - faMinusCircle as deleteIcon, - faEdit as editIcon, - faLink as linkIcon, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React from 'react'; -import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { serverType } from '../../servers/prop-types'; -import { shortUrlType } from '../reducers/shortUrlsList'; -import { useToggle } from '../../utils/helpers/hooks'; -import PreviewModal from './PreviewModal'; -import QrCodeModal from './QrCodeModal'; -import VisitStatsLink from './VisitStatsLink'; -import './ShortUrlsRowMenu.scss'; - -const propTypes = { - selectedServer: serverType, - shortUrl: shortUrlType, -}; - -const ShortUrlsRowMenu = (DeleteShortUrlModal, EditTagsModal, EditMetaModal, EditShortUrlModal, ForServerVersion) => { - const ShortUrlsRowMenuComp = ({ shortUrl, selectedServer }) => { - const [ isOpen, toggle ] = useToggle(); - const [ isQrModalOpen, toggleQrCode ] = useToggle(); - const [ isPreviewModalOpen, togglePreview ] = useToggle(); - const [ isTagsModalOpen, toggleTags ] = useToggle(); - const [ isMetaModalOpen, toggleMeta ] = useToggle(); - const [ isDeleteModalOpen, toggleDelete ] = useToggle(); - const [ isEditModalOpen, toggleEdit ] = useToggle(); - const completeShortUrl = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : ''; - - return ( - - -    - - - - Visit stats - - - - Edit tags - - - - - - Edit metadata - - - - - - - Edit long URL - - - - - - QR code - - - - - - Preview - - - - - - - - Delete short URL - - - - - ); - }; - - ShortUrlsRowMenuComp.propTypes = propTypes; - - return ShortUrlsRowMenuComp; -}; - -export default ShortUrlsRowMenu; diff --git a/src/short-urls/helpers/ShortUrlsRowMenu.tsx b/src/short-urls/helpers/ShortUrlsRowMenu.tsx new file mode 100644 index 00000000..141a5947 --- /dev/null +++ b/src/short-urls/helpers/ShortUrlsRowMenu.tsx @@ -0,0 +1,96 @@ +import { faImage as pictureIcon } from '@fortawesome/free-regular-svg-icons'; +import { + faTags as tagsIcon, + faChartPie as pieChartIcon, + faEllipsisV as menuIcon, + faQrcode as qrIcon, + faMinusCircle as deleteIcon, + faEdit as editIcon, + faLink as linkIcon, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { FC } from 'react'; +import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { useToggle } from '../../utils/helpers/hooks'; +import { ShortUrl, ShortUrlModalProps } from '../data'; +import { Versions } from '../../utils/helpers/version'; +import { SelectedServer } from '../../servers/data'; +import PreviewModal from './PreviewModal'; +import QrCodeModal from './QrCodeModal'; +import VisitStatsLink from './VisitStatsLink'; +import './ShortUrlsRowMenu.scss'; + +export interface ShortUrlsRowMenuProps { + selectedServer: SelectedServer; + shortUrl: ShortUrl; +} +type ShortUrlModal = FC; + +const ShortUrlsRowMenu = ( + DeleteShortUrlModal: ShortUrlModal, + EditTagsModal: ShortUrlModal, + EditMetaModal: ShortUrlModal, + EditShortUrlModal: ShortUrlModal, + ForServerVersion: FC, +) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => { + const [ isOpen, toggle ] = useToggle(); + const [ isQrModalOpen, toggleQrCode ] = useToggle(); + const [ isPreviewModalOpen, togglePreview ] = useToggle(); + const [ isTagsModalOpen, toggleTags ] = useToggle(); + const [ isMetaModalOpen, toggleMeta ] = useToggle(); + const [ isDeleteModalOpen, toggleDelete ] = useToggle(); + const [ isEditModalOpen, toggleEdit ] = useToggle(); + + return ( + + +    + + + + Visit stats + + + + Edit tags + + + + + + Edit metadata + + + + + + + Edit long URL + + + + + + QR code + + + + + + Preview + + + + + + + + Delete short URL + + + + + ); +}; + +export default ShortUrlsRowMenu; diff --git a/src/short-urls/helpers/VisitStatsLink.js b/src/short-urls/helpers/VisitStatsLink.js deleted file mode 100644 index f500e580..00000000 --- a/src/short-urls/helpers/VisitStatsLink.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; -import { serverType } from '../../servers/prop-types'; -import { shortUrlType } from '../reducers/shortUrlsList'; - -const propTypes = { - shortUrl: shortUrlType, - selectedServer: serverType, - children: PropTypes.node.isRequired, -}; - -const buildVisitsUrl = ({ id }, { shortCode, domain }) => { - const query = domain ? `?domain=${domain}` : ''; - - return `/server/${id}/short-code/${shortCode}/visits${query}`; -}; - -const VisitStatsLink = ({ selectedServer, shortUrl, children, ...rest }) => { - if (!selectedServer || !shortUrl) { - return {children}; - } - - return {children}; -}; - -VisitStatsLink.propTypes = propTypes; - -export default VisitStatsLink; diff --git a/src/short-urls/helpers/VisitStatsLink.tsx b/src/short-urls/helpers/VisitStatsLink.tsx new file mode 100644 index 00000000..7138533a --- /dev/null +++ b/src/short-urls/helpers/VisitStatsLink.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { isServerWithId, SelectedServer, ServerWithId } from '../../servers/data'; +import { ShortUrl } from '../data'; + +export interface VisitStatsLinkProps { + shortUrl?: ShortUrl | null; + selectedServer?: SelectedServer; +} + +const buildVisitsUrl = ({ id }: ServerWithId, { shortCode, domain }: ShortUrl) => { + const query = domain ? `?domain=${domain}` : ''; + + return `/server/${id}/short-code/${shortCode}/visits${query}`; +}; + +const VisitStatsLink: FC> = ( + { selectedServer, shortUrl, children, ...rest }, +) => { + if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) { + return {children}; + } + + return {children}; +}; + +export default VisitStatsLink; diff --git a/src/short-urls/reducers/shortUrlTags.ts b/src/short-urls/reducers/shortUrlTags.ts index cdaf9269..b81380f1 100644 --- a/src/short-urls/reducers/shortUrlTags.ts +++ b/src/short-urls/reducers/shortUrlTags.ts @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { Action, Dispatch } from 'redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; @@ -13,14 +12,6 @@ export const SHORT_URL_TAGS_EDITED = 'shlink/shortUrlTags/SHORT_URL_TAGS_EDITED' export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS'; /* eslint-enable padding-line-between-statements */ -/** @deprecated Use ShortUrlTags interface */ -export const shortUrlTagsType = PropTypes.shape({ - shortCode: PropTypes.string, - tags: PropTypes.arrayOf(PropTypes.string).isRequired, - saving: PropTypes.bool.isRequired, - error: PropTypes.bool.isRequired, -}); - export interface ShortUrlTags { shortCode: string | null; tags: string[]; diff --git a/src/short-urls/reducers/shortUrlsList.ts b/src/short-urls/reducers/shortUrlsList.ts index 6294dd17..de4a2370 100644 --- a/src/short-urls/reducers/shortUrlsList.ts +++ b/src/short-urls/reducers/shortUrlsList.ts @@ -6,12 +6,12 @@ import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCrea import { ShortUrl, ShortUrlIdentifier } from '../data'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; +import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder'; import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags'; import { SHORT_URL_DELETED } from './shortUrlDeletion'; import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction, shortUrlMetaType } from './shortUrlMeta'; import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition'; import { ShortUrlsListParams } from './shortUrlsListParams'; -import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder'; /* eslint-disable padding-line-between-statements */ export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START'; diff --git a/src/utils/helpers/leaflet.js b/src/utils/helpers/leaflet.ts similarity index 80% rename from src/utils/helpers/leaflet.js rename to src/utils/helpers/leaflet.ts index 8b9ac82f..2c1f621b 100644 --- a/src/utils/helpers/leaflet.js +++ b/src/utils/helpers/leaflet.ts @@ -1,12 +1,10 @@ -// TODO Migrate this file to Typescript - import L from 'leaflet'; import marker2x from 'leaflet/dist/images/marker-icon-2x.png'; import marker from 'leaflet/dist/images/marker-icon.png'; import markerShadow from 'leaflet/dist/images/marker-shadow.png'; export const fixLeafletIcons = () => { - delete L.Icon.Default.prototype._getIconUrl; + delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: marker2x, diff --git a/test/short-urls/helpers/CreateShortUrlResult.test.js b/test/short-urls/helpers/CreateShortUrlResult.test.tsx similarity index 61% rename from test/short-urls/helpers/CreateShortUrlResult.test.js rename to test/short-urls/helpers/CreateShortUrlResult.test.tsx index 09e41ddf..88173622 100644 --- a/test/short-urls/helpers/CreateShortUrlResult.test.js +++ b/test/short-urls/helpers/CreateShortUrlResult.test.tsx @@ -1,28 +1,31 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { identity } from 'ramda'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; +import CopyToClipboard from 'react-copy-to-clipboard'; import { Tooltip } from 'reactstrap'; +import { Mock } from 'ts-mockery'; import createCreateShortUrlResult from '../../../src/short-urls/helpers/CreateShortUrlResult'; +import { ShortUrl } from '../../../src/short-urls/data'; +import { StateFlagTimeout } from '../../../src/utils/helpers/hooks'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; const copyToClipboard = jest.fn(); - const useStateFlagTimeout = jest.fn(() => [ false, copyToClipboard ]); + const useStateFlagTimeout = jest.fn(() => [ false, copyToClipboard ]) as StateFlagTimeout; const CreateShortUrlResult = createCreateShortUrlResult(useStateFlagTimeout); - const createWrapper = (result, error = false) => { - wrapper = shallow(); + const createWrapper = (result: ShortUrl | null = null, error = false) => { + wrapper = shallow( + , + ); return wrapper; }; - afterEach(() => { - jest.clearAllMocks(); - wrapper && wrapper.unmount(); - }); + afterEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); it('renders an error when error is true', () => { - const wrapper = createWrapper({}, true); + const wrapper = createWrapper(Mock.all(), true); const errorCard = wrapper.find('.bg-danger'); expect(errorCard).toHaveLength(1); @@ -36,7 +39,7 @@ describe('', () => { }); it('renders a result message when result is provided', () => { - const wrapper = createWrapper({ shortUrl: 'https://doma.in/abc123' }); + const wrapper = createWrapper(Mock.of({ shortUrl: 'https://doma.in/abc123' })); expect(wrapper.html()).toContain('Great! The short URL is https://doma.in/abc123'); expect(wrapper.find(CopyToClipboard)).toHaveLength(1); @@ -44,7 +47,7 @@ describe('', () => { }); it('Invokes tooltip timeout when copy to clipboard button is clicked', () => { - const wrapper = createWrapper({ shortUrl: 'https://doma.in/abc123' }); + const wrapper = createWrapper(Mock.of({ shortUrl: 'https://doma.in/abc123' })); const copyBtn = wrapper.find(CopyToClipboard); expect(copyToClipboard).not.toHaveBeenCalled(); diff --git a/test/short-urls/helpers/EditTagsModal.test.js b/test/short-urls/helpers/EditTagsModal.test.tsx similarity index 74% rename from test/short-urls/helpers/EditTagsModal.test.js rename to test/short-urls/helpers/EditTagsModal.test.tsx index ceef068b..50e94c97 100644 --- a/test/short-urls/helpers/EditTagsModal.test.js +++ b/test/short-urls/helpers/EditTagsModal.test.tsx @@ -1,27 +1,31 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { Modal } from 'reactstrap'; +import { Mock } from 'ts-mockery'; import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal'; +import { ShortUrl } from '../../../src/short-urls/data'; +import { ShortUrlTags } from '../../../src/short-urls/reducers/shortUrlTags'; +import { OptionalString } from '../../../src/utils/utils'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; const shortCode = 'abc123'; - const TagsSelector = () => ''; - const editShortUrlTags = jest.fn(() => Promise.resolve()); + const TagsSelector = () => null; + const editShortUrlTags = jest.fn(async () => Promise.resolve()); const resetShortUrlsTags = jest.fn(); const toggle = jest.fn(); - const createWrapper = (shortUrlTags, domain) => { + const createWrapper = (shortUrlTags: ShortUrlTags, domain?: OptionalString) => { const EditTagsModal = createEditTagsModal(TagsSelector); wrapper = shallow( ({ tags: [], shortCode, domain, - originalUrl: 'https://long-domain.com/foo/bar', - }} + longUrl: 'https://long-domain.com/foo/bar', + })} shortUrlTags={shortUrlTags} toggle={toggle} editShortUrlTags={editShortUrlTags} @@ -32,10 +36,8 @@ describe('', () => { return wrapper; }; - afterEach(() => { - wrapper && wrapper.unmount(); - jest.clearAllMocks(); - }); + afterEach(() => wrapper?.unmount()); + afterEach(jest.clearAllMocks); it('renders tags selector and save button when loaded', () => { const wrapper = createWrapper({ @@ -64,7 +66,12 @@ describe('', () => { expect(saveBtn.text()).toEqual('Saving tags...'); }); - it.each([[ undefined ], [ null ], [ 'example.com' ]])('saves tags when save button is clicked', (domain, done) => { + it.each([ + [ undefined ], + [ null ], + [ 'example.com' ], + // @ts-expect-error + ])('saves tags when save button is clicked', (domain: OptionalString, done: jest.DoneCallback) => { const wrapper = createWrapper({ shortCode, tags: [], diff --git a/test/short-urls/helpers/PreviewModal.test.js b/test/short-urls/helpers/PreviewModal.test.tsx similarity index 55% rename from test/short-urls/helpers/PreviewModal.test.js rename to test/short-urls/helpers/PreviewModal.test.tsx index ee7356be..94c1145d 100644 --- a/test/short-urls/helpers/PreviewModal.test.js +++ b/test/short-urls/helpers/PreviewModal.test.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { ExternalLink } from 'react-external-link'; +import { Mock } from 'ts-mockery'; import PreviewModal from '../../../src/short-urls/helpers/PreviewModal'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper; - const url = 'https://doma.in/abc123'; + let wrapper: ShallowWrapper; + const shortUrl = 'https://doma.in/abc123'; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(({ shortUrl })} isOpen={true} toggle={() => {}} />); }); afterEach(() => wrapper.unmount()); @@ -16,13 +18,13 @@ describe('', () => { const externalLink = wrapper.find(ExternalLink); expect(externalLink).toHaveLength(1); - expect(externalLink.prop('href')).toEqual(url); + expect(externalLink.prop('href')).toEqual(shortUrl); }); it('displays an image with the preview of the URL', () => { const img = wrapper.find('img'); expect(img).toHaveLength(1); - expect(img.prop('src')).toEqual(`${url}/preview`); + expect(img.prop('src')).toEqual(`${shortUrl}/preview`); }); }); diff --git a/test/short-urls/helpers/QrCodeModal.test.js b/test/short-urls/helpers/QrCodeModal.test.tsx similarity index 55% rename from test/short-urls/helpers/QrCodeModal.test.js rename to test/short-urls/helpers/QrCodeModal.test.tsx index 44ccf2a3..b0d6b655 100644 --- a/test/short-urls/helpers/QrCodeModal.test.js +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { ExternalLink } from 'react-external-link'; +import { Mock } from 'ts-mockery'; import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper; - const url = 'https://doma.in/abc123'; + let wrapper: ShallowWrapper; + const shortUrl = 'https://doma.in/abc123'; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(({ shortUrl })} isOpen={true} toggle={() => {}} />); }); afterEach(() => wrapper.unmount()); @@ -16,13 +18,13 @@ describe('', () => { const externalLink = wrapper.find(ExternalLink); expect(externalLink).toHaveLength(1); - expect(externalLink.prop('href')).toEqual(url); + expect(externalLink.prop('href')).toEqual(shortUrl); }); it('displays an image with the QR code of the URL', () => { const img = wrapper.find('img'); expect(img).toHaveLength(1); - expect(img.prop('src')).toEqual(`${url}/qr-code`); + expect(img.prop('src')).toEqual(`${shortUrl}/qr-code`); }); }); diff --git a/test/short-urls/helpers/ShortUrlVisitsCount.test.js b/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx similarity index 74% rename from test/short-urls/helpers/ShortUrlVisitsCount.test.js rename to test/short-urls/helpers/ShortUrlVisitsCount.test.tsx index c7f34659..1803c4e8 100644 --- a/test/short-urls/helpers/ShortUrlVisitsCount.test.js +++ b/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx @@ -1,22 +1,24 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { UncontrolledTooltip } from 'reactstrap'; +import { Mock } from 'ts-mockery'; import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; - const createWrapper = (visitsCount, shortUrl) => { + const createWrapper = (visitsCount: number, shortUrl: ShortUrl) => { wrapper = shallow(); return wrapper; }; - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it.each([ undefined, {}])('just returns visits when no maxVisits is provided', (meta) => { const visitsCount = 45; - const wrapper = createWrapper(visitsCount, { meta }); + const wrapper = createWrapper(visitsCount, Mock.of({ meta })); const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control'); const maxVisitsTooltip = wrapper.find(UncontrolledTooltip); @@ -31,7 +33,7 @@ describe('', () => { const visitsCount = 45; const maxVisits = 500; const meta = { maxVisits }; - const wrapper = createWrapper(visitsCount, { meta }); + const wrapper = createWrapper(visitsCount, Mock.of({ meta })); const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control'); const maxVisitsTooltip = wrapper.find(UncontrolledTooltip); diff --git a/test/short-urls/helpers/ShortUrlsRow.test.js b/test/short-urls/helpers/ShortUrlsRow.test.tsx similarity index 75% rename from test/short-urls/helpers/ShortUrlsRow.test.js rename to test/short-urls/helpers/ShortUrlsRow.test.tsx index 38367d45..e878ec57 100644 --- a/test/short-urls/helpers/ShortUrlsRow.test.js +++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx @@ -1,40 +1,51 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import moment from 'moment'; import Moment from 'react-moment'; import { assoc, toString } from 'ramda'; +import { Mock } from 'ts-mockery'; import { ExternalLink } from 'react-external-link'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; +import CopyToClipboard from 'react-copy-to-clipboard'; import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow'; import Tag from '../../../src/tags/helpers/Tag'; +import ColorGenerator from '../../../src/utils/services/ColorGenerator'; +import { StateFlagTimeout } from '../../../src/utils/helpers/hooks'; +import { ShortUrl } from '../../../src/short-urls/data'; +import { ReachableServer } from '../../../src/servers/data'; describe('', () => { - let wrapper; - const mockFunction = () => ''; + let wrapper: ShallowWrapper; + const mockFunction = () => null; const ShortUrlsRowMenu = mockFunction; const stateFlagTimeout = jest.fn(() => true); - const useStateFlagTimeout = jest.fn(() => [ false, stateFlagTimeout ]); - const colorGenerator = { - getColorForKey: mockFunction, - setColorForKey: mockFunction, - }; - const server = { + const useStateFlagTimeout = jest.fn(() => [ false, stateFlagTimeout ]) as StateFlagTimeout; + const colorGenerator = Mock.of({ + getColorForKey: jest.fn(), + setColorForKey: jest.fn(), + }); + const server = Mock.of({ url: 'https://doma.in', - }; - const shortUrl = { + }); + const shortUrl: ShortUrl = { shortCode: 'abc123', shortUrl: 'http://doma.in/abc123', longUrl: 'http://foo.com/bar', dateCreated: moment('2018-05-23 18:30:41').format(), tags: [ 'nodejs', 'reactjs' ], visitsCount: 45, + domain: null, + meta: { + validSince: null, + validUntil: null, + maxVisits: null, + }, }; beforeEach(() => { const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout); wrapper = shallow( - , + , ); }); afterEach(() => wrapper.unmount()); diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.js b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx similarity index 67% rename from test/short-urls/helpers/ShortUrlsRowMenu.test.js rename to test/short-urls/helpers/ShortUrlsRowMenu.test.tsx index 28e1c5c5..e807ed19 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.js +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx @@ -1,43 +1,39 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { ButtonDropdown, DropdownItem } from 'reactstrap'; +import { Mock } from 'ts-mockery'; import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; import PreviewModal from '../../../src/short-urls/helpers/PreviewModal'; import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; +import { ReachableServer } from '../../../src/servers/data'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper; - const DeleteShortUrlModal = () => ''; - const EditTagsModal = () => ''; - const EditMetaModal = () => ''; - const EditShortUrlModal = () => ''; - const onCopyToClipboard = jest.fn(); - const selectedServer = { id: 'abc123' }; - const shortUrl = { + let wrapper: ShallowWrapper; + const DeleteShortUrlModal = () => null; + const EditTagsModal = () => null; + const EditMetaModal = () => null; + const EditShortUrlModal = () => null; + const selectedServer = Mock.of({ id: 'abc123' }); + const shortUrl = Mock.of({ shortCode: 'abc123', shortUrl: 'https://doma.in/abc123', - }; + }); const createWrapper = () => { const ShortUrlsRowMenu = createShortUrlsRowMenu( DeleteShortUrlModal, EditTagsModal, EditMetaModal, EditShortUrlModal, - () => '', + () => null, ); - wrapper = shallow( - , - ); + wrapper = shallow(); return wrapper; }; - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it('renders modal windows', () => { const wrapper = createWrapper(); @@ -62,8 +58,8 @@ describe('', () => { expect(items.find('[divider]')).toHaveLength(1); }); - describe('toggles state when toggling modal windows', () => { - const assert = (modalComponent) => { + describe('toggles state when toggling modals or the dropdown', () => { + const assert = (modalComponent: Function) => { const wrapper = createWrapper(); expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false); @@ -76,13 +72,6 @@ describe('', () => { it('PreviewModal', () => assert(PreviewModal)); it('QrCodeModal', () => assert(QrCodeModal)); it('EditShortUrlModal', () => assert(EditShortUrlModal)); - }); - - it('toggles dropdown state when toggling dropdown', () => { - const wrapper = createWrapper(); - - expect(wrapper.find(ButtonDropdown).prop('isOpen')).toEqual(false); - wrapper.find(ButtonDropdown).prop('toggle')(); - expect(wrapper.find(ButtonDropdown).prop('isOpen')).toEqual(true); + it('EditShortUrlModal', () => assert(ButtonDropdown)); }); }); diff --git a/test/short-urls/helpers/VisitStatsLink.test.js b/test/short-urls/helpers/VisitStatsLink.test.tsx similarity index 51% rename from test/short-urls/helpers/VisitStatsLink.test.js rename to test/short-urls/helpers/VisitStatsLink.test.tsx index f029304e..a2d076dc 100644 --- a/test/short-urls/helpers/VisitStatsLink.test.js +++ b/test/short-urls/helpers/VisitStatsLink.test.tsx @@ -1,21 +1,25 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { Link } from 'react-router-dom'; +import { Mock } from 'ts-mockery'; import VisitStatsLink from '../../../src/short-urls/helpers/VisitStatsLink'; +import { NotFoundServer, ReachableServer } from '../../../src/servers/data'; +import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper; + let wrapper: ShallowWrapper; - afterEach(() => wrapper && wrapper.unmount()); + afterEach(() => wrapper?.unmount()); it.each([ [ undefined, undefined ], [ null, null ], - [{}, null ], - [{}, undefined ], - [ null, {}], - [ undefined, {}], - ])('only renders a plan span when either server or short URL are not set', (selectedServer, shortUrl) => { + [ Mock.of({ id: '1' }), null ], + [ Mock.of({ id: '1' }), undefined ], + [ Mock.of(), Mock.all() ], + [ null, Mock.all() ], + [ undefined, Mock.all() ], + ])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => { wrapper = shallow(Something); const link = wrapper.find(Link); @@ -24,10 +28,14 @@ describe('', () => { }); it.each([ - [{ id: '1' }, { shortCode: 'abc123' }, '/server/1/short-code/abc123/visits' ], [ - { id: '3' }, - { shortCode: 'def456', domain: 'example.com' }, + Mock.of({ id: '1' }), + Mock.of({ shortCode: 'abc123' }), + '/server/1/short-code/abc123/visits', + ], + [ + Mock.of({ id: '3' }), + Mock.of({ shortCode: 'def456', domain: 'example.com' }), '/server/3/short-code/def456/visits?domain=example.com', ], ])('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => { diff --git a/test/utils/helpers/leaflet.test.js b/test/utils/helpers/leaflet.test.ts similarity index 100% rename from test/utils/helpers/leaflet.test.js rename to test/utils/helpers/leaflet.test.ts