mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Reduce code duplication for QR code scan (#8004)
* Add Link/Unlink Barcode action Fixes #7920 * remove unneeded imports * remove duplication * simplify * add testing * refactor type * wait for reload to add coverage * Add warning if custom barcode is used * Add Image based assign * fix action button size * fix selection to prevent wrapping * use left section for button * Refactor to seperate Input * Add comment when not scanning * Fix punctuation * factor scan area out * fix readonly arg * make BarcodeInput more generic * make button optional * reduce code duplication by using BarcodeInput * remove unneeded abstraction
This commit is contained in:
parent
313cb4758e
commit
450abcd353
59
src/frontend/src/components/items/BarcodeInput.tsx
Normal file
59
src/frontend/src/components/items/BarcodeInput.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Box, Button, Divider, TextInput } from '@mantine/core';
|
||||
import { IconQrcode } from '@tabler/icons-react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputImageBarcode } from '../../pages/Index/Scan';
|
||||
|
||||
type BarcodeInputProps = {
|
||||
onScan: (decodedText: string) => void;
|
||||
value?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onAction?: () => void;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
actionText?: string;
|
||||
};
|
||||
|
||||
export function BarcodeInput({
|
||||
onScan,
|
||||
value,
|
||||
onChange,
|
||||
onAction,
|
||||
placeholder = t`Scan barcode data here using barcode scanner`,
|
||||
label = t`Barcode`,
|
||||
actionText = t`Scan`
|
||||
}: Readonly<BarcodeInputProps>) {
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isScanning && (
|
||||
<>
|
||||
<InputImageBarcode action={onScan} />
|
||||
<Divider mt={'sm'} />
|
||||
</>
|
||||
)}
|
||||
<TextInput
|
||||
label={label}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
leftSection={
|
||||
<ActionIcon
|
||||
variant={isScanning ? 'filled' : 'subtle'}
|
||||
onClick={() => setIsScanning(!isScanning)}
|
||||
>
|
||||
<IconQrcode />
|
||||
</ActionIcon>
|
||||
}
|
||||
w="100%"
|
||||
/>
|
||||
{onAction ? (
|
||||
<Button color="green" onClick={onAction} mt="lg" fullWidth>
|
||||
{actionText}
|
||||
</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,35 +1,29 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Code,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Select,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { IconQrcode } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import QR from 'qrcode';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { set } from 'react-hook-form';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { InputImageBarcode, ScanItem } from '../../pages/Index/Scan';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { CopyButton } from '../buttons/CopyButton';
|
||||
import { QrCodeType } from './ActionDropdown';
|
||||
import { BarcodeInput } from './BarcodeInput';
|
||||
|
||||
type QRCodeProps = {
|
||||
ecl?: 'L' | 'M' | 'Q' | 'H';
|
||||
@ -162,37 +156,22 @@ export const QRCodeLink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
const actionSubmit = (data: ScanItem[]) => {
|
||||
linkBarcode(data[0].data);
|
||||
const actionSubmit = (decodedText: string) => {
|
||||
linkBarcode(decodedText);
|
||||
};
|
||||
|
||||
const handleLinkBarcode = () => {
|
||||
linkBarcode(barcode);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{isScanning ? (
|
||||
<>
|
||||
<InputImageBarcode action={actionSubmit} />
|
||||
<Divider />
|
||||
</>
|
||||
) : null}
|
||||
<TextInput
|
||||
label={t`Barcode`}
|
||||
value={barcode}
|
||||
onChange={(event) => setBarcode(event.currentTarget.value)}
|
||||
placeholder={t`Scan barcode data here using barcode scanner`}
|
||||
leftSection={
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={toggleIsScanning.toggle}
|
||||
size="input-sm"
|
||||
>
|
||||
<IconQrcode />
|
||||
</ActionIcon>
|
||||
}
|
||||
w="100%"
|
||||
/>
|
||||
<Button color="green" onClick={() => linkBarcode()} mt="lg" fullWidth>
|
||||
<Trans>Link</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
<BarcodeInput
|
||||
value={barcode}
|
||||
onChange={(event) => setBarcode(event.currentTarget.value)}
|
||||
onScan={actionSubmit}
|
||||
onAction={handleLinkBarcode}
|
||||
actionText={t`Link`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,69 +1,21 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
ScrollArea,
|
||||
Space,
|
||||
Stack,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
useDocumentVisibility,
|
||||
useListState,
|
||||
useLocalStorage
|
||||
} from '@mantine/hooks';
|
||||
import { Button, ScrollArea, Stack, Text } from '@mantine/core';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ContextModalProps } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import { CameraDevice } from 'html5-qrcode/camera/core';
|
||||
import { Html5QrcodeResult } from 'html5-qrcode/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { BarcodeInput } from '../items/BarcodeInput';
|
||||
|
||||
export function QrCodeModal({
|
||||
context,
|
||||
id
|
||||
}: ContextModalProps<{ modalBody: string }>) {
|
||||
const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
|
||||
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
|
||||
key: 'camId',
|
||||
defaultValue: null
|
||||
});
|
||||
const [scanningEnabled, setScanningEnabled] = useState<boolean>(false);
|
||||
const [wasAutoPaused, setWasAutoPaused] = useState<boolean>(false);
|
||||
const documentState = useDocumentVisibility();
|
||||
|
||||
}: Readonly<ContextModalProps<{ modalBody: string }>>) {
|
||||
const [values, handlers] = useListState<string>([]);
|
||||
|
||||
// Mount QR code once we are loaded
|
||||
useEffect(() => {
|
||||
setQrCodeScanner(new Html5Qrcode('reader'));
|
||||
}, []);
|
||||
|
||||
// Stop/star when leaving or reentering page
|
||||
useEffect(() => {
|
||||
if (scanningEnabled && documentState === 'hidden') {
|
||||
stopScanning();
|
||||
setWasAutoPaused(true);
|
||||
} else if (wasAutoPaused && documentState === 'visible') {
|
||||
startScanning();
|
||||
setWasAutoPaused(false);
|
||||
}
|
||||
}, [documentState]);
|
||||
|
||||
// Scanner functions
|
||||
function onScanSuccess(
|
||||
decodedText: string,
|
||||
decodedResult: Html5QrcodeResult
|
||||
) {
|
||||
qrCodeScanner?.pause();
|
||||
|
||||
function onScanAction(decodedText: string) {
|
||||
handlers.append(decodedText);
|
||||
api
|
||||
.post(apiUrl(ApiEndpoints.barcode), { barcode: decodedText })
|
||||
@ -77,124 +29,28 @@ export function QrCodeModal({
|
||||
window.location.href = response.data.url;
|
||||
}
|
||||
});
|
||||
|
||||
qrCodeScanner?.resume();
|
||||
}
|
||||
|
||||
function onScanFailure(error: string) {
|
||||
if (
|
||||
error !=
|
||||
'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.'
|
||||
) {
|
||||
console.warn(`Code scan error = ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function selectCamera() {
|
||||
Html5Qrcode.getCameras()
|
||||
.then((devices) => {
|
||||
if (devices?.length) {
|
||||
setCamId(devices[0]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
showNotification({
|
||||
title: t`Error while getting camera`,
|
||||
message: err,
|
||||
color: 'red',
|
||||
icon: <IconX />
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startScanning() {
|
||||
if (camId && qrCodeScanner) {
|
||||
qrCodeScanner
|
||||
.start(
|
||||
camId.id,
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
(decodedText, decodedResult) => {
|
||||
onScanSuccess(decodedText, decodedResult);
|
||||
},
|
||||
(errorMessage) => {
|
||||
onScanFailure(errorMessage);
|
||||
}
|
||||
)
|
||||
.catch((err: string) => {
|
||||
showNotification({
|
||||
title: t`Error while scanning`,
|
||||
message: err,
|
||||
color: 'red',
|
||||
icon: <IconX />
|
||||
});
|
||||
});
|
||||
setScanningEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
function stopScanning() {
|
||||
if (qrCodeScanner && scanningEnabled) {
|
||||
qrCodeScanner.stop().catch((err: string) => {
|
||||
showNotification({
|
||||
title: t`Error while stopping`,
|
||||
message: err,
|
||||
color: 'red',
|
||||
icon: <IconX />
|
||||
});
|
||||
});
|
||||
setScanningEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group>
|
||||
<Text size="sm">{camId?.label}</Text>
|
||||
<Space style={{ flex: 1 }} />
|
||||
<Badge>{scanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
|
||||
</Group>
|
||||
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
||||
{!camId ? (
|
||||
<Button onClick={() => selectCamera()}>
|
||||
<Trans>Select Camera</Trans>
|
||||
</Button>
|
||||
<Stack gap="xs">
|
||||
<BarcodeInput onScan={onScanAction} />
|
||||
{values.length == 0 ? (
|
||||
<Text c={'grey'}>
|
||||
<Trans>No scans yet!</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<Group>
|
||||
<Button
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => startScanning()}
|
||||
disabled={camId != undefined && scanningEnabled}
|
||||
>
|
||||
<Trans>Start scanning</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => stopScanning()}
|
||||
disabled={!scanningEnabled}
|
||||
>
|
||||
<Trans>Stop scanning</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
{values.length == 0 ? (
|
||||
<Text c={'grey'}>
|
||||
<Trans>No scans yet!</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
|
||||
{values.map((value, index) => (
|
||||
<div key={index}>{value}</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
)}
|
||||
</>
|
||||
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
|
||||
{values.map((value, index) => (
|
||||
<div key={`${index}-${value}`}>{value}</div>
|
||||
))}
|
||||
</ScrollArea>
|
||||
)}
|
||||
<Button
|
||||
fullWidth
|
||||
mt="md"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
stopScanning();
|
||||
// stopScanning();
|
||||
context.closeModal(id);
|
||||
}}
|
||||
>
|
||||
|
@ -222,7 +222,21 @@ export default function Scan() {
|
||||
case InputMethod.Manual:
|
||||
return <InputManual action={addItems} />;
|
||||
case InputMethod.ImageBarcode:
|
||||
return <InputImageBarcode action={addItems} />;
|
||||
return (
|
||||
<InputImageBarcode
|
||||
action={(decodedText: string) => {
|
||||
addItems([
|
||||
{
|
||||
id: randomId(),
|
||||
ref: decodedText,
|
||||
data: decodedText,
|
||||
timestamp: new Date(),
|
||||
source: InputMethod.ImageBarcode
|
||||
}
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Text>No input selected</Text>;
|
||||
}
|
||||
@ -489,10 +503,15 @@ enum InputMethod {
|
||||
ImageBarcode = 'imageBarcode'
|
||||
}
|
||||
|
||||
interface ScanInputInterface {
|
||||
export interface ScanInputInterface {
|
||||
action: (items: ScanItem[]) => void;
|
||||
}
|
||||
|
||||
interface BarcodeInputProps {
|
||||
action: (decodedText: string) => void;
|
||||
notScanningPlaceholder?: string;
|
||||
}
|
||||
|
||||
function InputManual({ action }: Readonly<ScanInputInterface>) {
|
||||
const [value, setValue] = useState<string>('');
|
||||
|
||||
@ -545,7 +564,10 @@ function InputManual({ action }: Readonly<ScanInputInterface>) {
|
||||
}
|
||||
|
||||
/* Input that uses QR code detection from images */
|
||||
export function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
export function InputImageBarcode({
|
||||
action,
|
||||
notScanningPlaceholder = t`Start scanning by selecting a camera and pressing the play button.`
|
||||
}: Readonly<BarcodeInputProps>) {
|
||||
const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
|
||||
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
|
||||
key: 'camId',
|
||||
@ -601,15 +623,7 @@ export function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
lastValue = decodedText;
|
||||
|
||||
// submit value upstream
|
||||
action([
|
||||
{
|
||||
id: randomId(),
|
||||
ref: decodedText,
|
||||
data: decodedText,
|
||||
timestamp: new Date(),
|
||||
source: InputMethod.ImageBarcode
|
||||
}
|
||||
]);
|
||||
action(decodedText);
|
||||
|
||||
qrCodeScanner?.resume();
|
||||
}
|
||||
@ -749,7 +763,13 @@ export function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
{scanningEnabled ? t`Scanning` : t`Not scanning`}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
||||
{scanningEnabled ? (
|
||||
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
||||
) : (
|
||||
<Container px={0} id="reader" w={'100%'} mih="300px">
|
||||
{notScanningPlaceholder}
|
||||
</Container>
|
||||
)}
|
||||
{!camId && (
|
||||
<Button onClick={btnSelectCamera}>
|
||||
<Trans>Select Camera</Trans>
|
||||
|
Loading…
Reference in New Issue
Block a user