Finished migrating ll short-url helpers to TS

This commit is contained in:
Alejandro Celaya 2020-08-30 09:59:14 +02:00
parent c0f5d9c12c
commit 4b33d39d44
30 changed files with 483 additions and 499 deletions

9
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -27,14 +27,14 @@ export type SelectedServer = RegularServer | NotFoundServer | null;
export type ServersMap = Record<string, ServerWithId>;
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');

View file

@ -14,6 +14,7 @@ const notFoundServerType = PropTypes.shape({
serverNotFound: PropTypes.bool.isRequired,
});
/** @deprecated Use SelectedServer type instead */
export const serverType = PropTypes.oneOfType([
regularServerType,
notFoundServerType,

View file

@ -16,6 +16,7 @@ export interface ShortUrl {
shortCode: string;
shortUrl: string;
longUrl: string;
dateCreated: string;
visitsCount: number;
meta: Required<Nullable<ShortUrlMeta>>;
tags: string[];

View file

@ -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 (
<Card body color="danger" inverse className="bg-danger mt-3">
An error occurred while creating the URL :(
</Card>
);
}
if (isNil(result)) {
return null;
}
const { shortUrl } = result;
return (
<Card inverse className="bg-main mt-3">
<CardBody>
<b>Great!</b> The short URL is <b>{shortUrl}</b>
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
<button
className="btn btn-light btn-sm create-short-url-result__copy-btn"
id="copyBtn"
type="button"
>
<FontAwesomeIcon icon={copyIcon} /> Copy
</button>
</CopyToClipboard>
<Tooltip placement="left" isOpen={showCopyTooltip} target="copyBtn">
Copied!
</Tooltip>
</CardBody>
</Card>
);
};
CreateShortUrlResultComp.propTypes = propTypes;
return CreateShortUrlResultComp;
};
export default CreateShortUrlResult;

View file

@ -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 (
<Card body color="danger" inverse className="bg-danger mt-3">
An error occurred while creating the URL :(
</Card>
);
}
if (isNil(result)) {
return null;
}
const { shortUrl } = result;
return (
<Card inverse className="bg-main mt-3">
<CardBody>
<b>Great!</b> The short URL is <b>{shortUrl}</b>
<CopyToClipboard text={shortUrl} onCopy={setShowCopyTooltip}>
<button
className="btn btn-light btn-sm create-short-url-result__copy-btn"
id="copyBtn"
type="button"
>
<FontAwesomeIcon icon={copyIcon} /> Copy
</button>
</CopyToClipboard>
<Tooltip placement="left" isOpen={showCopyTooltip} target="copyBtn">
Copied!
</Tooltip>
</CardBody>
</Card>
);
};
export default CreateShortUrlResult;

View file

@ -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 (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>
Edit tags for <ExternalLink href={url} />
</ModalHeader>
<ModalBody>
<TagsSelector tags={selectedTags} onChange={(tags) => setSelectedTags(tags)} />
{shortUrlTags.error && (
<div className="p-2 mt-2 bg-danger text-white text-center">
Something went wrong while saving the tags :(
</div>
)}
</ModalBody>
<ModalFooter>
<button className="btn btn-link" onClick={toggle}>Cancel</button>
<button className="btn btn-primary" type="button" disabled={shortUrlTags.saving} onClick={saveTags}>
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
</button>
</ModalFooter>
</Modal>
);
};
EditTagsModalComp.propTypes = propTypes;
return EditTagsModalComp;
};
export default EditTagsModal;

View file

@ -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<void>;
resetShortUrlsTags: () => void;
}
const EditTagsModal = (TagsSelector: FC<any>) => ( // TODO Use TagsSelector type when available
{ isOpen, toggle, shortUrl, shortUrlTags, editShortUrlTags, resetShortUrlsTags }: EditTagsModalProps,
) => {
const [ selectedTags, setSelectedTags ] = useState<string[]>(shortUrl.tags || []);
useEffect(() => resetShortUrlsTags, []);
const url = shortUrl?.shortUrl ?? '';
const saveTags = async () => editShortUrlTags(shortUrl.shortCode, shortUrl.domain, selectedTags)
.then(toggle)
.catch(() => {});
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>
Edit tags for <ExternalLink href={url} />
</ModalHeader>
<ModalBody>
<TagsSelector tags={selectedTags} onChange={setSelectedTags} />
{shortUrlTags.error && (
<div className="p-2 mt-2 bg-danger text-white text-center">
Something went wrong while saving the tags :(
</div>
)}
</ModalBody>
<ModalFooter>
<button className="btn btn-link" onClick={toggle}>Cancel</button>
<button className="btn btn-primary" type="button" disabled={shortUrlTags.saving} onClick={saveTags}>
{shortUrlTags.saving ? 'Saving tags...' : 'Save tags'}
</button>
</ModalFooter>
</Modal>
);
};
export default EditTagsModal;

