From 61c8a8f703909d1a0a80d15cd4cc5a364195f87c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Aug 2025 15:29:57 +1000 Subject: [PATCH] [UI] Enhance stock item rendering (#10143) * Enhance stock item rendering - Show location in forms - Allow differentiation between items * Unit test fixes - Account for longer query time in docker mode * Cast to string * Improved rendering --- src/frontend/lib/types/Rendering.tsx | 1 + .../src/components/render/Instance.tsx | 73 +++++++++++++++---- src/frontend/src/components/render/Part.tsx | 25 ++++++- src/frontend/src/components/render/Stock.tsx | 69 +++++++++++++++--- src/frontend/src/tables/TableHoverCard.tsx | 23 +++++- 5 files changed, 163 insertions(+), 28 deletions(-) diff --git a/src/frontend/lib/types/Rendering.tsx b/src/frontend/lib/types/Rendering.tsx index b4a5af117a..c2f4b1e676 100644 --- a/src/frontend/lib/types/Rendering.tsx +++ b/src/frontend/lib/types/Rendering.tsx @@ -8,6 +8,7 @@ export interface InstanceRenderInterface { link?: boolean; navigate?: any; showSecondary?: boolean; + extra?: Record; } type EnumDictionary = { diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 00e89ae192..06e1c29272 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -1,5 +1,14 @@ import { t } from '@lingui/core/macro'; -import { Alert, Anchor, Group, Skeleton, Space, Text } from '@mantine/core'; +import { + Alert, + Anchor, + Group, + type MantineSize, + Paper, + Skeleton, + Space, + Text +} from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useCallback } from 'react'; @@ -158,8 +167,8 @@ export function RenderInlineModel({ showSecondary = true, tooltip }: Readonly<{ - primary: string; - secondary?: string; + primary: ReactNode; + secondary?: ReactNode; showSecondary?: boolean; prefix?: ReactNode; suffix?: ReactNode; @@ -180,15 +189,25 @@ export function RenderInlineModel({ [url, navigate] ); - const primaryText = shortenString({ - str: primary, - len: 50 - }); + if (typeof primary === 'string') { + primary = shortenString({ + str: primary, + len: 50 + }); - const secondaryText = shortenString({ - str: secondary, - len: 75 - }); + primary = {primary}; + } + + if (typeof secondary === 'string') { + secondary = shortenString({ + str: secondary, + len: 75 + }); + + if (secondary.toString()?.length > 0) { + secondary = ; + } + } return ( @@ -197,12 +216,12 @@ export function RenderInlineModel({ {image && } {url ? ( onClick(event)}> - {primaryText} + {primary} ) : ( - {primaryText} + primary )} - {showSecondary && secondary && {secondaryText}} + {showSecondary && secondary && secondary} {suffix && ( <> @@ -222,3 +241,29 @@ export function UnknownRenderer({ const model_name = model ? model.toString() : 'undefined'; return ; } + +/** + * Render a "badge like" component with a text label + */ +export function InlineSecondaryBadge({ + text, + title, + size = 'xs' +}: { + text: string; + title?: string; + size?: MantineSize; +}): ReactNode { + return ( + + + {title && ( + + {title}: + + )} + {text} + + + ); +} diff --git a/src/frontend/src/components/render/Part.tsx b/src/frontend/src/components/render/Part.tsx index b678109c61..613740b58d 100644 --- a/src/frontend/src/components/render/Part.tsx +++ b/src/frontend/src/components/render/Part.tsx @@ -1,10 +1,12 @@ import { t } from '@lingui/core/macro'; -import { Badge } from '@mantine/core'; +import { Badge, Group, Text } from '@mantine/core'; import type { ReactNode } from 'react'; import { ModelType } from '@lib/enums/ModelType'; import { formatDecimal } from '@lib/functions/Formatting'; import { getDetailUrl } from '@lib/functions/Navigation'; +import { shortenString } from '../../functions/tables'; +import { TableHoverCard } from '../../tables/TableHoverCard'; import { ApiIcon } from '../items/ApiIcon'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; @@ -58,6 +60,24 @@ export function RenderPartCategory( ): ReactNode { const { instance } = props; + const suffix: ReactNode = ( + + {instance.pathstring}]} + /> + + ); + + const category = shortenString({ + str: instance.pathstring, + len: 50 + }); + return ( } } - primary={instance.pathstring} + primary={category} secondary={instance.description} + suffix={suffix} url={ props.link ? getDetailUrl(ModelType.partcategory, instance.pk) diff --git a/src/frontend/src/components/render/Stock.tsx b/src/frontend/src/components/render/Stock.tsx index f683dc7b7e..ea8726eb45 100644 --- a/src/frontend/src/components/render/Stock.tsx +++ b/src/frontend/src/components/render/Stock.tsx @@ -1,12 +1,18 @@ import { t } from '@lingui/core/macro'; -import { Text } from '@mantine/core'; +import { Group, Text } from '@mantine/core'; import type { ReactNode } from 'react'; import { ModelType } from '@lib/enums/ModelType'; import { formatDecimal } from '@lib/functions/Formatting'; import { getDetailUrl } from '@lib/functions/Navigation'; +import { shortenString } from '../../functions/tables'; +import { TableHoverCard } from '../../tables/TableHoverCard'; import { ApiIcon } from '../items/ApiIcon'; -import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; +import { + InlineSecondaryBadge, + type InstanceRenderInterface, + RenderInlineModel +} from './Instance'; /** * Inline rendering of a single StockLocation instance @@ -16,6 +22,24 @@ export function RenderStockLocation( ): ReactNode { const { instance } = props; + const suffix: ReactNode = ( + + {instance.pathstring}]} + /> + + ); + + const location = shortenString({ + str: instance.pathstring, + len: 50 + }); + return ( } } - primary={instance.pathstring} + primary={location} secondary={instance.description} + suffix={suffix} url={ props.link ? getDetailUrl(ModelType.stocklocation, instance.pk) @@ -69,18 +94,44 @@ export function RenderStockItem( quantity_string = `${t`Quantity`}: ${formatDecimal(instance.quantity)}`; } - let batch_string = ''; + const showLocation: boolean = props.extra?.show_location !== false; + const location: any = props.instance?.location_detail; - if (!!instance.batch) { - batch_string = `${t`Batch`}: ${instance.batch}`; - } + // Form the "secondary" text to display + const secondary: ReactNode = ( + + {showLocation && location?.name && ( + + )} + {instance.batch && ( + + )} + + ); + + // Form the "suffix" text to display + const suffix: ReactNode = ( + + {quantity_string} + {location && ( + {location.pathstring}]} + /> + )} + + ); return ( {quantity_string}} + secondary={secondary} + suffix={suffix} image={instance.part_detail?.thumbnail || instance.part_detail?.image} url={ props.link ? getDetailUrl(ModelType.stockitem, instance.pk) : undefined diff --git a/src/frontend/src/tables/TableHoverCard.tsx b/src/frontend/src/tables/TableHoverCard.tsx index 31351e29d5..8159c1e930 100644 --- a/src/frontend/src/tables/TableHoverCard.tsx +++ b/src/frontend/src/tables/TableHoverCard.tsx @@ -1,5 +1,12 @@ import { t } from '@lingui/core/macro'; -import { Divider, Group, HoverCard, Stack, Text } from '@mantine/core'; +import { + Divider, + type FloatingPosition, + Group, + HoverCard, + Stack, + Text +} from '@mantine/core'; import { type ReactNode, useMemo } from 'react'; import type { InvenTreeIconType } from '@lib/types/Icons'; @@ -15,13 +22,17 @@ export function TableHoverCard({ extra, // The extra information to display title, // The title of the hovercard icon, // The icon to display - iconColor // The icon color + iconColor, // The icon color + position, // The position of the hovercard + zIndex // Optional z-index for the hovercard }: Readonly<{ value: any; extra?: ReactNode; title?: string; icon?: keyof InvenTreeIconType; iconColor?: string; + position?: FloatingPosition; + zIndex?: string | number; }>) { const extraItems: ReactNode = useMemo(() => { if (Array.isArray(extra)) { @@ -47,7 +58,13 @@ export function TableHoverCard({ } return ( - + {value}