mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Added new component for QR codes error correction when consuming Shlink 2.8
This commit is contained in:
parent
0ecb771b23
commit
461c0e0bc9
4 changed files with 70 additions and 40 deletions
|
@ -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<Versions>) => (
|
||||
const QrCodeModal = (imageDownloader: ImageDownloader, ForServerVersion: FC<Versions>) => ( // eslint-disable-line
|
||||
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps,
|
||||
) => {
|
||||
const [ size, setSize ] = useState(300);
|
||||
const [ margin, setMargin ] = useState(0);
|
||||
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
||||
const [ errorCorrection, setErrorCorrection ] = useState<QrErrorCorrection>('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<Vers
|
|||
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Row className="mb-2">
|
||||
<div
|
||||
<Row>
|
||||
<FormGroup
|
||||
className={classNames({
|
||||
'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported,
|
||||
'col-md-6': (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
|
||||
'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported,
|
||||
'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
|
||||
'col-md-6': capabilities.errorCorrectionIsSupported || (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
|
||||
'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported,
|
||||
})}
|
||||
>
|
||||
<FormGroup>
|
||||
<label className="mb-0">Size: {size}px</label>
|
||||
<label className="mb-0">Size: {size}px</label>
|
||||
<input
|
||||
type="range"
|
||||
className="form-control-range"
|
||||
value={size}
|
||||
step={10}
|
||||
min={50}
|
||||
max={1000}
|
||||
onChange={(e) => setSize(Number(e.target.value))}
|
||||
/>
|
||||
</FormGroup>
|
||||
{capabilities.marginIsSupported && (
|
||||
<FormGroup className={capabilities.svgIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||
<label className="mb-0">Margin: {margin}px</label>
|
||||
<input
|
||||
type="range"
|
||||
className="form-control-range"
|
||||
value={size}
|
||||
step={10}
|
||||
min={50}
|
||||
max={1000}
|
||||
onChange={(e) => setSize(Number(e.target.value))}
|
||||
value={margin}
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) => setMargin(Number(e.target.value))}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
{capabilities.marginIsSupported && (
|
||||
<div className={capabilities.svgIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||
<FormGroup>
|
||||
<label className="mb-0">Margin: {margin}px</label>
|
||||
<input
|
||||
type="range"
|
||||
className="form-control-range"
|
||||
value={margin}
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) => setMargin(Number(e.target.value))}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
{capabilities.svgIsSupported && (
|
||||
<div className={capabilities.marginIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||
<FormGroup className={capabilities.marginIsSupported && !capabilities.errorCorrectionIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||
<DropdownBtn text={`Format (${format})`}>
|
||||
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
|
||||
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
|
||||
</DropdownBtn>
|
||||
</div>
|
||||
</FormGroup>
|
||||
)}
|
||||
{capabilities.errorCorrectionIsSupported && (
|
||||
<FormGroup className="col-md-6">
|
||||
<DropdownBtn text={`Error correction (${errorCorrection})`}>
|
||||
<DropdownItem active={errorCorrection === 'L'} onClick={() => setErrorCorrection('L')}>
|
||||
<b>L</b>ow
|
||||
</DropdownItem>
|
||||
<DropdownItem active={errorCorrection === 'M'} onClick={() => setErrorCorrection('M')}>
|
||||
<b>M</b>edium
|
||||
</DropdownItem>
|
||||
<DropdownItem active={errorCorrection === 'Q'} onClick={() => setErrorCorrection('Q')}>
|
||||
<b>Q</b>uartile
|
||||
</DropdownItem>
|
||||
<DropdownItem active={errorCorrection === 'H'} onClick={() => setErrorCorrection('H')}>
|
||||
<b>H</b>igh
|
||||
</DropdownItem>
|
||||
</DropdownBtn>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Row>
|
||||
<div className="text-center">
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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}`}`;
|
||||
|
|
|
@ -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('<QrCodeModal />', () => {
|
|||
[ '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('<QrCodeModal />', () => {
|
|||
[ '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);
|
||||
|
|
Loading…
Reference in a new issue