From 0197e9ab0b9690a4c4ac1a61d4e9d965b7829374 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 2 Oct 2024 08:49:25 +0200 Subject: [PATCH 01/10] Make QR codes start with default options from server --- .../helpers/QrCodeDimensionControl.tsx | 65 ++++++++++++++ src/short-urls/helpers/QrCodeModal.scss | 4 - src/short-urls/helpers/QrCodeModal.tsx | 90 ++++++++++--------- .../qr-codes/QrErrorCorrectionDropdown.tsx | 8 +- .../helpers/qr-codes/QrFormatDropdown.tsx | 8 +- src/utils/helpers/qrCodes.ts | 10 +-- 6 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 src/short-urls/helpers/QrCodeDimensionControl.tsx delete mode 100644 src/short-urls/helpers/QrCodeModal.scss diff --git a/src/short-urls/helpers/QrCodeDimensionControl.tsx b/src/short-urls/helpers/QrCodeDimensionControl.tsx new file mode 100644 index 00000000..92efc92e --- /dev/null +++ b/src/short-urls/helpers/QrCodeDimensionControl.tsx @@ -0,0 +1,65 @@ +import { faArrowRotateLeft } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import type { FC } from 'react'; +import { useId } from 'react'; +import { Button, FormGroup } from 'reactstrap'; + +export type QrCodeDimensionControlProps = { + name: string; + value?: number; + step: number; + min: number; + max: number; + initial?: number; + onChange: (newValue?: number) => void; + className?: string; +}; + +export const QrCodeDimensionControl: FC = ( + { name, value, step, min, max, onChange, className, initial = min }, +) => { + const id = useId(); + + return ( + + {value === undefined && ( + + )} + {value !== undefined && ( +
+
+ + onChange(Number(e.target.value))} + /> +
+ +
+ )} +
+ ); +}; diff --git a/src/short-urls/helpers/QrCodeModal.scss b/src/short-urls/helpers/QrCodeModal.scss deleted file mode 100644 index 40b9ca40..00000000 --- a/src/short-urls/helpers/QrCodeModal.scss +++ /dev/null @@ -1,4 +0,0 @@ -.qr-code-modal__img { - max-width: 100%; - box-shadow: 0 0 .25rem rgb(0 0 0 / .2); -} diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index a44f8311..53a59fea 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -12,7 +12,7 @@ import type { ImageDownloader } from '../../utils/services/ImageDownloader'; import type { ShortUrlModalProps } from '../data'; import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown'; import { QrFormatDropdown } from './qr-codes/QrFormatDropdown'; -import './QrCodeModal.scss'; +import { QrCodeDimensionControl } from './QrCodeDimensionControl'; type QrCodeModalDeps = { ImageDownloader: ImageDownloader @@ -22,22 +22,15 @@ const QrCodeModal: FCWithDeps = ( { shortUrl: { shortUrl, shortCode }, toggle, isOpen }, ) => { const { ImageDownloader: imageDownloader } = useDependencies(QrCodeModal); - const [size, setSize] = useState(300); - const [margin, setMargin] = useState(0); - const [format, setFormat] = useState('png'); - const [errorCorrection, setErrorCorrection] = useState('L'); + const [size, setSize] = useState(); + const [margin, setMargin] = useState(); + const [format, setFormat] = useState(); + const [errorCorrection, setErrorCorrection] = useState(); const qrCodeUrl = useMemo( () => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }), [shortUrl, size, format, margin, errorCorrection], ); - const totalSize = useMemo(() => size + margin, [size, margin]); - const modalSize = useMemo(() => { - if (totalSize < 500) { - return undefined; - } - - return totalSize < 800 ? 'lg' : 'xl'; - }, [totalSize]); + const [modalSize, setModalSize] = useState<'lg' | 'xl'>(); return ( @@ -46,36 +39,29 @@ const QrCodeModal: FCWithDeps = ( - - - setSize(Number(e.target.value))} - /> - - - - setMargin(Number(e.target.value))} - /> - - + + + - + @@ -84,7 +70,27 @@ const QrCodeModal: FCWithDeps = ( - QR code + { + if (!image) { + return; + } + + image.addEventListener('load', () => { + const { naturalWidth } = image; + + if (naturalWidth < 500) { + setModalSize(undefined); + } else { + setModalSize(naturalWidth < 800 ? 'lg' : 'xl'); + } + }); + }} + src={qrCodeUrl} + alt="QR code" + className="shadow-lg" + style={{ maxWidth: '100%' }} + />
)} {activeCities && ( - + @@ -49,7 +48,6 @@ export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: Op )} - Show in map ); diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx index 5bde1137..9fc822d6 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { QrCodeModalFactory } from '../../../src/short-urls/helpers/QrCodeModal'; import type { ImageDownloader } from '../../../src/utils/services/ImageDownloader'; @@ -63,8 +63,16 @@ describe('', () => { expect(screen.getByText(`size: ${size}px`)).toBeInTheDocument(); expect(screen.getByText(`margin: ${margin}px`)).toBeInTheDocument(); + + // Fake the images load event with a width that matches the size+margin + const image = screen.getByAltText('QR code'); + Object.defineProperty(image, 'naturalWidth', { + get: () => size + margin, + }); + image.dispatchEvent(new Event('load')); + if (modalSize) { - expect(screen.getByRole('document')).toHaveClass(`modal-${modalSize}`); + await waitFor(() => expect(screen.getByRole('document')).toHaveClass(`modal-${modalSize}`)); } }); diff --git a/test/visits/helpers/OpenMapModalBtn.test.tsx b/test/visits/helpers/OpenMapModalBtn.test.tsx index efa33723..abe86911 100644 --- a/test/visits/helpers/OpenMapModalBtn.test.tsx +++ b/test/visits/helpers/OpenMapModalBtn.test.tsx @@ -27,15 +27,13 @@ describe('', () => { }], ])('passes a11y checks', (setUp) => checkAccessibility(setUp())); - it('renders tooltip on button hover and opens modal on click', async () => { + it('opens modal on click', async () => { const { user } = setUp(); - expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('menu')).not.toBeInTheDocument(); await openDropdown(user); - await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument()); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); From 4e85e3cf03e0a0218759cd988d4e58bfb41c8f78 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 4 Oct 2024 15:40:42 +0200 Subject: [PATCH 09/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40bde402..79b93c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#307](https://github.com/shlinkio/shlink-web-component/issues/307) Add new setting to disable short URL deletions confirmation. * [#435](https://github.com/shlinkio/shlink-web-component/issues/435) Allow toggling between displaying raw user agent and parsed browser/OS in visits table. * [#197](https://github.com/shlinkio/shlink-web-component/issues/197) Allow line charts to be expanded to the full size of the viewport, both in individual visits views, and when comparing visits. +* [#382](https://github.com/shlinkio/shlink-web-component/issues/382) Initialize QR code modal with all params unset, so that they fall back to the server defaults. Additionally, allow them to be unset if desired. ### Changed * Update to `@shlinkio/eslint-config-js-coding-standard` 3.0, and migrate to ESLint flat config. From f59b06a062294549722bd5f0936c664b5557caf1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 7 Oct 2024 09:29:00 +0200 Subject: [PATCH 10/10] Handle QR code load event via react event callback --- src/short-urls/helpers/QrCodeModal.tsx | 29 ++++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/short-urls/helpers/QrCodeModal.tsx b/src/short-urls/helpers/QrCodeModal.tsx index 54c42006..97d6a591 100644 --- a/src/short-urls/helpers/QrCodeModal.tsx +++ b/src/short-urls/helpers/QrCodeModal.tsx @@ -1,6 +1,7 @@ import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useMemo, useState } from 'react'; +import type { SyntheticEvent } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { ExternalLink } from 'react-external-link'; import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; import type { FCWithDeps } from '../../container/utils'; @@ -31,6 +32,16 @@ const QrCodeModal: FCWithDeps = ( [shortUrl, size, format, margin, errorCorrection], ); const [modalSize, setModalSize] = useState<'lg' | 'xl'>(); + const onImageLoad = useCallback((e: SyntheticEvent) => { + const image = e.target as HTMLImageElement; + const { naturalWidth } = image; + + if (naturalWidth < 500) { + setModalSize(undefined); + } else { + setModalSize(naturalWidth < 800 ? 'lg' : 'xl'); + } + }, []); return ( @@ -71,25 +82,11 @@ const QrCodeModal: FCWithDeps = (
{ - if (!image) { - return; - } - - image.addEventListener('load', () => { - const { naturalWidth } = image; - - if (naturalWidth < 500) { - setModalSize(undefined); - } else { - setModalSize(naturalWidth < 800 ? 'lg' : 'xl'); - } - }); - }} src={qrCodeUrl} alt="QR code" className="shadow-lg" style={{ maxWidth: '100%' }} + onLoad={onImageLoad} />