mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-09 01:37:24 +03:00
Finished migrating ll short-url helpers to TS
This commit is contained in:
parent
c0f5d9c12c
commit
4b33d39d44
30 changed files with 483 additions and 499 deletions
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -14,6 +14,7 @@ const notFoundServerType = PropTypes.shape({
|
|||
serverNotFound: PropTypes.bool.isRequired,
|
||||
});
|
||||
|
||||
/** @deprecated Use SelectedServer type instead */
|
||||
export const serverType = PropTypes.oneOfType([
|
||||
regularServerType,
|
||||
notFoundServerType,
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface ShortUrl {
|
|||
shortCode: string;
|
||||
shortUrl: string;
|
||||
longUrl: string;
|
||||
dateCreated: string;
|
||||
visitsCount: number;
|
||||
meta: Required<Nullable<ShortUrlMeta>>;
|
||||
tags: string[];
|
||||
|
|
|
@ -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;
|
61
src/short-urls/helpers/CreateShortUrlResult.tsx
Normal file
61
src/short-urls/helpers/CreateShortUrlResult.tsx
Normal 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;
|
|
@ -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;
|
49
src/short-urls/helpers/EditTagsModal.tsx
Normal file
49
src/short-urls/helpers/EditTagsModal.tsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
94
src/short-urls/helpers/ShortUrlsRow.tsx
Normal file
94
src/short-urls/helpers/ShortUrlsRow.tsx
Normal 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;
|
|
@ -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">
|
||||
<FontAwesomeIcon icon={menuIcon} />
|
||||
</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;
|
96
src/short-urls/helpers/ShortUrlsRowMenu.tsx
Normal file
96
src/short-urls/helpers/ShortUrlsRowMenu.tsx
Normal 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">
|
||||
<FontAwesomeIcon icon={menuIcon} />
|
||||
</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;
|
|
@ -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;
|
27
src/short-urls/helpers/VisitStatsLink.tsx
Normal file
27
src/short-urls/helpers/VisitStatsLink.tsx
Normal 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;
|
|
@ -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[];
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
|
@ -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();
|
|
@ -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: [],
|
|
@ -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`);
|
||||
});
|
||||
});
|
|
@ -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`);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
@ -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());
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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) => {
|
Loading…
Reference in a new issue