From be085f50e033ede162dddab719ad382c1bf4c51e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Feb 2021 09:29:20 +0100 Subject: [PATCH 1/5] Updated to bootstrap 4.6 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85b61b90..8106a4dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8056,9 +8056,9 @@ "dev": true }, "bootstrap": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz", - "integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", + "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==" }, "bottlejs": { "version": "2.0.0", diff --git a/package.json b/package.json index 223ed929..2f4a282a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/react-fontawesome": "^0.1.12", "axios": "^0.21.0", - "bootstrap": "^4.5.3", + "bootstrap": "^4.6.0", "bottlejs": "^2.0.0", "bowser": "^2.11.0", "chart.js": "^2.9.4", From f9da22c5a1c989bf09484da9b86549b407ed5446 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Feb 2021 09:50:26 +0100 Subject: [PATCH 2/5] Added support for margin param in buildQrCodeUrl function --- src/short-urls/helpers/QrCodeModal.tsx | 4 +- src/utils/helpers/qrCodes.ts | 30 +++++++----- test/utils/helpers/qrCodes.test.ts | 65 ++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index 8f968c87..dc601d84 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -15,13 +15,15 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps { const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => { const [ size, setSize ] = useState(300); + const [ margin ] = useState(0); 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' }), + marginIsSupported: versionMatch(selectedServer.version, { minVersion: '2.6.0' }), }), [ selectedServer ]); const qrCodeUrl = useMemo( - () => buildQrCodeUrl(shortUrl, size, format, capabilities), + () => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities), [ shortUrl, size, format, capabilities ], ); const modalSize = useMemo(() => { diff --git a/src/utils/helpers/qrCodes.ts b/src/utils/helpers/qrCodes.ts index 13b298ca..707eeb2f 100644 --- a/src/utils/helpers/qrCodes.ts +++ b/src/utils/helpers/qrCodes.ts @@ -1,25 +1,31 @@ -import { always, cond } from 'ramda'; +import { isEmpty } from 'ramda'; +import { stringifyQuery } from './query'; export interface QrCodeCapabilities { useSizeInPath: boolean; svgIsSupported: boolean; + marginIsSupported: boolean; } export type QrCodeFormat = 'svg' | 'png'; +export interface QrCodeOptions { + size: number; + format: QrCodeFormat; + margin: number; +} + export const buildQrCodeUrl = ( shortUrl: string, - size: number, - format: QrCodeFormat, - { useSizeInPath, svgIsSupported }: QrCodeCapabilities, + { size, format, margin }: QrCodeOptions, + { useSizeInPath, svgIsSupported, marginIsSupported }: QrCodeCapabilities, ): string => { - const sizeFragment = useSizeInPath ? `/${size}` : `?size=${size}`; - const formatFragment = !svgIsSupported ? '' : `format=${format}`; - const joinSymbolResolver = cond([ - [ () => useSizeInPath && svgIsSupported, always('?') ], - [ () => !useSizeInPath && svgIsSupported, always('&') ], - ]); - const joinSymbol = joinSymbolResolver() ?? ''; + const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`; + const query = stringifyQuery({ + size: useSizeInPath ? undefined : size, + format: svgIsSupported ? format : undefined, + margin: marginIsSupported ? margin : undefined, + }); - return `${shortUrl}/qr-code${sizeFragment}${joinSymbol}${formatFragment}`; + return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`; }; diff --git a/test/utils/helpers/qrCodes.test.ts b/test/utils/helpers/qrCodes.test.ts index fe0a0f76..e432f855 100644 --- a/test/utils/helpers/qrCodes.test.ts +++ b/test/utils/helpers/qrCodes.test.ts @@ -3,15 +3,62 @@ import { buildQrCodeUrl, QrCodeFormat } from '../../../src/utils/helpers/qrCodes describe('qrCodes', () => { describe('buildQrCodeUrl', () => { test.each([ - [ 'foo.com', 530, 'svg', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=svg' ], - [ 'foo.com', 530, 'png', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=png' ], - [ 'bar.io', 870, 'svg', { useSizeInPath: false, svgIsSupported: false }, 'bar.io/qr-code?size=870' ], - [ 'bar.io', 200, 'png', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=png' ], - [ 'bar.io', 200, 'svg', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=svg' ], - [ 'foo.net', 480, 'png', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ], - [ 'foo.net', 480, 'svg', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ], - ])('builds expected URL based in params', (shortUrl, size, format, capabilities, expectedUrl) => { - expect(buildQrCodeUrl(shortUrl, size, format as QrCodeFormat, capabilities)).toEqual(expectedUrl); + [ + 'foo.com', + { size: 530, format: 'svg' as QrCodeFormat, margin: 0 }, + { useSizeInPath: true, svgIsSupported: true, marginIsSupported: false }, + 'foo.com/qr-code/530?format=svg', + ], + [ + 'foo.com', + { size: 530, format: 'png' as QrCodeFormat, margin: 0 }, + { useSizeInPath: true, svgIsSupported: true, marginIsSupported: false }, + 'foo.com/qr-code/530?format=png', + ], + [ + 'bar.io', + { size: 870, format: 'svg' as QrCodeFormat, margin: 0 }, + { useSizeInPath: false, svgIsSupported: false, marginIsSupported: false }, + 'bar.io/qr-code?size=870', + ], + [ + 'bar.io', + { size: 200, format: 'png' as QrCodeFormat, margin: 0 }, + { useSizeInPath: false, svgIsSupported: true, marginIsSupported: false }, + 'bar.io/qr-code?size=200&format=png', + ], + [ + 'bar.io', + { size: 200, format: 'svg' as QrCodeFormat, margin: 0 }, + { useSizeInPath: false, svgIsSupported: true, marginIsSupported: false }, + 'bar.io/qr-code?size=200&format=svg', + ], + [ + 'foo.net', + { size: 480, format: 'png' as QrCodeFormat, margin: 0 }, + { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false }, + 'foo.net/qr-code/480', + ], + [ + 'foo.net', + { size: 480, format: 'svg' as QrCodeFormat, margin: 0 }, + { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false }, + 'foo.net/qr-code/480', + ], + [ + 'shlink.io', + { size: 123, format: 'svg' as QrCodeFormat, margin: 10 }, + { useSizeInPath: true, svgIsSupported: false, marginIsSupported: false }, + 'shlink.io/qr-code/123', + ], + [ + 'shlink.io', + { size: 456, format: 'png' as QrCodeFormat, margin: 10 }, + { useSizeInPath: true, svgIsSupported: true, marginIsSupported: true }, + 'shlink.io/qr-code/456?format=png&margin=10', + ], + ])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => { + expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl); }); }); }); From c95cb144a84505ca2027c07e76737dc74561ff5f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Feb 2021 10:16:30 +0100 Subject: [PATCH 3/5] Added margin option to QR code component --- src/short-urls/helpers/QrCodeModal.tsx | 38 +++++++++++++++---- src/utils/helpers/qrCodes.ts | 2 +- test/short-urls/helpers/QrCodeModal.test.tsx | 39 ++++++++++++++------ test/utils/helpers/qrCodes.test.ts | 6 +++ 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index dc601d84..e26d289a 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; import { ExternalLink } from 'react-external-link'; +import classNames from 'classnames'; import { ShortUrlModalProps } from '../data'; import { ReachableServer } from '../../servers/data'; import { versionMatch } from '../../utils/helpers/version'; @@ -15,7 +16,7 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps { const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => { const [ size, setSize ] = useState(300); - const [ margin ] = useState(0); + const [ margin, setMargin ] = useState(0); const [ format, setFormat ] = useState('png'); const capabilities: QrCodeCapabilities = useMemo(() => ({ useSizeInPath: !versionMatch(selectedServer.version, { minVersion: '2.5.0' }), @@ -24,15 +25,16 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: }), [ selectedServer ]); const qrCodeUrl = useMemo( () => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities), - [ shortUrl, size, format, capabilities ], + [ shortUrl, size, format, margin, capabilities ], ); + const totalSize = useMemo(() => size + margin, [ size, margin ]); const modalSize = useMemo(() => { - if (size < 500) { + if (totalSize < 500) { return undefined; } - return size < 800 ? 'lg' : 'xl'; - }, [ size ]); + return totalSize < 800 ? 'lg' : 'xl'; + }, [ totalSize ]); return ( @@ -41,7 +43,13 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: -
+
+ {capabilities.marginIsSupported && ( +
+ + + setMargin(Number(e.target.value))} + /> + +
+ )} {capabilities.svgIsSupported && ( -
+
setFormat('png')}>PNG setFormat('svg')}>SVG diff --git a/src/utils/helpers/qrCodes.ts b/src/utils/helpers/qrCodes.ts index 707eeb2f..60342bd4 100644 --- a/src/utils/helpers/qrCodes.ts +++ b/src/utils/helpers/qrCodes.ts @@ -24,7 +24,7 @@ export const buildQrCodeUrl = ( const query = stringifyQuery({ size: useSizeInPath ? undefined : size, format: svgIsSupported ? format : undefined, - margin: marginIsSupported ? margin : undefined, + margin: marginIsSupported && margin > 0 ? margin : 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 924c8cb3..d84b5572 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -11,7 +11,7 @@ import { DropdownBtn } from '../../../src/utils/DropdownBtn'; describe('', () => { let wrapper: ShallowWrapper; const shortUrl = 'https://doma.in/abc123'; - const createWrapper = (version = '2.5.0') => { + const createWrapper = (version = '2.6.0') => { const selectedServer = Mock.of({ version }); wrapper = shallow( @@ -37,11 +37,20 @@ describe('', () => { }); 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) => { + [ '2.3.0', 0, '/qr-code/300' ], + [ '2.4.0', 0, '/qr-code/300?format=png' ], + [ '2.4.0', 10, '/qr-code/300?format=png' ], + [ '2.5.0', 0, '/qr-code?size=300&format=png' ], + [ '2.6.0', 0, '/qr-code?size=300&format=png' ], + [ '2.6.0', 10, '/qr-code?size=300&format=png&margin=10' ], + ])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => { const wrapper = createWrapper(version); + const formControls = wrapper.find('.form-control-range'); + + if (formControls.length > 1) { + formControls.at(1).simulate('change', { target: { value: `${margin}` } }); + } + const modalBody = wrapper.find(ModalBody); const img = modalBody.find('img'); const linkInBody = modalBody.find(ExternalLink); @@ -53,23 +62,31 @@ describe('', () => { }); it.each([ - [ 530, 'lg' ], - [ 200, undefined ], - [ 830, 'xl' ], - ])('renders expected size', (size, modalSize) => { + [ 530, 0, 'lg' ], + [ 200, 0, undefined ], + [ 830, 0, 'xl' ], + [ 430, 80, 'lg' ], + [ 200, 50, undefined ], + [ 720, 100, 'xl' ], + ])('renders expected size', (size, margin, modalSize) => { const wrapper = createWrapper(); - const sizeInput = wrapper.find('.form-control-range'); + const formControls = wrapper.find('.form-control-range'); + const sizeInput = formControls.at(0); + const marginInput = formControls.at(1); sizeInput.simulate('change', { target: { value: `${size}` } }); + marginInput.simulate('change', { target: { value: `${margin}` } }); expect(wrapper.find('.mt-2').text()).toEqual(`${size}x${size}`); - expect(wrapper.find('label').text()).toEqual(`Size: ${size}px`); + expect(wrapper.find('label').at(0).text()).toEqual(`Size: ${size}px`); + expect(wrapper.find('label').at(1).text()).toEqual(`Margin: ${margin}px`); expect(wrapper.find(Modal).prop('size')).toEqual(modalSize); }); it.each([ [ '2.3.0', 0, 'col-12' ], [ '2.4.0', 1, 'col-md-6' ], + [ '2.6.0', 1, 'col-md-4' ], ])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => { const wrapper = createWrapper(version); const dropdown = wrapper.find(DropdownBtn); diff --git a/test/utils/helpers/qrCodes.test.ts b/test/utils/helpers/qrCodes.test.ts index e432f855..12f1afdd 100644 --- a/test/utils/helpers/qrCodes.test.ts +++ b/test/utils/helpers/qrCodes.test.ts @@ -57,6 +57,12 @@ describe('qrCodes', () => { { useSizeInPath: true, svgIsSupported: true, marginIsSupported: true }, 'shlink.io/qr-code/456?format=png&margin=10', ], + [ + 'shlink.io', + { size: 456, format: 'png' as QrCodeFormat, margin: 0 }, + { useSizeInPath: true, svgIsSupported: true, marginIsSupported: true }, + 'shlink.io/qr-code/456?format=png', + ], ])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => { expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl); }); From f8edcda665388c5921e5bbb33dd0e92f6fd8a6d6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Feb 2021 10:17:34 +0100 Subject: [PATCH 4/5] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff5b77a..448a383b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added -* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) Improved QR code modal, including controls to customize size and format, as well as a button to copy the link to the clipboard. +* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard. ### Changed * *Nothing* From 8a2e39a935e6db2feb315be7eb4abdbb813711ce Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 14 Feb 2021 10:21:10 +0100 Subject: [PATCH 5/5] Added subtle shadow in QR code image, so that it's easier to notice the margin --- src/short-urls/helpers/QrCodeModal.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/short-urls/helpers/QrCodeModal.scss b/src/short-urls/helpers/QrCodeModal.scss index 40602269..40b9ca40 100644 --- a/src/short-urls/helpers/QrCodeModal.scss +++ b/src/short-urls/helpers/QrCodeModal.scss @@ -1,3 +1,4 @@ .qr-code-modal__img { max-width: 100%; + box-shadow: 0 0 .25rem rgb(0 0 0 / .2); }