mirror of
https://github.com/inventree/InvenTree.git
synced 2025-09-13 14:11:37 +00:00
[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
This commit is contained in:
@@ -8,6 +8,7 @@ export interface InstanceRenderInterface {
|
||||
link?: boolean;
|
||||
navigate?: any;
|
||||
showSecondary?: boolean;
|
||||
extra?: Record<string, any>;
|
||||
}
|
||||
|
||||
type EnumDictionary<T extends string | symbol | number, U> = {
|
||||
|
@@ -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 = <Text size='sm'>{primary}</Text>;
|
||||
}
|
||||
|
||||
if (typeof secondary === 'string') {
|
||||
secondary = shortenString({
|
||||
str: secondary,
|
||||
len: 75
|
||||
});
|
||||
|
||||
if (secondary.toString()?.length > 0) {
|
||||
secondary = <InlineSecondaryBadge text={secondary.toString()} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Group gap='xs' justify='space-between' wrap='nowrap' title={tooltip}>
|
||||
@@ -197,12 +216,12 @@ export function RenderInlineModel({
|
||||
{image && <Thumbnail src={image} size={18} />}
|
||||
{url ? (
|
||||
<Anchor href='' onClick={(event: any) => onClick(event)}>
|
||||
<Text size='sm'>{primaryText}</Text>
|
||||
{primary}
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text size='sm'>{primaryText}</Text>
|
||||
primary
|
||||
)}
|
||||
{showSecondary && secondary && <Text size='xs'>{secondaryText}</Text>}
|
||||
{showSecondary && secondary && secondary}
|
||||
</Group>
|
||||
{suffix && (
|
||||
<>
|
||||
@@ -222,3 +241,29 @@ export function UnknownRenderer({
|
||||
const model_name = model ? model.toString() : 'undefined';
|
||||
return <Alert color='red' title={t`Unknown model: ${model_name}`} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a "badge like" component with a text label
|
||||
*/
|
||||
export function InlineSecondaryBadge({
|
||||
text,
|
||||
title,
|
||||
size = 'xs'
|
||||
}: {
|
||||
text: string;
|
||||
title?: string;
|
||||
size?: MantineSize;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<Paper p={2} withBorder style={{ backgroundColor: 'transparent' }}>
|
||||
<Group gap='xs'>
|
||||
{title && (
|
||||
<Text size={size} title={title}>
|
||||
{title}:
|
||||
</Text>
|
||||
)}
|
||||
<Text size={size ?? 'xs'}>{text}</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
@@ -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 = (
|
||||
<Group gap='xs'>
|
||||
<TableHoverCard
|
||||
value=''
|
||||
position='bottom-end'
|
||||
zIndex={10000}
|
||||
icon='sitemap'
|
||||
title={t`Category`}
|
||||
extra={[<Text>{instance.pathstring}</Text>]}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const category = shortenString({
|
||||
str: instance.pathstring,
|
||||
len: 50
|
||||
});
|
||||
|
||||
return (
|
||||
<RenderInlineModel
|
||||
{...props}
|
||||
@@ -68,8 +88,9 @@ export function RenderPartCategory(
|
||||
{instance.icon && <ApiIcon name={instance.icon} />}
|
||||
</>
|
||||
}
|
||||
primary={instance.pathstring}
|
||||
primary={category}
|
||||
secondary={instance.description}
|
||||
suffix={suffix}
|
||||
url={
|
||||
props.link
|
||||
? getDetailUrl(ModelType.partcategory, instance.pk)
|
||||
|
@@ -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 = (
|
||||
<Group gap='xs'>
|
||||
<TableHoverCard
|
||||
value=''
|
||||
position='bottom-end'
|
||||
zIndex={10000}
|
||||
icon='sitemap'
|
||||
title={t`Location`}
|
||||
extra={[<Text>{instance.pathstring}</Text>]}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const location = shortenString({
|
||||
str: instance.pathstring,
|
||||
len: 50
|
||||
});
|
||||
|
||||
return (
|
||||
<RenderInlineModel
|
||||
{...props}
|
||||
@@ -26,8 +50,9 @@ export function RenderStockLocation(
|
||||
{instance.icon && <ApiIcon name={instance.icon} />}
|
||||
</>
|
||||
}
|
||||
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 = (
|
||||
<Group gap='xs' style={{ paddingLeft: '5px' }}>
|
||||
{showLocation && location?.name && (
|
||||
<InlineSecondaryBadge title={t`Location`} text={location.name} />
|
||||
)}
|
||||
{instance.batch && (
|
||||
<InlineSecondaryBadge title={t`Batch`} text={instance.batch} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
|
||||
// Form the "suffix" text to display
|
||||
const suffix: ReactNode = (
|
||||
<Group gap='xs'>
|
||||
<Text size='xs'>{quantity_string}</Text>
|
||||
{location && (
|
||||
<TableHoverCard
|
||||
value=''
|
||||
position='bottom-end'
|
||||
zIndex={10000}
|
||||
icon='sitemap'
|
||||
title={t`Location`}
|
||||
extra={[<Text>{location.pathstring}</Text>]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
|
||||
return (
|
||||
<RenderInlineModel
|
||||
{...props}
|
||||
primary={instance.part_detail?.full_name}
|
||||
secondary={batch_string}
|
||||
suffix={<Text size='xs'>{quantity_string}</Text>}
|
||||
secondary={secondary}
|
||||
suffix={suffix}
|
||||
image={instance.part_detail?.thumbnail || instance.part_detail?.image}
|
||||
url={
|
||||
props.link ? getDetailUrl(ModelType.stockitem, instance.pk) : undefined
|
||||
|
@@ -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 (
|
||||
<HoverCard withinPortal={true} closeDelay={20} openDelay={250}>
|
||||
<HoverCard
|
||||
withinPortal={true}
|
||||
closeDelay={20}
|
||||
openDelay={250}
|
||||
position={position}
|
||||
zIndex={zIndex}
|
||||
>
|
||||
<HoverCard.Target>
|
||||
<Group gap='xs' justify='space-between' wrap='nowrap'>
|
||||
{value}
|
||||
|
Reference in New Issue
Block a user