From 461c0e0bc9fcc66965dd09a89989d947a7f3d515 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 16 Aug 2021 17:13:31 +0200 Subject: [PATCH] Added new component for QR codes error correction when consuming Shlink 2.8 --- src/short-urls/helpers/QrCodeModal.tsx | 93 ++++++++++++-------- src/utils/helpers/features.ts | 2 + src/utils/helpers/qrCodes.ts | 9 +- test/short-urls/helpers/QrCodeModal.test.tsx | 6 +- 4 files changed, 70 insertions(+), 40 deletions(-) diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index 9b43b58c..fd20672a 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -8,8 +8,13 @@ import { ShortUrlModalProps } from '../data'; import { SelectedServer } from '../../servers/data'; import { DropdownBtn } from '../../utils/DropdownBtn'; import { CopyToClipboardIcon } from '../../utils/CopyToClipboardIcon'; -import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat } from '../../utils/helpers/qrCodes'; -import { supportsQrCodeSizeInQuery, supportsQrCodeSvgFormat, supportsQrCodeMargin } from '../../utils/helpers/features'; +import { buildQrCodeUrl, QrCodeCapabilities, QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCodes'; +import { + supportsQrCodeSizeInQuery, + supportsQrCodeSvgFormat, + supportsQrCodeMargin, + supportsQrErrorCorrection, +} from '../../utils/helpers/features'; import { ImageDownloader } from '../../common/services/ImageDownloader'; import { Versions } from '../../utils/helpers/version'; import './QrCodeModal.scss'; @@ -18,20 +23,22 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps { selectedServer: SelectedServer; } -const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC) => ( +const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC) => ( // eslint-disable-line { shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps, ) => { const [ size, setSize ] = useState(300); const [ margin, setMargin ] = useState(0); const [ format, setFormat ] = useState('png'); + const [ errorCorrection, setErrorCorrection ] = useState('L'); const capabilities: QrCodeCapabilities = useMemo(() => ({ useSizeInPath: !supportsQrCodeSizeInQuery(selectedServer), svgIsSupported: supportsQrCodeSvgFormat(selectedServer), marginIsSupported: supportsQrCodeMargin(selectedServer), - }), [ selectedServer ]); + errorCorrectionIsSupported: supportsQrErrorCorrection(selectedServer), + }) as QrCodeCapabilities, [ selectedServer ]); const qrCodeUrl = useMemo( - () => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities), - [ shortUrl, size, format, margin, capabilities ], + () => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }, capabilities), + [ shortUrl, size, format, margin, errorCorrection, capabilities ], ); const totalSize = useMemo(() => size + margin, [ size, margin ]); const modalSize = useMemo(() => { @@ -48,50 +55,64 @@ const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC{shortUrl} - -
+ - - + + setSize(Number(e.target.value))} + /> + + {capabilities.marginIsSupported && ( + + setSize(Number(e.target.value))} + value={margin} + step={1} + min={0} + max={100} + onChange={(e) => setMargin(Number(e.target.value))} /> -
- {capabilities.marginIsSupported && ( -
- - - setMargin(Number(e.target.value))} - /> - -
)} {capabilities.svgIsSupported && ( -
+ setFormat('png')}>PNG setFormat('svg')}>SVG -
+ + )} + {capabilities.errorCorrectionIsSupported && ( + + + setErrorCorrection('L')}> + Low + + setErrorCorrection('M')}> + Medium + + setErrorCorrection('Q')}> + Quartile + + setErrorCorrection('H')}> + High + + + )}
diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts index 9eb314f2..609c9e95 100644 --- a/src/utils/helpers/features.ts +++ b/src/utils/helpers/features.ts @@ -27,3 +27,5 @@ export const supportsTagsInPatch = supportsShortUrlTitle; export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' }); export const supportsCrawlableVisits = supportsBotVisits; + +export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2.8.0' }); diff --git a/src/utils/helpers/qrCodes.ts b/src/utils/helpers/qrCodes.ts index 60342bd4..fb94350a 100644 --- a/src/utils/helpers/qrCodes.ts +++ b/src/utils/helpers/qrCodes.ts @@ -5,26 +5,31 @@ export interface QrCodeCapabilities { useSizeInPath: boolean; svgIsSupported: boolean; marginIsSupported: boolean; + errorCorrectionIsSupported: boolean; } export type QrCodeFormat = 'svg' | 'png'; +export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H'; + export interface QrCodeOptions { size: number; format: QrCodeFormat; margin: number; + errorCorrection: QrErrorCorrection; } export const buildQrCodeUrl = ( shortUrl: string, - { size, format, margin }: QrCodeOptions, - { useSizeInPath, svgIsSupported, marginIsSupported }: QrCodeCapabilities, + { size, format, margin, errorCorrection }: QrCodeOptions, + { useSizeInPath, svgIsSupported, marginIsSupported, errorCorrectionIsSupported }: QrCodeCapabilities, ): string => { const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`; const query = stringifyQuery({ size: useSizeInPath ? undefined : size, format: svgIsSupported ? format : undefined, margin: marginIsSupported && margin > 0 ? margin : undefined, + errorCorrection: errorCorrectionIsSupported ? errorCorrection : undefined, }); return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`; diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx index 9cdf1bb7..2daeafce 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -1,6 +1,6 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { ExternalLink } from 'react-external-link'; -import { Button, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; +import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; import { Mock } from 'ts-mockery'; import createQrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; import { ShortUrl } from '../../../src/short-urls/data'; @@ -48,6 +48,7 @@ describe('', () => { [ '2.5.0' as SemVer, 0, '/qr-code?size=300&format=png' ], [ '2.6.0' as SemVer, 0, '/qr-code?size=300&format=png' ], [ '2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10' ], + [ '2.8.0' as SemVer, 0, '/qr-code?size=300&format=png&errorCorrection=L' ], ])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => { const wrapper = createWrapper(version); const formControls = wrapper.find('.form-control-range'); @@ -91,10 +92,11 @@ describe('', () => { [ '2.3.0' as SemVer, 0, 'col-12' ], [ '2.4.0' as SemVer, 1, 'col-md-6' ], [ '2.6.0' as SemVer, 1, 'col-md-4' ], + [ '2.8.0' as SemVer, 2, '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(); + const firstCol = wrapper.find(Row).find(FormGroup).first(); expect(dropdown).toHaveLength(expectedAmountOfDropdowns); expect(firstCol.prop('className')).toEqual(expectedRangeClass);