mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 02:07:26 +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"
|
"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": {
|
"@types/react-datepicker": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-1.8.0.tgz",
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"@types/qs": "^6.9.4",
|
"@types/qs": "^6.9.4",
|
||||||
"@types/ramda": "^0.27.14",
|
"@types/ramda": "^0.27.14",
|
||||||
"@types/react": "^16.9.46",
|
"@types/react": "^16.9.46",
|
||||||
|
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||||
"@types/react-datepicker": "~1.8.0",
|
"@types/react-datepicker": "~1.8.0",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-redux": "^7.1.9",
|
"@types/react-redux": "^7.1.9",
|
||||||
|
|
|
@ -27,14 +27,14 @@ export type SelectedServer = RegularServer | NotFoundServer | null;
|
||||||
|
|
||||||
export type ServersMap = Record<string, ServerWithId>;
|
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;
|
!!(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 =>
|
export const isServerWithId = (server: SelectedServer | ServerWithId): server is ServerWithId =>
|
||||||
!!server?.hasOwnProperty('id');
|
!!server?.hasOwnProperty('id');
|
||||||
|
|
||||||
|
export const isReachableServer = (server: SelectedServer): server is ReachableServer =>
|
||||||
|
!!server?.hasOwnProperty('printableVersion');
|
||||||
|
|
||||||
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
|
export const isNotFoundServer = (server: SelectedServer): server is NotFoundServer =>
|
||||||
!!server?.hasOwnProperty('serverNotFound');
|
!!server?.hasOwnProperty('serverNotFound');
|
||||||
|
|
|
@ -14,6 +14,7 @@ const notFoundServerType = PropTypes.shape({
|
||||||
serverNotFound: PropTypes.bool.isRequired,
|
serverNotFound: PropTypes.bool.isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @deprecated Use SelectedServer type instead */
|
||||||
export const serverType = PropTypes.oneOfType([
|
export const serverType = PropTypes.oneOfType([
|
||||||
regularServerType,
|
regularServerType,
|
||||||
notFoundServerType,
|
notFoundServerType,
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface ShortUrl {
|
||||||
shortCode: string;
|
shortCode: string;
|
||||||
shortUrl: string;
|
shortUrl: string;
|
||||||
longUrl: string;
|
longUrl: string;
|
||||||
|
dateCreated: string;
|
||||||
visitsCount: number;
|
visitsCount: number;
|
||||||
meta: Required<Nullable<ShortUrlMeta>>;
|
meta: Required<Nullable<ShortUrlMeta>>;
|
||||||
tags: string[];
|
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 React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { ShortUrlModalProps } from '../data';
|
||||||
import './PreviewModal.scss';
|
import './PreviewModal.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const PreviewModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => (
|
||||||
url: PropTypes.string,
|
|
||||||
toggle: PropTypes.func,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const PreviewModal = ({ url, toggle, isOpen }) => (
|
|
||||||
<Modal isOpen={isOpen} toggle={toggle} size="lg">
|
<Modal isOpen={isOpen} toggle={toggle} size="lg">
|
||||||
<ModalHeader toggle={toggle}>
|
<ModalHeader toggle={toggle}>
|
||||||
Preview for <ExternalLink href={url}>{url}</ExternalLink>
|
Preview for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="preview-modal__loader">Loading...</p>
|
<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>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
PreviewModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default PreviewModal;
|
export default PreviewModal;
|
|
@ -1,28 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { ShortUrlModalProps } from '../data';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
|
||||||
const propTypes = {
|
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => (
|
||||||
url: PropTypes.string,
|
|
||||||
toggle: PropTypes.func,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const QrCodeModal = ({ url, toggle, isOpen }) => (
|
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
<Modal isOpen={isOpen} toggle={toggle} centered>
|
||||||
<ModalHeader toggle={toggle}>
|
<ModalHeader toggle={toggle}>
|
||||||
QR code for <ExternalLink href={url}>{url}</ExternalLink>
|
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="text-center">
|
<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>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
QrCodeModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default QrCodeModal;
|
export default QrCodeModal;
|
|
@ -1,24 +1,19 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { serverType } from '../../servers/prop-types';
|
|
||||||
import { prettify } from '../../utils/helpers/numbers';
|
import { prettify } from '../../utils/helpers/numbers';
|
||||||
import { shortUrlType } from '../reducers/shortUrlsList';
|
import VisitStatsLink, { VisitStatsLinkProps } from './VisitStatsLink';
|
||||||
import VisitStatsLink from './VisitStatsLink';
|
|
||||||
import './ShortUrlVisitsCount.scss';
|
import './ShortUrlVisitsCount.scss';
|
||||||
|
|
||||||
const propTypes = {
|
export interface ShortUrlVisitsCount extends VisitStatsLinkProps {
|
||||||
visitsCount: PropTypes.number.isRequired,
|
visitsCount: number;
|
||||||
shortUrl: shortUrlType,
|
active?: boolean;
|
||||||
selectedServer: serverType,
|
}
|
||||||
active: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }) => {
|
const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = false }: ShortUrlVisitsCount) => {
|
||||||
const maxVisits = shortUrl && shortUrl.meta && shortUrl.meta.maxVisits;
|
const maxVisits = shortUrl?.meta?.maxVisits;
|
||||||
const visitsLink = (
|
const visitsLink = (
|
||||||
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>
|
||||||
<strong
|
<strong
|
||||||
|
@ -34,7 +29,7 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
|
||||||
}
|
}
|
||||||
|
|
||||||
const prettifiedMaxVisits = prettify(maxVisits);
|
const prettifiedMaxVisits = prettify(maxVisits);
|
||||||
const tooltipRef = useRef();
|
const tooltipRef = useRef<HTMLElement | null>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -52,13 +47,11 @@ const ShortUrlVisitsCount = ({ visitsCount, shortUrl, selectedServer, active = f
|
||||||
</sup>
|
</sup>
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</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.
|
This short URL will not accept more than <b>{prettifiedMaxVisits}</b> visits.
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ShortUrlVisitsCount.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default ShortUrlVisitsCount;
|
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 { Action, Dispatch } from 'redux';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
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';
|
export const RESET_EDIT_SHORT_URL_TAGS = 'shlink/shortUrlTags/RESET_EDIT_SHORT_URL_TAGS';
|
||||||
/* eslint-enable padding-line-between-statements */
|
/* 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 {
|
export interface ShortUrlTags {
|
||||||
shortCode: string | null;
|
shortCode: string | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
|
@ -6,12 +6,12 @@ import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCrea
|
||||||
import { ShortUrl, ShortUrlIdentifier } from '../data';
|
import { ShortUrl, ShortUrlIdentifier } from '../data';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
import { EditShortUrlTagsAction, SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||||
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||||
import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction, shortUrlMetaType } from './shortUrlMeta';
|
import { SHORT_URL_META_EDITED, ShortUrlMetaEditedAction, shortUrlMetaType } from './shortUrlMeta';
|
||||||
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
||||||
import { ShortUrlsListParams } from './shortUrlsListParams';
|
import { ShortUrlsListParams } from './shortUrlsListParams';
|
||||||
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
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 L from 'leaflet';
|
||||||
import marker2x from 'leaflet/dist/images/marker-icon-2x.png';
|
import marker2x from 'leaflet/dist/images/marker-icon-2x.png';
|
||||||
import marker from 'leaflet/dist/images/marker-icon.png';
|
import marker from 'leaflet/dist/images/marker-icon.png';
|
||||||
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||||
|
|
||||||
export const fixLeafletIcons = () => {
|
export const fixLeafletIcons = () => {
|
||||||
delete L.Icon.Default.prototype._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
|
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: marker2x,
|
iconRetinaUrl: marker2x,
|
|
@ -1,28 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { identity } from 'ramda';
|
import { identity } from 'ramda';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { Tooltip } from 'reactstrap';
|
import { Tooltip } from 'reactstrap';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import createCreateShortUrlResult from '../../../src/short-urls/helpers/CreateShortUrlResult';
|
import createCreateShortUrlResult from '../../../src/short-urls/helpers/CreateShortUrlResult';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
import { StateFlagTimeout } from '../../../src/utils/helpers/hooks';
|
||||||
|
|
||||||
describe('<CreateShortUrlResult />', () => {
|
describe('<CreateShortUrlResult />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const copyToClipboard = jest.fn();
|
const copyToClipboard = jest.fn();
|
||||||
const useStateFlagTimeout = jest.fn(() => [ false, copyToClipboard ]);
|
const useStateFlagTimeout = jest.fn(() => [ false, copyToClipboard ]) as StateFlagTimeout;
|
||||||
const CreateShortUrlResult = createCreateShortUrlResult(useStateFlagTimeout);
|
const CreateShortUrlResult = createCreateShortUrlResult(useStateFlagTimeout);
|
||||||
const createWrapper = (result, error = false) => {
|
const createWrapper = (result: ShortUrl | null = null, error = false) => {
|
||||||
wrapper = shallow(<CreateShortUrlResult resetCreateShortUrl={identity} result={result} error={error} />);
|
wrapper = shallow(
|
||||||
|
<CreateShortUrlResult resetCreateShortUrl={identity} result={result} error={error} saving={false} />,
|
||||||
|
);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(jest.clearAllMocks);
|
||||||
jest.clearAllMocks();
|
afterEach(() => wrapper?.unmount());
|
||||||
wrapper && wrapper.unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders an error when error is true', () => {
|
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');
|
const errorCard = wrapper.find('.bg-danger');
|
||||||
|
|
||||||
expect(errorCard).toHaveLength(1);
|
expect(errorCard).toHaveLength(1);
|
||||||
|
@ -36,7 +39,7 @@ describe('<CreateShortUrlResult />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a result message when result is provided', () => {
|
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.html()).toContain('<b>Great!</b> The short URL is <b>https://doma.in/abc123</b>');
|
||||||
expect(wrapper.find(CopyToClipboard)).toHaveLength(1);
|
expect(wrapper.find(CopyToClipboard)).toHaveLength(1);
|
||||||
|
@ -44,7 +47,7 @@ describe('<CreateShortUrlResult />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Invokes tooltip timeout when copy to clipboard button is clicked', () => {
|
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);
|
const copyBtn = wrapper.find(CopyToClipboard);
|
||||||
|
|
||||||
expect(copyToClipboard).not.toHaveBeenCalled();
|
expect(copyToClipboard).not.toHaveBeenCalled();
|
|
@ -1,27 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Modal } from 'reactstrap';
|
import { Modal } from 'reactstrap';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import createEditTagsModal from '../../../src/short-urls/helpers/EditTagsModal';
|
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 />', () => {
|
describe('<EditTagsModal />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const shortCode = 'abc123';
|
const shortCode = 'abc123';
|
||||||
const TagsSelector = () => '';
|
const TagsSelector = () => null;
|
||||||
const editShortUrlTags = jest.fn(() => Promise.resolve());
|
const editShortUrlTags = jest.fn(async () => Promise.resolve());
|
||||||
const resetShortUrlsTags = jest.fn();
|
const resetShortUrlsTags = jest.fn();
|
||||||
const toggle = jest.fn();
|
const toggle = jest.fn();
|
||||||
const createWrapper = (shortUrlTags, domain) => {
|
const createWrapper = (shortUrlTags: ShortUrlTags, domain?: OptionalString) => {
|
||||||
const EditTagsModal = createEditTagsModal(TagsSelector);
|
const EditTagsModal = createEditTagsModal(TagsSelector);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<EditTagsModal
|
<EditTagsModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
shortUrl={{
|
shortUrl={Mock.of<ShortUrl>({
|
||||||
tags: [],
|
tags: [],
|
||||||
shortCode,
|
shortCode,
|
||||||
domain,
|
domain,
|
||||||
originalUrl: 'https://long-domain.com/foo/bar',
|
longUrl: 'https://long-domain.com/foo/bar',
|
||||||
}}
|
})}
|
||||||
shortUrlTags={shortUrlTags}
|
shortUrlTags={shortUrlTags}
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
editShortUrlTags={editShortUrlTags}
|
editShortUrlTags={editShortUrlTags}
|
||||||
|
@ -32,10 +36,8 @@ describe('<EditTagsModal />', () => {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => wrapper?.unmount());
|
||||||
wrapper && wrapper.unmount();
|
afterEach(jest.clearAllMocks);
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders tags selector and save button when loaded', () => {
|
it('renders tags selector and save button when loaded', () => {
|
||||||
const wrapper = createWrapper({
|
const wrapper = createWrapper({
|
||||||
|
@ -64,7 +66,12 @@ describe('<EditTagsModal />', () => {
|
||||||
expect(saveBtn.text()).toEqual('Saving tags...');
|
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({
|
const wrapper = createWrapper({
|
||||||
shortCode,
|
shortCode,
|
||||||
tags: [],
|
tags: [],
|
|
@ -1,14 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<PreviewModal />', () => {
|
describe('<PreviewModal />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const url = 'https://doma.in/abc123';
|
const shortUrl = 'https://doma.in/abc123';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<PreviewModal url={url} />);
|
wrapper = shallow(<PreviewModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
|
@ -16,13 +18,13 @@ describe('<PreviewModal />', () => {
|
||||||
const externalLink = wrapper.find(ExternalLink);
|
const externalLink = wrapper.find(ExternalLink);
|
||||||
|
|
||||||
expect(externalLink).toHaveLength(1);
|
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', () => {
|
it('displays an image with the preview of the URL', () => {
|
||||||
const img = wrapper.find('img');
|
const img = wrapper.find('img');
|
||||||
|
|
||||||
expect(img).toHaveLength(1);
|
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 React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<QrCodeModal />', () => {
|
describe('<QrCodeModal />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const url = 'https://doma.in/abc123';
|
const shortUrl = 'https://doma.in/abc123';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<QrCodeModal url={url} />);
|
wrapper = shallow(<QrCodeModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
||||||
|
|
||||||
|
@ -16,13 +18,13 @@ describe('<QrCodeModal />', () => {
|
||||||
const externalLink = wrapper.find(ExternalLink);
|
const externalLink = wrapper.find(ExternalLink);
|
||||||
|
|
||||||
expect(externalLink).toHaveLength(1);
|
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', () => {
|
it('displays an image with the QR code of the URL', () => {
|
||||||
const img = wrapper.find('img');
|
const img = wrapper.find('img');
|
||||||
|
|
||||||
expect(img).toHaveLength(1);
|
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 React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount';
|
import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<ShortUrlVisitsCount />', () => {
|
describe('<ShortUrlVisitsCount />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
const createWrapper = (visitsCount, shortUrl) => {
|
const createWrapper = (visitsCount: number, shortUrl: ShortUrl) => {
|
||||||
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} shortUrl={shortUrl} />);
|
wrapper = shallow(<ShortUrlVisitsCount visitsCount={visitsCount} shortUrl={shortUrl} />);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it.each([ undefined, {}])('just returns visits when no maxVisits is provided', (meta) => {
|
it.each([ undefined, {}])('just returns visits when no maxVisits is provided', (meta) => {
|
||||||
const visitsCount = 45;
|
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 maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ describe('<ShortUrlVisitsCount />', () => {
|
||||||
const visitsCount = 45;
|
const visitsCount = 45;
|
||||||
const maxVisits = 500;
|
const maxVisits = 500;
|
||||||
const meta = { maxVisits };
|
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 maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control');
|
||||||
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
const maxVisitsTooltip = wrapper.find(UncontrolledTooltip);
|
||||||
|
|
|
@ -1,40 +1,51 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import { assoc, toString } from 'ramda';
|
import { assoc, toString } from 'ramda';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import { ExternalLink } from 'react-external-link';
|
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 createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow';
|
||||||
import Tag from '../../../src/tags/helpers/Tag';
|
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 />', () => {
|
describe('<ShortUrlsRow />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const mockFunction = () => '';
|
const mockFunction = () => null;
|
||||||
const ShortUrlsRowMenu = mockFunction;
|
const ShortUrlsRowMenu = mockFunction;
|
||||||
const stateFlagTimeout = jest.fn(() => true);
|
const stateFlagTimeout = jest.fn(() => true);
|
||||||
const useStateFlagTimeout = jest.fn(() => [ false, stateFlagTimeout ]);
|
const useStateFlagTimeout = jest.fn(() => [ false, stateFlagTimeout ]) as StateFlagTimeout;
|
||||||
const colorGenerator = {
|
const colorGenerator = Mock.of<ColorGenerator>({
|
||||||
getColorForKey: mockFunction,
|
getColorForKey: jest.fn(),
|
||||||
setColorForKey: mockFunction,
|
setColorForKey: jest.fn(),
|
||||||
};
|
});
|
||||||
const server = {
|
const server = Mock.of<ReachableServer>({
|
||||||
url: 'https://doma.in',
|
url: 'https://doma.in',
|
||||||
};
|
});
|
||||||
const shortUrl = {
|
const shortUrl: ShortUrl = {
|
||||||
shortCode: 'abc123',
|
shortCode: 'abc123',
|
||||||
shortUrl: 'http://doma.in/abc123',
|
shortUrl: 'http://doma.in/abc123',
|
||||||
longUrl: 'http://foo.com/bar',
|
longUrl: 'http://foo.com/bar',
|
||||||
dateCreated: moment('2018-05-23 18:30:41').format(),
|
dateCreated: moment('2018-05-23 18:30:41').format(),
|
||||||
tags: [ 'nodejs', 'reactjs' ],
|
tags: [ 'nodejs', 'reactjs' ],
|
||||||
visitsCount: 45,
|
visitsCount: 45,
|
||||||
|
domain: null,
|
||||||
|
meta: {
|
||||||
|
validSince: null,
|
||||||
|
validUntil: null,
|
||||||
|
maxVisits: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout);
|
const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
<ShortUrlsRow shortUrlsListParams={{}} refreshList={mockFunction} selecrtedServer={server} shortUrl={shortUrl} />,
|
<ShortUrlsRow shortUrlsListParams={{}} refreshList={mockFunction} selectedServer={server} shortUrl={shortUrl} />,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
afterEach(() => wrapper.unmount());
|
afterEach(() => wrapper.unmount());
|
|
@ -1,43 +1,39 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
import { ButtonDropdown, DropdownItem } from 'reactstrap';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu';
|
||||||
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
import PreviewModal from '../../../src/short-urls/helpers/PreviewModal';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
|
import { ReachableServer } from '../../../src/servers/data';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<ShortUrlsRowMenu />', () => {
|
describe('<ShortUrlsRowMenu />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const DeleteShortUrlModal = () => '';
|
const DeleteShortUrlModal = () => null;
|
||||||
const EditTagsModal = () => '';
|
const EditTagsModal = () => null;
|
||||||
const EditMetaModal = () => '';
|
const EditMetaModal = () => null;
|
||||||
const EditShortUrlModal = () => '';
|
const EditShortUrlModal = () => null;
|
||||||
const onCopyToClipboard = jest.fn();
|
const selectedServer = Mock.of<ReachableServer>({ id: 'abc123' });
|
||||||
const selectedServer = { id: 'abc123' };
|
const shortUrl = Mock.of<ShortUrl>({
|
||||||
const shortUrl = {
|
|
||||||
shortCode: 'abc123',
|
shortCode: 'abc123',
|
||||||
shortUrl: 'https://doma.in/abc123',
|
shortUrl: 'https://doma.in/abc123',
|
||||||
};
|
});
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
const ShortUrlsRowMenu = createShortUrlsRowMenu(
|
const ShortUrlsRowMenu = createShortUrlsRowMenu(
|
||||||
DeleteShortUrlModal,
|
DeleteShortUrlModal,
|
||||||
EditTagsModal,
|
EditTagsModal,
|
||||||
EditMetaModal,
|
EditMetaModal,
|
||||||
EditShortUrlModal,
|
EditShortUrlModal,
|
||||||
() => '',
|
() => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper = shallow(
|
wrapper = shallow(<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />);
|
||||||
<ShortUrlsRowMenu
|
|
||||||
selectedServer={selectedServer}
|
|
||||||
shortUrl={shortUrl}
|
|
||||||
onCopyToClipboard={onCopyToClipboard}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('renders modal windows', () => {
|
it('renders modal windows', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
@ -62,8 +58,8 @@ describe('<ShortUrlsRowMenu />', () => {
|
||||||
expect(items.find('[divider]')).toHaveLength(1);
|
expect(items.find('[divider]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toggles state when toggling modal windows', () => {
|
describe('toggles state when toggling modals or the dropdown', () => {
|
||||||
const assert = (modalComponent) => {
|
const assert = (modalComponent: Function) => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
|
||||||
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false);
|
expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false);
|
||||||
|
@ -76,13 +72,6 @@ describe('<ShortUrlsRowMenu />', () => {
|
||||||
it('PreviewModal', () => assert(PreviewModal));
|
it('PreviewModal', () => assert(PreviewModal));
|
||||||
it('QrCodeModal', () => assert(QrCodeModal));
|
it('QrCodeModal', () => assert(QrCodeModal));
|
||||||
it('EditShortUrlModal', () => assert(EditShortUrlModal));
|
it('EditShortUrlModal', () => assert(EditShortUrlModal));
|
||||||
});
|
it('EditShortUrlModal', () => assert(ButtonDropdown));
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,21 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import VisitStatsLink from '../../../src/short-urls/helpers/VisitStatsLink';
|
import VisitStatsLink from '../../../src/short-urls/helpers/VisitStatsLink';
|
||||||
|
import { NotFoundServer, ReachableServer } from '../../../src/servers/data';
|
||||||
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<VisitStatsLink />', () => {
|
describe('<VisitStatsLink />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[ undefined, undefined ],
|
[ undefined, undefined ],
|
||||||
[ null, null ],
|
[ null, null ],
|
||||||
[{}, null ],
|
[ Mock.of<ReachableServer>({ id: '1' }), null ],
|
||||||
[{}, undefined ],
|
[ Mock.of<ReachableServer>({ id: '1' }), undefined ],
|
||||||
[ null, {}],
|
[ Mock.of<NotFoundServer>(), Mock.all<ShortUrl>() ],
|
||||||
[ undefined, {}],
|
[ null, Mock.all<ShortUrl>() ],
|
||||||
])('only renders a plan span when either server or short URL are not set', (selectedServer, 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>);
|
wrapper = shallow(<VisitStatsLink selectedServer={selectedServer} shortUrl={shortUrl}>Something</VisitStatsLink>);
|
||||||
const link = wrapper.find(Link);
|
const link = wrapper.find(Link);
|
||||||
|
|
||||||
|
@ -24,10 +28,14 @@ describe('<VisitStatsLink />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ id: '1' }, { shortCode: 'abc123' }, '/server/1/short-code/abc123/visits' ],
|
|
||||||
[
|
[
|
||||||
{ id: '3' },
|
Mock.of<ReachableServer>({ id: '1' }),
|
||||||
{ shortCode: 'def456', domain: 'example.com' },
|
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',
|
'/server/3/short-code/def456/visits?domain=example.com',
|
||||||
],
|
],
|
||||||
])('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => {
|
])('renders link with expected query when', (selectedServer, shortUrl, expectedLink) => {
|
Loading…
Reference in a new issue