add PUI qrcode preview

This commit is contained in:
wolflu05 2024-07-15 00:06:48 +02:00
parent e80b00f5b6
commit 2162eef133
No known key found for this signature in database
GPG Key ID: 9099EFC7C5EB963C
14 changed files with 240 additions and 83 deletions

View File

@ -38,7 +38,6 @@
"@mantine/spotlight": "^7.11.0",
"@mantine/vanilla-extract": "^7.11.0",
"@mdxeditor/editor": "^3.6.1",
"@naisutech/react-tree": "^3.1.0",
"@sentry/react": "^8.13.0",
"@tabler/icons-react": "^3.7.0",
"@tanstack/react-query": "^5.49.2",
@ -53,6 +52,7 @@
"embla-carousel-react": "^8.1.6",
"html5-qrcode": "^2.3.8",
"mantine-datatable": "^7.11.1",
"qrcode": "^1.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-grid-layout": "^1.4.4",
@ -72,6 +72,7 @@
"@lingui/macro": "^4.11.1",
"@playwright/test": "^1.45.0",
"@types/node": "^20.14.9",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-grid-layout": "^1.3.5",

View File

@ -11,7 +11,7 @@ export function ButtonMenu({
label = ''
}: {
icon: any;
actions: any[];
actions: React.ReactNode[];
label?: string;
tooltip?: string;
}) {

View File

@ -1,6 +1,13 @@
import { t } from '@lingui/macro';
import { Button, CopyButton as MantineCopyButton } from '@mantine/core';
import { IconCopy } from '@tabler/icons-react';
import {
ActionIcon,
Button,
CopyButton as MantineCopyButton,
Text,
Tooltip
} from '@mantine/core';
import { InvenTreeIcon } from '../../functions/icons';
export function CopyButton({
value,
@ -9,24 +16,27 @@ export function CopyButton({
value: any;
label?: JSX.Element;
}) {
const ButtonComponent = label ? Button : ActionIcon;
return (
<MantineCopyButton value={value}>
{({ copied, copy }) => (
<Button
color={copied ? 'teal' : 'gray'}
onClick={copy}
title={t`Copy to clipboard`}
variant="subtle"
size="compact-md"
>
<IconCopy size={10} />
{label && (
<>
<div>&nbsp;</div>
{label}
</>
)}
</Button>
<Tooltip label={copied ? t`Copied` : t`Copy`} withArrow>
<ButtonComponent
color={copied ? 'teal' : 'gray'}
onClick={copy}
variant="transparent"
size="sm"
>
{copied ? (
<InvenTreeIcon icon="check" />
) : (
<InvenTreeIcon icon="copy" />
)}
{label && <Text ml={10}>{label}</Text>}
</ButtonComponent>
</Tooltip>
)}
</MantineCopyButton>
);

View File

@ -1,15 +1,12 @@
import { t } from '@lingui/macro';
import {
ActionIcon,
Anchor,
Badge,
CopyButton,
Paper,
Skeleton,
Stack,
Table,
Text,
Tooltip
Text
} from '@mantine/core';
import { useSuspenseQuery } from '@tanstack/react-query';
import { getValueAtPath } from 'mantine-datatable';
@ -24,6 +21,7 @@ import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton';
import { YesNoButton } from '../buttons/YesNoButton';
import { ProgressBar } from '../items/ProgressBar';
import { StylishText } from '../items/StylishText';
@ -325,26 +323,7 @@ function StatusValue(props: Readonly<FieldProps>) {
}
function CopyField({ value }: { value: string }) {
return (
<CopyButton value={value}>
{({ copied, copy }) => (
<Tooltip label={copied ? t`Copied` : t`Copy`} withArrow>
<ActionIcon
color={copied ? 'teal' : 'gray'}
onClick={copy}
variant="transparent"
size="sm"
>
{copied ? (
<InvenTreeIcon icon="check" />
) : (
<InvenTreeIcon icon="copy" />
)}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
);
return <CopyButton value={value} />;
}
export function DetailsTableField({

View File

@ -6,6 +6,7 @@ import {
Menu,
Tooltip
} from '@mantine/core';
import { modals } from '@mantine/modals';
import {
IconCopy,
IconEdit,
@ -16,9 +17,11 @@ import {
} from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType';
import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
import { InvenTreeQRCode } from './QRCode';
export type ActionDropdownItem = {
icon: ReactNode;
@ -128,11 +131,20 @@ export function BarcodeActionDropdown({
// Common action button for viewing a barcode
export function ViewBarcodeAction({
hidden = false,
onClick
model,
pk
}: {
hidden?: boolean;
onClick?: () => void;
model: ModelType;
pk: number;
}): ActionDropdownItem {
const onClick = () => {
modals.open({
title: t`View Barcode`,
children: <InvenTreeQRCode model={model} pk={pk} />
});
};
return {
icon: <IconQrcode />,
name: t`View`,

View File

@ -0,0 +1,119 @@
import { t } from '@lingui/macro';
import {
Box,
Code,
Group,
Image,
Select,
Skeleton,
Stack
} from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton';
type QRCodeProps = {
ecl?: 'L' | 'M' | 'Q' | 'H';
margin?: number;
data?: string;
};
export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => {
const [qrCode, setQRCode] = useState<string>();
useEffect(() => {
if (!data) return setQRCode(undefined);
QR.toString(data, { errorCorrectionLevel: ecl, type: 'svg', margin }).then(
(svg) => {
setQRCode(`data:image/svg+xml;utf8,${encodeURIComponent(svg)}`);
}
);
}, [data, ecl]);
return (
<Box>
{qrCode ? (
<Image src={qrCode} alt="QR Code" />
) : (
<Skeleton height={500} />
)}
</Box>
);
};
type InvenTreeQRCodeProps = {
model: ModelType;
pk: number;
showEclSelector?: boolean;
} & Omit<QRCodeProps, 'data'>;
export const InvenTreeQRCode = ({
showEclSelector = true,
model,
pk,
ecl: eclProp = 'Q',
...props
}: InvenTreeQRCodeProps) => {
const settings = useGlobalSettingsState();
const [ecl, setEcl] = useState(eclProp);
useEffect(() => {
if (eclProp) setEcl(eclProp);
}, [eclProp]);
const { data } = useQuery({
queryKey: ['qr-code', model, pk],
queryFn: async () => {
const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), {
model,
pk
});
return res.data?.barcode as string;
}
});
const eclOptions = useMemo(
() => [
{ value: 'L', label: t`Low (7%)` },
{ value: 'M', label: t`Medium (15%)` },
{ value: 'Q', label: t`Quartile (25%)` },
{ value: 'H', label: t`High (30%)` }
],
[]
);
return (
<Stack>
<QRCode data={data} ecl={ecl} {...props} />
{data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && (
<Group justify={showEclSelector ? 'space-between' : 'center'}>
{showEclSelector && (
<Select
allowDeselect={false}
label={t`Select Error Correction Level`}
value={ecl}
onChange={(v) =>
setEcl(v as Exclude<QRCodeProps['ecl'], undefined>)
}
data={eclOptions}
/>
)}
<Group>
<Code>{data}</Code>
<CopyButton value={data} />
</Group>
</Group>
)}
</Stack>
);
};

View File

@ -38,6 +38,7 @@ export enum ApiEndpoints {
settings_global_list = 'settings/global/',
settings_user_list = 'settings/user/',
barcode = 'barcode/',
generate_barcode = 'barcode/generate/',
news = 'news/',
global_status = 'generic/status/',
version = 'version/',

View File

@ -370,7 +370,10 @@ export default function BuildDetail() {
tooltip={t`Barcode Actions`}
icon={<IconQrcode />}
actions={[
ViewBarcodeAction({}),
ViewBarcodeAction({
model: ModelType.build,
pk: build.pk
}),
LinkBarcodeAction({
hidden: build?.barcode_hash
}),

View File

@ -894,7 +894,10 @@ export default function PartDetail() {
<AdminButton model={ModelType.part} pk={part.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({}),
ViewBarcodeAction({
model: ModelType.part,
pk: part.pk
}),
LinkBarcodeAction({
hidden: part?.barcode_hash || !user.hasChangeRole(UserRoles.part)
}),

View File

@ -305,7 +305,10 @@ export default function PurchaseOrderDetail() {
<AdminButton model={ModelType.purchaseorder} pk={order.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({}),
ViewBarcodeAction({
model: ModelType.purchaseorder,
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash
}),

View File

@ -276,23 +276,28 @@ export default function Stock() {
variant="outline"
size="lg"
/>,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({}),
LinkBarcodeAction({}),
UnlinkBarcodeAction({}),
{
name: 'Scan in stock items',
icon: <InvenTreeIcon icon="stock" />,
tooltip: 'Scan items'
},
{
name: 'Scan in container',
icon: <InvenTreeIcon icon="unallocated_stock" />,
tooltip: 'Scan container'
}
]}
/>,
location.pk ? (
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({
model: ModelType.stocklocation,
pk: location.pk
}),
LinkBarcodeAction({}),
UnlinkBarcodeAction({}),
{
name: 'Scan in stock items',
icon: <InvenTreeIcon icon="stock" />,
tooltip: 'Scan items'
},
{
name: 'Scan in container',
icon: <InvenTreeIcon icon="unallocated_stock" />,
tooltip: 'Scan container'
}
]}
/>
) : null,
<PrintingActions
modelType={ModelType.stocklocation}
items={[location.pk ?? 0]}

View File

@ -423,7 +423,10 @@ export default function StockDetail() {
<AdminButton model={ModelType.stockitem} pk={stockitem.pk} />,
<BarcodeActionDropdown
actions={[
ViewBarcodeAction({}),
ViewBarcodeAction({
model: ModelType.stockitem,
pk: stockitem.pk
}),
LinkBarcodeAction({
hidden:
stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock)

View File

@ -104,7 +104,7 @@ export type InvenTreeTableProps<T = any> = {
enableLabels?: boolean;
enableReports?: boolean;
pageSize?: number;
barcodeActions?: any[];
barcodeActions?: React.ReactNode[];
tableFilters?: TableFilter[];
tableActions?: React.ReactNode[];
rowExpansion?: any;

View File

@ -796,7 +796,7 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43"
integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==
"@emotion/is-prop-valid@1.2.2", "@emotion/is-prop-valid@^1.2.0":
"@emotion/is-prop-valid@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337"
integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==
@ -1822,15 +1822,6 @@
dependencies:
moo "^0.5.1"
"@naisutech/react-tree@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@naisutech/react-tree/-/react-tree-3.1.0.tgz#a83820425b53a1ec7a39804ff8bd9024f0a953f4"
integrity sha512-6p1l3ZIaTmbgiAf/mpFELvqwl51LDhr+09f7L+C27DBLWjtleezCMoUuiSLhrJgpixCPNL13PuI3q2yn+0AGvA==
dependencies:
"@emotion/is-prop-valid" "^1.2.0"
nanoid "^4.0.0"
react-draggable "^4.4.5"
"@open-draft/deferred-promise@^2.1.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd"
@ -2598,6 +2589,13 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/qrcode@^1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac"
integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==
dependencies:
"@types/node" "*"
"@types/react-dom@^18.3.0":
version "18.3.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
@ -3455,6 +3453,11 @@ diff@^5.1.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
dijkstrajs@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
@ -3507,6 +3510,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
encode-utf8@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -4952,11 +4960,6 @@ nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
nanoid@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
integrity sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==
next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
@ -5236,6 +5239,11 @@ playwright@1.45.0:
optionalDependencies:
fsevents "2.3.2"
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
pofile@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.4.tgz#eab7e29f5017589b2a61b2259dff608c0cad76a2"
@ -5302,6 +5310,16 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
qrcode@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170"
integrity sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==
dependencies:
dijkstrajs "^1.0.1"
encode-utf8 "^1.0.3"
pngjs "^5.0.0"
yargs "^15.3.1"
ramda@^0.27.1:
version "0.27.2"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1"
@ -6280,7 +6298,7 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^15.0.2:
yargs@^15.0.2, yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==