View file

@ -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) => (
<Modal isOpen={isOpen} toggle={toggle} size="lg">
<ModalHeader toggle={toggle}>
Preview for <ExternalLink href={url}>{url}</ExternalLink>
Preview for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
</ModalHeader>
<ModalBody>
<div className="text-center">
<p className="preview-modal__loader">Loading...</p>
<img src={`${url}/preview`} className="preview-modal__img" alt="Preview" />
<img src={`${shortUrl}/preview`} className="preview-modal__img" alt="Preview" />
</div>
</ModalBody>
</Modal>
);
PreviewModal.propTypes = propTypes;
export default PreviewModal;

View file

@ -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) => (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>
QR code for <ExternalLink href={url}>{url}</ExternalLink>
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
</ModalHeader>
<ModalBody>
<div className="text-center">
<img src={`${url}/qr-code`} className="qr-code-modal__img" alt="QR code" />
<img src={`${shortUrl}/qr-code`} className="qr-code-modal__img" alt="QR code" />
</div>
</ModalBody>
</Modal>
);
QrCodeModal.propTypes = propTypes;
export default QrCodeModal;

View file

@ -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 = (
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
<strong
@ -34,7 +29,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
}
const prettifiedMaxVisits = prettify(maxVisits);
const tooltipRef = useRef();
const tooltipRef = useRef<HTMLElement | null>();
return (
<React.Fragment>
@ -52,13 +47,11 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
</sup>
</small>
</span>
<UncontrolledTooltip target={() => tooltipRef.current} placement="bottom">
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="bottom">
This short URL will not accept more than <b>{prettifiedMaxVisits}</b> visits.
</UncontrolledTooltip>
</React.Fragment>
);
};
ShortUrlVisitsCount.propTypes = propTypes;
export default ShortUrlVisitsCount;

View file

@ -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 <i className="indivisible"><small>No tags</small></i>;
}
const selectedTags = shortUrlsListParams.tags || [];
return tags.map((tag) => (
<Tag
colorGenerator={colorGenerator}
key={tag}
text={tag}
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
/>
));
};
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
} else {
setActive(true);
}
}, [ shortUrl.visitsCount ]);
return (
<tr className="short-urls-row">
<td className="indivisible short-urls-row__cell" data-th="Created at: ">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</td>
<td className="short-urls-row__cell" data-th="Short URL: ">
<span className="indivisible short-urls-row__cell--relative">
<ExternalLink href={shortUrl.shortUrl} />
<CopyToClipboard text={shortUrl.shortUrl} onCopy={setCopiedToClipboard}>
<FontAwesomeIcon icon={copyIcon} className="ml-2 short-urls-row__copy-btn" />
</CopyToClipboard>
<span className="badge badge-warning short-urls-row__copy-hint" hidden={!copiedToClipboard}>
Copied short URL!
</span>
</span>
</td>
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
<ExternalLink href={shortUrl.longUrl} />
</td>
<td className="short-urls-row__cell" data-th="Tags: ">{renderTags(shortUrl.tags)}</td>
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">
<ShortUrlVisitsCount
visitsCount={shortUrl.visitsCount}
shortUrl={shortUrl}
selectedServer={selectedServer}
active={active}
/>
</td>
<td className="short-urls-row__cell">
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />
</td>
</tr>
);
};
ShortUrlsRowComp.propTypes = propTypes;
return ShortUrlsRowComp;
};
export default ShortUrlsRow;

View file

