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 { Trans, t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Code,
|
Code,
|
||||||
Divider,
|
|
||||||
Flex,
|
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
Select,
|
Select,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text
|
||||||
TextInput
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { modals } from '@mantine/modals';
|
import { modals } from '@mantine/modals';
|
||||||
import { IconQrcode } from '@tabler/icons-react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import QR from 'qrcode';
|
import QR from 'qrcode';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { set } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { InputImageBarcode, ScanItem } from '../../pages/Index/Scan';
|
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { CopyButton } from '../buttons/CopyButton';
|
import { CopyButton } from '../buttons/CopyButton';
|
||||||
import { QrCodeType } from './ActionDropdown';
|
import { QrCodeType } from './ActionDropdown';
|
||||||
|
import { BarcodeInput } from './BarcodeInput';
|
||||||
|
|
||||||
type QRCodeProps = {
|
type QRCodeProps = {
|
||||||
ecl?: 'L' | 'M' | 'Q' | 'H';
|
ecl?: 'L' | 'M' | 'Q' | 'H';
|
||||||
@ -162,37 +156,22 @@ export const QRCodeLink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
|
|||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const actionSubmit = (data: ScanItem[]) => {
|
const actionSubmit = (decodedText: string) => {
|
||||||
linkBarcode(data[0].data);
|
linkBarcode(decodedText);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinkBarcode = () => {
|
||||||
|
linkBarcode(barcode);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<BarcodeInput
|
||||||
{isScanning ? (
|
|
||||||
<>
|
|
||||||
<InputImageBarcode action={actionSubmit} />
|
|
||||||
<Divider />
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<TextInput
|
|
||||||
label={t`Barcode`}
|
|
||||||
value={barcode}
|
value={barcode}
|
||||||
onChange={(event) => setBarcode(event.currentTarget.value)}
|
onChange={(event) => setBarcode(event.currentTarget.value)}
|
||||||
placeholder={t`Scan barcode data here using barcode scanner`}
|
onScan={actionSubmit}
|
||||||
leftSection={
|
onAction={handleLinkBarcode}
|
||||||
<ActionIcon
|
actionText={t`Link`}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,69 +1,21 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import {
|
import { Button, ScrollArea, Stack, Text } from '@mantine/core';
|
||||||
Badge,
|
import { useListState } from '@mantine/hooks';
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Group,
|
|
||||||
ScrollArea,
|
|
||||||
Space,
|
|
||||||
Stack,
|
|
||||||
Text
|
|
||||||
} from '@mantine/core';
|
|
||||||
import {
|
|
||||||
useDocumentVisibility,
|
|
||||||
useListState,
|
|
||||||
useLocalStorage
|
|
||||||
} from '@mantine/hooks';
|
|
||||||
import { ContextModalProps } from '@mantine/modals';
|
import { ContextModalProps } from '@mantine/modals';
|
||||||
import { showNotification } from '@mantine/notifications';
|
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 { api } from '../../App';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
|
import { BarcodeInput } from '../items/BarcodeInput';
|
||||||
|
|
||||||
export function QrCodeModal({
|
export function QrCodeModal({
|
||||||
context,
|
context,
|
||||||
id
|
id
|
||||||
}: ContextModalProps<{ modalBody: string }>) {
|
}: Readonly<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();
|
|
||||||
|
|
||||||
const [values, handlers] = useListState<string>([]);
|
const [values, handlers] = useListState<string>([]);
|
||||||
|
|
||||||
// Mount QR code once we are loaded
|
function onScanAction(decodedText: string) {
|
||||||
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();
|
|
||||||
|
|
||||||
handlers.append(decodedText);
|
handlers.append(decodedText);
|
||||||
api
|
api
|
||||||
.post(apiUrl(ApiEndpoints.barcode), { barcode: decodedText })
|
.post(apiUrl(ApiEndpoints.barcode), { barcode: decodedText })
|
||||||
@ -77,105 +29,11 @@ export function QrCodeModal({
|
|||||||
window.location.href = response.data.url;
|
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 (
|
return (
|
||||||
<Stack>
|
<Stack gap="xs">
|
||||||
<Group>
|
<BarcodeInput onScan={onScanAction} />
|
||||||
<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>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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 ? (
|
{values.length == 0 ? (
|
||||||
<Text c={'grey'}>
|
<Text c={'grey'}>
|
||||||
<Trans>No scans yet!</Trans>
|
<Trans>No scans yet!</Trans>
|
||||||
@ -183,18 +41,16 @@ export function QrCodeModal({
|
|||||||
) : (
|
) : (
|
||||||
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
|
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
|
||||||
{values.map((value, index) => (
|
{values.map((value, index) => (
|
||||||
<div key={index}>{value}</div>
|
<div key={`${index}-${value}`}>{value}</div>
|
||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
mt="md"
|
mt="md"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
stopScanning();
|
// stopScanning();
|
||||||
context.closeModal(id);
|
context.closeModal(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -222,7 +222,21 @@ export default function Scan() {
|
|||||||
case InputMethod.Manual:
|
case InputMethod.Manual:
|
||||||
return <InputManual action={addItems} />;
|
return <InputManual action={addItems} />;
|
||||||
case InputMethod.ImageBarcode:
|
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:
|
default:
|
||||||
return <Text>No input selected</Text>;
|
return <Text>No input selected</Text>;
|
||||||
}
|
}
|
||||||
@ -489,10 +503,15 @@ enum InputMethod {
|
|||||||
ImageBarcode = 'imageBarcode'
|
ImageBarcode = 'imageBarcode'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScanInputInterface {
|
export interface ScanInputInterface {
|
||||||
action: (items: ScanItem[]) => void;
|
action: (items: ScanItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BarcodeInputProps {
|
||||||
|
action: (decodedText: string) => void;
|
||||||
|
notScanningPlaceholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function InputManual({ action }: Readonly<ScanInputInterface>) {
|
function InputManual({ action }: Readonly<ScanInputInterface>) {
|
||||||
const [value, setValue] = useState<string>('');
|
const [value, setValue] = useState<string>('');
|
||||||
|
|
||||||
@ -545,7 +564,10 @@ function InputManual({ action }: Readonly<ScanInputInterface>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Input that uses QR code detection from images */
|
/* 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 [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
|
||||||
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
|
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
|
||||||
key: 'camId',
|
key: 'camId',
|
||||||
@ -601,15 +623,7 @@ export function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
|||||||
lastValue = decodedText;
|
lastValue = decodedText;
|
||||||
|
|
||||||
// submit value upstream
|
// submit value upstream
|
||||||
action([
|
action(decodedText);
|
||||||
{
|
|
||||||
id: randomId(),
|
|
||||||
ref: decodedText,
|
|
||||||
data: decodedText,
|
|
||||||
timestamp: new Date(),
|
|
||||||
source: InputMethod.ImageBarcode
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
qrCodeScanner?.resume();
|
qrCodeScanner?.resume();
|
||||||
}
|
}
|
||||||
@ -749,7 +763,13 @@ export function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
|||||||
{scanningEnabled ? t`Scanning` : t`Not scanning`}
|
{scanningEnabled ? t`Scanning` : t`Not scanning`}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
{scanningEnabled ? (
|
||||||
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
||||||
|
) : (
|
||||||
|
<Container px={0} id="reader" w={'100%'} mih="300px">
|
||||||
|
{notScanningPlaceholder}
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
{!camId && (
|
{!camId && (
|
||||||
<Button onClick={btnSelectCamera}>
|
<Button onClick={btnSelectCamera}>
|
||||||
<Trans>Select Camera</Trans>
|
<Trans>Select Camera</Trans>
|
||||||
|
Loading…
Reference in New Issue
Block a user