diff --git a/src/servers/services/ServersImporter.ts b/src/servers/services/ServersImporter.ts index 03c1ca25..7dcf8175 100644 --- a/src/servers/services/ServersImporter.ts +++ b/src/servers/services/ServersImporter.ts @@ -1,7 +1,6 @@ import { CsvJson } from 'csvjson'; import { ServerData } from '../data'; - interface CsvFile extends File { type: 'text/csv' | 'text/comma-separated-values' | 'application/csv'; } diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index c1906db2..8f968c87 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -1,36 +1,28 @@ import { useMemo, useState } from 'react'; import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; import { ExternalLink } from 'react-external-link'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ShortUrlModalProps } from '../data'; import { ReachableServer } from '../../servers/data'; import { versionMatch } from '../../utils/helpers/version'; import { DropdownBtn } from '../../utils/DropdownBtn'; +import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; +import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes'; import './QrCodeModal.scss'; interface QrCodeModalConnectProps extends ShortUrlModalProps { selectedServer: ReachableServer; } -type QrCodeFormat = 'svg' | 'png'; - -const buildQrCodeUrl = (shortUrl: string, size: number, format: QrCodeFormat, version: string): string => { - const useSizeInPath = !versionMatch(version, { minVersion: '2.5.0' }); - const svgIsSupported = versionMatch(version, { minVersion: '2.4.0' }); - const sizeFragment = useSizeInPath ? `/${size}?` : `?size=${size}&`; - const formatFragment = !svgIsSupported ? '' : `format=${format}`; - - return `${shortUrl}/qr-code${sizeFragment}${formatFragment}`; -}; - const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => { const [ size, setSize ] = useState(300); const [ format, setFormat ] = useState('png'); + const capabilities: QrCodeCapabilities = useMemo(() => ({ + useSizeInPath: !versionMatch(selectedServer.version, { minVersion: '2.5.0' }), + svgIsSupported: versionMatch(selectedServer.version, { minVersion: '2.4.0' }), + }), [ selectedServer ]); const qrCodeUrl = useMemo( - () => buildQrCodeUrl(shortUrl, size, format, selectedServer.version), - [ shortUrl, size, format, selectedServer ], + () => buildQrCodeUrl(shortUrl, size, format, capabilities), + [ shortUrl, size, format, capabilities ], ); const modalSize = useMemo(() => { if (size < 500) { @@ -47,7 +39,7 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: -
+
-
- - setFormat('png')}>PNG - setFormat('svg')}>SVG - -
+ {capabilities.svgIsSupported && ( +
+ + setFormat('png')}>PNG + setFormat('svg')}>SVG + +
+ )}
QR code URL:
- - - +
QR code
{size}x{size}
diff --git a/src/short-urls/helpers/ShortUrlsRow.scss b/src/short-urls/helpers/ShortUrlsRow.scss index 6cf6ee42..89255646 100644 --- a/src/short-urls/helpers/ShortUrlsRow.scss +++ b/src/short-urls/helpers/ShortUrlsRow.scss @@ -48,11 +48,6 @@ transform: scale(1.5); } -.short-urls-row__copy-btn { - cursor: pointer; - font-size: 1.2rem; -} - .short-urls-row__copy-hint { @include vertical-align(translateX(10px)); diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 1399099b..5551938c 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -2,13 +2,11 @@ import { isEmpty } from 'ramda'; import { 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 ColorGenerator from '../../utils/services/ColorGenerator'; import { StateFlagTimeout } from '../../utils/helpers/hooks'; import Tag from '../../tags/helpers/Tag'; import { SelectedServer } from '../../servers/data'; +import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; import { ShortUrl } from '../data'; import ShortUrlVisitsCount from './ShortUrlVisitsCount'; import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu'; @@ -60,9 +58,7 @@ const ShortUrlsRow = ( - - - + diff --git a/src/utils/CopyToClipboardIcon.scss b/src/utils/CopyToClipboardIcon.scss new file mode 100644 index 00000000..a9e958e9 --- /dev/null +++ b/src/utils/CopyToClipboardIcon.scss @@ -0,0 +1,4 @@ +.copy-to-clipboard-icon { + cursor: pointer; + font-size: 1.2rem; +} diff --git a/src/utils/CopyToClipboardIcon.tsx b/src/utils/CopyToClipboardIcon.tsx new file mode 100644 index 00000000..46d12a8c --- /dev/null +++ b/src/utils/CopyToClipboardIcon.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import './CopyToClipboardIcon.scss'; + +interface CopyToClipboardIconProps { + text: string; + onCopy?: (text: string, result: boolean) => void; +} + +export const CopyToClipboardIcon: FC = ({ text, onCopy }) => ( + + + +); diff --git a/src/utils/helpers/qrCodes.ts b/src/utils/helpers/qrCodes.ts new file mode 100644 index 00000000..06e81b4d --- /dev/null +++ b/src/utils/helpers/qrCodes.ts @@ -0,0 +1,19 @@ +export interface QrCodeCapabilities { + useSizeInPath: boolean; + svgIsSupported: boolean; +} + +export type QrCodeFormat = 'svg' | 'png'; + +export const buildQrCodeUrl = ( + shortUrl: string, + size: number, + format: QrCodeFormat, + { useSizeInPath, svgIsSupported }: QrCodeCapabilities, +): string => { + const sizeFragment = useSizeInPath ? `/${size}` : `?size=${size}`; + const formatFragment = !svgIsSupported ? '' : `format=${format}`; + const joinSymbol = useSizeInPath && svgIsSupported ? '?' : !useSizeInPath && svgIsSupported ? '&' : ''; + + return `${shortUrl}/qr-code${sizeFragment}${joinSymbol}${formatFragment}`; +}; diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx index 2372f033..924c8cb3 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -1,10 +1,12 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { ExternalLink } from 'react-external-link'; -import { ModalHeader } from 'reactstrap'; +import { Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; import { Mock } from 'ts-mockery'; import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; import { ShortUrl } from '../../../src/short-urls/data'; import { ReachableServer } from '../../../src/servers/data'; +import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; +import { DropdownBtn } from '../../../src/utils/DropdownBtn'; describe('', () => { let wrapper: ShallowWrapper; @@ -34,11 +36,46 @@ describe('', () => { expect(externalLink.prop('href')).toEqual(shortUrl); }); - it('displays an image with the QR code of the URL', () => { - const wrapper = createWrapper(); - const img = wrapper.find('img'); + it.each([ + [ '2.3.0', '/qr-code/300' ], + [ '2.4.0', '/qr-code/300?format=png' ], + [ '2.5.0', '/qr-code?size=300&format=png' ], + ])('displays an image with the QR code of the URL', (version, expectedUrl) => { + const wrapper = createWrapper(version); + const modalBody = wrapper.find(ModalBody); + const img = modalBody.find('img'); + const linkInBody = modalBody.find(ExternalLink); + const copyToClipboard = modalBody.find(CopyToClipboardIcon); - expect(img).toHaveLength(1); - expect(img.prop('src')).toEqual(`${shortUrl}/qr-code`); + expect(img.prop('src')).toEqual(`${shortUrl}${expectedUrl}`); + expect(linkInBody.prop('href')).toEqual(`${shortUrl}${expectedUrl}`); + expect(copyToClipboard.prop('text')).toEqual(`${shortUrl}${expectedUrl}`); + }); + + it.each([ + [ 530, 'lg' ], + [ 200, undefined ], + [ 830, 'xl' ], + ])('renders expected size', (size, modalSize) => { + const wrapper = createWrapper(); + const sizeInput = wrapper.find('.form-control-range'); + + sizeInput.simulate('change', { target: { value: `${size}` } }); + + expect(wrapper.find('.mt-2').text()).toEqual(`${size}x${size}`); + expect(wrapper.find('label').text()).toEqual(`Size: ${size}px`); + expect(wrapper.find(Modal).prop('size')).toEqual(modalSize); + }); + + it.each([ + [ '2.3.0', 0, 'col-12' ], + [ '2.4.0', 1, 'col-md-6' ], + ])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => { + const wrapper = createWrapper(version); + const dropdown = wrapper.find(DropdownBtn); + const firstCol = wrapper.find(Row).find('div').first(); + + expect(dropdown).toHaveLength(expectedAmountOfDropdowns); + expect(firstCol.prop('className')).toEqual(expectedRangeClass); }); }); diff --git a/test/short-urls/helpers/ShortUrlsRow.test.tsx b/test/short-urls/helpers/ShortUrlsRow.test.tsx index c4f565a9..e5429a7c 100644 --- a/test/short-urls/helpers/ShortUrlsRow.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx @@ -4,13 +4,13 @@ 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 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'; +import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; describe('', () => { let wrapper: ShallowWrapper; @@ -98,7 +98,7 @@ describe('', () => { it('updates state when copied to clipboard', () => { const col = wrapper.find('td').at(1); - const menu = col.find(CopyToClipboard); + const menu = col.find(CopyToClipboardIcon); expect(menu).toHaveLength(1); expect(stateFlagTimeout).not.toHaveBeenCalled(); diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx index 66af635b..3e0cfd5d 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx @@ -2,7 +2,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { ButtonDropdown, DropdownItem } from 'reactstrap'; import { Mock } from 'ts-mockery'; import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; -import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; import { ReachableServer } from '../../../src/servers/data'; import { ShortUrl } from '../../../src/short-urls/data'; @@ -12,6 +11,7 @@ describe('', () => { const EditTagsModal = () => null; const EditMetaModal = () => null; const EditShortUrlModal = () => null; + const QrCodeModal = () => null; const selectedServer = Mock.of({ id: 'abc123' }); const shortUrl = Mock.of({ shortCode: 'abc123', @@ -23,6 +23,7 @@ describe('', () => { EditTagsModal, EditMetaModal, EditShortUrlModal, + QrCodeModal, () => null, );