@ -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<ShortUrlsRowMenuProps>,
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 <i className="indivisible"><small>No tags</small></i>;
}
const selectedTags = shortUrlsListParams.tags ?? [];
return tags.map((tag) => (
<Tag
colorGenerator={colorGenerator}
key={tag}
text={tag}
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
/>
));
};
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
} else {
setActive();
}
}, [ shortUrl.visitsCount ]);
return (
<tr className="short-urls-row">
<td className="indivisible short-urls-row__cell" data-th="Created at: ">
<Moment format="YYYY-MM-DD HH:mm">{shortUrl.dateCreated}</Moment>
</td>
<td className="short-urls-row__cell" data-th="Short URL: ">
<span className="indivisible short-urls-row__cell--relative">
<ExternalLink href={shortUrl.shortUrl} />
<CopyToClipboard text={shortUrl.shortUrl} onCopy={setCopiedToClipboard}>
<FontAwesomeIcon icon={copyIcon} className="ml-2 short-urls-row__copy-btn" />
</CopyToClipboard>
<span className="badge badge-warning short-urls-row__copy-hint" hidden={!copiedToClipboard}>
Copied short URL!
</span>
</span>
</td>
<td className="short-urls-row__cell short-urls-row__cell--break" data-th="Long URL: ">
<ExternalLink href={shortUrl.longUrl} />
</td>
<td className="short-urls-row__cell" data-th="Tags: ">{renderTags(shortUrl.tags)}</td>
<td className="short-urls-row__cell text-md-right" data-th="Visits: ">
<ShortUrlVisitsCount
visitsCount={shortUrl.visitsCount}
shortUrl={shortUrl}
selectedServer={selectedServer}
active={active}
/>
</td>
<td className="short-urls-row__cell">
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />
</td>
</tr>
);
};
export default ShortUrlsRow;

View file

@ -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 (
<ButtonDropdown toggle={toggle} isOpen={isOpen}>
<DropdownToggle size="sm" caret outline className="short-urls-row-menu__dropdown-toggle">
&nbsp;<FontAwesomeIcon icon={menuIcon} />&nbsp;
</DropdownToggle>
<DropdownMenu right>
<DropdownItem tag={VisitStatsLink} selectedServer={selectedServer} shortUrl={shortUrl}>
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
</DropdownItem>
<DropdownItem onClick={toggleTags}>
<FontAwesomeIcon icon={tagsIcon} fixedWidth /> Edit tags
</DropdownItem>
<EditTagsModal shortUrl={shortUrl} isOpen={isTagsModalOpen} toggle={toggleTags} />
<ForServerVersion minVersion="1.18.0">
<DropdownItem onClick={toggleMeta}>
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit metadata
</DropdownItem>
<EditMetaModal shortUrl={shortUrl} isOpen={isMetaModalOpen} toggle={toggleMeta} />
</ForServerVersion>
<ForServerVersion minVersion="2.1.0">
<DropdownItem onClick={toggleEdit}>
<FontAwesomeIcon icon={linkIcon} fixedWidth /> Edit long URL
</DropdownItem>
<EditShortUrlModal shortUrl={shortUrl} isOpen={isEditModalOpen} toggle={toggleEdit} />
</ForServerVersion>
<DropdownItem onClick={toggleQrCode}>
<FontAwesomeIcon icon={qrIcon} fixedWidth /> QR code
</DropdownItem>
<QrCodeModal url={completeShortUrl} isOpen={isQrModalOpen} toggle={toggleQrCode} />
<ForServerVersion maxVersion="1.x">
<DropdownItem onClick={togglePreview}>
<FontAwesomeIcon icon={pictureIcon} fixedWidth /> Preview
</DropdownItem>
<PreviewModal url={completeShortUrl} isOpen={isPreviewModalOpen} toggle={togglePreview} />
</ForServerVersion>
<DropdownItem divider />
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Delete short URL
</DropdownItem>
<DeleteShortUrlModal shortUrl={shortUrl} isOpen={isDeleteModalOpen} toggle={toggleDelete} />
</DropdownMenu>
</ButtonDropdown>
);
};
ShortUrlsRowMenuComp.propTypes = propTypes;
return ShortUrlsRowMenuComp;
};
export default ShortUrlsRowMenu;

View file

