diff --git a/src/frontend/package.json b/src/frontend/package.json index 6abad83d32..d6df1680a8 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -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", diff --git a/src/frontend/src/components/buttons/ButtonMenu.tsx b/src/frontend/src/components/buttons/ButtonMenu.tsx index 42feeb0718..0bf2a36df7 100644 --- a/src/frontend/src/components/buttons/ButtonMenu.tsx +++ b/src/frontend/src/components/buttons/ButtonMenu.tsx @@ -11,7 +11,7 @@ export function ButtonMenu({ label = '' }: { icon: any; - actions: any[]; + actions: React.ReactNode[]; label?: string; tooltip?: string; }) { diff --git a/src/frontend/src/components/buttons/CopyButton.tsx b/src/frontend/src/components/buttons/CopyButton.tsx index 3ac02e5da2..c63ec29630 100644 --- a/src/frontend/src/components/buttons/CopyButton.tsx +++ b/src/frontend/src/components/buttons/CopyButton.tsx @@ -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 ( {({ copied, copy }) => ( - + + + {copied ? ( + + ) : ( + + )} + + {label && {label}} + + )} ); diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index d3675bceaa..7c2346eb38 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -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) { } function CopyField({ value }: { value: string }) { - return ( - - {({ copied, copy }) => ( - - - {copied ? ( - - ) : ( - - )} - - - )} - - ); + return ; } export function DetailsTableField({ diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index 7d04fb0d56..d29c21c93f 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -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: + }); + }; + return { icon: , name: t`View`, diff --git a/src/frontend/src/components/items/QRCode.tsx b/src/frontend/src/components/items/QRCode.tsx new file mode 100644 index 0000000000..b0826bd1d3 --- /dev/null +++ b/src/frontend/src/components/items/QRCode.tsx @@ -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(); + + 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 ( + + {qrCode ? ( + QR Code + ) : ( + + )} + + ); +}; + +type InvenTreeQRCodeProps = { + model: ModelType; + pk: number; + showEclSelector?: boolean; +} & Omit; + +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 ( + + + {data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && ( + + {showEclSelector && ( +