@ -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<ShortUrlModalProps>;
const ShortUrlsRowMenu = (
DeleteShortUrlModal: ShortUrlModal,
EditTagsModal: ShortUrlModal,
EditMetaModal: ShortUrlModal,
EditShortUrlModal: ShortUrlModal,
ForServerVersion: FC<Versions>,
) => ({ 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 (
<ButtonDropdown toggle={toggle} isOpen={isOpen}>
<DropdownToggle size="sm" caret outline className="short-urls-row-menu__dropdown-toggle">
&nbsp;<FontAwesomeIcon icon={menuIcon} />&nbsp;
</DropdownToggle>
<DropdownMenu right>
<DropdownItem tag={VisitStatsLink} selectedServer={selectedServer} shortUrl={shortUrl}>
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
</DropdownItem>
<DropdownItem onClick={toggleTags}>
<FontAwesomeIcon icon={tagsIcon} fixedWidth /> Edit tags
</DropdownItem>
<EditTagsModal shortUrl={shortUrl} isOpen={isTagsModalOpen} toggle={toggleTags} />
<ForServerVersion minVersion="1.18.0">
<DropdownItem onClick={toggleMeta}>
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit metadata
</DropdownItem>
<EditMetaModal shortUrl={shortUrl} isOpen={isMetaModalOpen} toggle={toggleMeta} />
</ForServerVersion>
<ForServerVersion minVersion="2.1.0">
<DropdownItem onClick={toggleEdit}>
<FontAwesomeIcon icon={linkIcon} fixedWidth /> Edit long URL
</DropdownItem>
<EditShortUrlModal shortUrl={shortUrl} isOpen={isEditModalOpen} toggle={toggleEdit} />
</ForServerVersion>
<DropdownItem onClick={toggleQrCode}>
<FontAwesomeIcon icon={qrIcon} fixedWidth /> QR code
</DropdownItem>
<QrCodeModal shortUrl={shortUrl} isOpen={isQrModalOpen} toggle={toggleQrCode} />
<ForServerVersion maxVersion="1.x">
<DropdownItem onClick={togglePreview}>
<FontAwesomeIcon icon={pictureIcon} fixedWidth /> Preview
</DropdownItem>
<PreviewModal shortUrl={shortUrl} isOpen={isPreviewModalOpen} toggle={togglePreview} />
</ForServerVersion>
<DropdownItem divider />
<DropdownItem className="short-urls-row-menu__dropdown-item--danger" onClick={toggleDelete}>
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Delete short URL
</DropdownItem>
<DeleteShortUrlModal shortUrl={shortUrl} isOpen={isDeleteModalOpen} toggle={toggleDelete} />
</DropdownMenu>
</ButtonDropdown>
);
};
export default ShortUrlsRowMenu;

View file

@ -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 <span {...rest}>{children}</span>;
}
return <Link to={buildVisitsUrl(selectedServer, shortUrl)} {...rest}>{children}</Link>;
};
VisitStatsLink.propTypes = propTypes;
export default VisitStatsLink;

View file

@ -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<VisitStatsLinkProps & Record<string | number, any>> = (
{ selectedServer, shortUrl, children, ...rest },
) => {
if (!selectedServer || !isServerWithId(selectedServer) || !shortUrl) {
return <span {...rest}>{children}</span>;
}
return <Link to={buildVisitsUrl(selectedServer, shortUrl)} {...rest}>{children}</Link>;
};
export default VisitStatsLink;

View file

@ -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[];

View file

@ -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';

View file

@ -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,

View file

@ -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('<CreateShortUrlResult />', () => {
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(<CreateShortUrlResult resetCreateShortUrl={identity} result={result} error={error} />);
const createWrapper = (result: ShortUrl | null = null, error = false) => {
wrapper = shallow(
<CreateShortUrlResult resetCreateShortUrl={identity} result={result} error={error} saving={false} />,
);
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<ShortUrl>(), true);
const errorCard = wrapper.find('.bg-danger');
expect(errorCard).toHaveLength(1);
@ -36,7 +39,7 @@ describe('<CreateShortUrlResult />', () => {
});
it('renders a result message when result is provided', () => {
const wrapper = createWrapper({ shortUrl: 'https://doma.in/abc123' });
const wrapper = createWrapper(Mock.of<ShortUrl>({ shortUrl: 'https://doma.in/abc123' }));
expect(wrapper.html()).toContain('<b>Great!</b> The short URL is <b>https://doma.in/abc123</b>');
expect(wrapper.find(CopyToClipboard)).toHaveLength(1);
@ -44,7 +47,7 @@ describe('<CreateShortUrlResult />', () => {
});
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>({ shortUrl: 'https://doma.in/abc123' }));
const copyBtn = wrapper.find(CopyToClipboard);
expect(copyToClipboard).not.toHaveBeenCalled();

View file

@ -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('<EditTagsModal />', () => {
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(
<EditTagsModal
isOpen={true}
shortUrl={{
shortUrl={Mock.of<ShortUrl>({
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('<EditTagsModal />', () => {
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('<EditTagsModal />', () => {
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: [],

View file

@ -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('<PreviewModal />', () => {
let wrapper;
const url = 'https://doma.in/abc123';
let wrapper: ShallowWrapper;
const shortUrl = 'https://doma.in/abc123';
beforeEach(() => {
wrapper = shallow(<PreviewModal url={url} />);
wrapper = shallow(<PreviewModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
});
afterEach(() => wrapper.unmount());
@ -16,13 +18,13 @@ describe('<PreviewModal />', () => {
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`);
});
});

View file

@ -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('<QrCodeModal />', () => {
let wrapper;
const url = 'https://doma.in/abc123';
let wrapper: ShallowWrapper;
const shortUrl = 'https://doma.in/abc123';
beforeEach(() => {
wrapper = shallow(<QrCodeModal url={url} />);
wrapper = shallow(<QrCodeModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
});
afterEach(() => wrapper.unmount());
@ -16,13 +18,13 @@ describe('<QrCodeModal />', () => {
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`);
});
});

View file

@ -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('<ShortUrlVisitsCount />', () => {
let wrapper;
let wrapper: ShallowWrapper;
const createWrapper = (visitsCount, shortUrl) => {
const createWrapper = (visitsCount: number, shortUrl: ShortUrl) => {
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} shortUrl={shortUrl} />);
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<ShortUrl>({ meta }));
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
@ -31,7 +33,7 @@ describe('<ShortUrlVisitsCount />', () => {
const visitsCount = 45;
const maxVisits = 500;
const meta = { maxVisits };
const wrapper = createWrapper(visitsCount, { meta });
const wrapper = createWrapper(visitsCount, Mock.of<ShortUrl>({ meta }));
const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);

View file

@ -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('<ShortUrlsRow />', () => {
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<ColorGenerator>({
getColorForKey: jest.fn(),
setColorForKey: jest.fn(),
});
const server = Mock.of<ReachableServer>({
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(
<ShortUrlsRow shortUrlsListParams={{}} refreshList={mockFunction} selecrtedServer={server} shortUrl={shortUrl} />,
<ShortUrlsRow shortUrlsListParams={{}} refreshList={mockFunction} selectedServer={server} shortUrl={shortUrl} />,
);
});
afterEach(() => wrapper.unmount());

View file

@ -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('<ShortUrlsRowMenu />', () => {
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<ReachableServer>({ id: 'abc123' });
const shortUrl = Mock.of<ShortUrl>({
shortCode: 'abc123',
shortUrl: 'https://doma.in/abc123',
};
});
const createWrapper = () => {
const ShortUrlsRowMenu = createShortUrlsRowMenu(
DeleteShortUrlModal,
EditTagsModal,
EditMetaModal,
EditShortUrlModal,
() => '',
() => null,
);
wrapper = shallow(
<ShortUrlsRowMenu
selectedServer={selectedServer}
shortUrl={shortUrl}
onCopyToClipboard={onCopyToClipboard}
/>,
);
wrapper = shallow(<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />);
return wrapper;
};
afterEach(() => wrapper && wrapper.unmount());
afterEach(() => wrapper?.unmount());
it('renders modal windows', () => {
const wrapper = createWrapper();
@ -62,8 +58,8 @@ describe('<ShortUrlsRowMenu />', () => {
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('<ShortUrlsRowMenu />', () => {
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));
});
});

View file

@ -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('<VisitStatsLink />', () => {
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<ReachableServer>({ id: '1' }), null ],
[ Mock.of<ReachableServer>({ id: '1' }), undefined ],
[ Mock.of<NotFoundServer>(), Mock.all<ShortUrl>() ],
[ null, Mock.all<ShortUrl>() ],
[ undefined, Mock.all<ShortUrl>() ],
])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => {
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
const link = wrapper.find(Link);
@ -24,10 +28,14 @@ describe('<VisitStatsLink />', () => {
});
it.each([
[{ id: '1' }, { shortCode: 'abc123' }, '/server/1/short-code/abc123/visits' ],
[
{ id: '3' },
{ shortCode: 'def456', domain: 'example.com' },
Mock.of<ReachableServer>({ id: '1' }),
Mock.of<ShortUrl>({ shortCode: 'abc123' }),
'/server/1/short-code/abc123/visits',
],
[
Mock.of<ReachableServer>({ id: '3' }),
Mock.of<ShortUrl>({ shortCode: 'def456', domain: 'example.com' }),
'/server/3/short-code/def456/visits?domain=example.com',
],
])('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => {