[PUI] Tweaks (#7007)

* Allow pk field spec for row click

* Fix row click-through for UsedInTable

* Cleanup "details" view for part page

* Add 'stock' column to parametric part table

* Add details badges to some pages

* Add extra badges to PartDetailPage
This commit is contained in:
Oliver 2024-04-12 16:58:51 +10:00 committed by GitHub
parent cbbdb70762
commit f4d748ebed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 179 additions and 44 deletions

View File

@ -0,0 +1,26 @@
import { Badge } from '@mantine/core';
export type DetailsBadgeProps = {
color: string;
label: string;
size?: string;
visible?: boolean;
key?: any;
};
export default function DetailsBadge(props: DetailsBadgeProps) {
if (props.visible == false) {
return null;
}
return (
<Badge
key={props.key}
color={props.color}
variant="filled"
size={props.size ?? 'lg'}
>
{props.label}
</Badge>
);
}

View File

@ -1,6 +1,7 @@
import { Group, Paper, Space, Stack, Text } from '@mantine/core'; import { Group, Paper, Space, Stack, Text } from '@mantine/core';
import { Fragment, ReactNode } from 'react'; import { Fragment, ReactNode } from 'react';
import DetailsBadge, { DetailsBadgeProps } from '../details/DetailsBadge';
import { ApiImage } from '../images/ApiImage'; import { ApiImage } from '../images/ApiImage';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList'; import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
@ -15,6 +16,7 @@ export function PageDetail({
title, title,
subtitle, subtitle,
detail, detail,
badges,
imageUrl, imageUrl,
breadcrumbs, breadcrumbs,
breadcrumbAction, breadcrumbAction,
@ -24,6 +26,7 @@ export function PageDetail({
subtitle?: string; subtitle?: string;
imageUrl?: string; imageUrl?: string;
detail?: ReactNode; detail?: ReactNode;
badges?: ReactNode[];
breadcrumbs?: Breadcrumb[]; breadcrumbs?: Breadcrumb[];
breadcrumbAction?: () => void; breadcrumbAction?: () => void;
actions?: ReactNode[]; actions?: ReactNode[];
@ -56,6 +59,9 @@ export function PageDetail({
</Group> </Group>
<Space /> <Space />
{detail} {detail}
<Group position="right" spacing="xs" noWrap>
{badges}
</Group>
<Space /> <Space />
{actions && ( {actions && (
<Group spacing={5} position="right"> <Group spacing={5} position="right">

View File

@ -334,16 +334,17 @@ export default function BuildDetail() {
]; ];
}, [id, build, user]); }, [id, build, user]);
const buildDetail = useMemo(() => { const buildBadges = useMemo(() => {
return build?.status ? ( return instanceQuery.isFetching
StatusRenderer({ ? []
status: build.status, : [
type: ModelType.build <StatusRenderer
}) status={build.status}
) : ( type={ModelType.build}
<Skeleton /> options={{ size: 'lg' }}
); />
}, [build, id]); ];
}, [build, instanceQuery]);
return ( return (
<> <>
@ -353,7 +354,7 @@ export default function BuildDetail() {
<PageDetail <PageDetail
title={build.reference} title={build.reference}
subtitle={build.title} subtitle={build.title}
detail={buildDetail} badges={buildBadges}
imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail} imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail}
breadcrumbs={[ breadcrumbs={[
{ name: t`Build Orders`, url: '/build' }, { name: t`Build Orders`, url: '/build' },

View File

@ -1,5 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
Badge,
Grid, Grid,
Group, Group,
LoadingOverlay, LoadingOverlay,
@ -30,11 +31,14 @@ import {
IconVersions IconVersions
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge, {
DetailsBadgeProps
} from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PartIcons } from '../../components/details/PartIcons'; import { PartIcons } from '../../components/details/PartIcons';
@ -631,15 +635,34 @@ export default function PartDetail() {
[part] [part]
); );
const partDetail = useMemo(() => { const badges: ReactNode[] = useMemo(() => {
return ( if (instanceQuery.isLoading || instanceQuery.isFetching) {
<Group spacing="xs" noWrap={true}> return [];
<Stack spacing="xs"> }
<Text>Stock: {part.in_stock}</Text>
</Stack> return [
</Group> <DetailsBadge
); label={t`In Stock` + `: ${part.in_stock}`}
}, [part, id]); color={part.in_stock >= part.minimum_stock ? 'green' : 'orange'}
visible={part.in_stock > 0}
/>,
<DetailsBadge
label={t`No Stock`}
color="red"
visible={part.in_stock == 0}
/>,
<DetailsBadge
label={t`On Order` + `: ${part.ordering}`}
color="blue"
visible={part.on_order > 0}
/>,
<DetailsBadge
label={t`In Production` + `: ${part.building}`}
color="blue"
visible={part.building > 0}
/>
];
}, [part, instanceQuery]);
const partFields = usePartFields({ create: false }); const partFields = usePartFields({ create: false });
@ -740,7 +763,7 @@ export default function PartDetail() {
title={t`Part` + ': ' + part.full_name} title={t`Part` + ': ' + part.full_name}
subtitle={part.description} subtitle={part.description}
imageUrl={part.image} imageUrl={part.image}
detail={partDetail} badges={badges}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Group, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots, IconDots,
IconInfoCircle, IconInfoCircle,
@ -8,7 +8,7 @@ import {
IconPackages, IconPackages,
IconPaperclip IconPaperclip
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -25,6 +25,7 @@ import {
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
@ -297,6 +298,18 @@ export default function PurchaseOrderDetail() {
]; ];
}, [id, order, user]); }, [id, order, user]);
const orderBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading
? []
: [
<StatusRenderer
status={order.status}
type={ModelType.purchaseorder}
options={{ size: 'lg' }}
/>
];
}, [order, instanceQuery]);
return ( return (
<> <>
{editPurchaseOrder.modal} {editPurchaseOrder.modal}
@ -308,6 +321,7 @@ export default function PurchaseOrderDetail() {
imageUrl={order.supplier_detail?.image} imageUrl={order.supplier_detail?.image}
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]} breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
actions={poActions} actions={poActions}
badges={orderBadges}
/> />
<PanelGroup pageKey="purchaseorder" panels={orderPanels} /> <PanelGroup pageKey="purchaseorder" panels={orderPanels} />
</Stack> </Stack>

View File

@ -6,7 +6,7 @@ import {
IconNotes, IconNotes,
IconPaperclip IconPaperclip
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -14,6 +14,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
@ -220,6 +221,18 @@ export default function ReturnOrderDetail() {
]; ];
}, [order, id]); }, [order, id]);
const orderBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading
? []
: [
<StatusRenderer
status={order.status}
type={ModelType.returnorder}
options={{ size: 'lg' }}
/>
];
}, [order, instanceQuery]);
return ( return (
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
@ -228,6 +241,7 @@ export default function ReturnOrderDetail() {
title={t`Return Order` + `: ${order.reference}`} title={t`Return Order` + `: ${order.reference}`}
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="returnorder" panels={orderPanels} /> <PanelGroup pageKey="returnorder" panels={orderPanels} />

View File

@ -9,7 +9,7 @@ import {
IconTruckDelivery, IconTruckDelivery,
IconTruckLoading IconTruckLoading
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
@ -17,6 +17,7 @@ import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
@ -244,6 +245,18 @@ export default function SalesOrderDetail() {
]; ];
}, [order, id]); }, [order, id]);
const orderBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading
? []
: [
<StatusRenderer
status={order.status}
type={ModelType.salesorder}
options={{ size: 'lg' }}
/>
];
}, [order, instanceQuery]);
return ( return (
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
@ -252,6 +265,7 @@ export default function SalesOrderDetail() {
title={t`Sales Order` + `: ${order.reference}`} title={t`Sales Order` + `: ${order.reference}`}
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="salesorder" panels={orderPanels} /> <PanelGroup pageKey="salesorder" panels={orderPanels} />

View File

@ -1,7 +1,9 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
Alert, Alert,
Badge,
Grid, Grid,
Group,
LoadingOverlay, LoadingOverlay,
Skeleton, Skeleton,
Stack, Stack,
@ -20,10 +22,11 @@ import {
IconPaperclip, IconPaperclip,
IconSitemap IconSitemap
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DetailsField, DetailsTable } from '../../components/details/Details'; import { DetailsField, DetailsTable } from '../../components/details/Details';
import DetailsBadge from '../../components/details/DetailsBadge';
import { DetailsImage } from '../../components/details/DetailsImage'; import { DetailsImage } from '../../components/details/DetailsImage';
import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { ItemDetailsGrid } from '../../components/details/ItemDetails';
import { import {
@ -38,6 +41,7 @@ import {
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { StockLocationTree } from '../../components/nav/StockLocationTree';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
@ -437,6 +441,33 @@ export default function StockDetail() {
[id, stockitem, user] [id, stockitem, user]
); );
const stockBadges: ReactNode[] = useMemo(() => {
return instanceQuery.isLoading
? []
: [
<DetailsBadge
color="blue"
label={t`Serial Number` + `: ${stockitem.serial}`}
visible={!!stockitem.serial}
/>,
<DetailsBadge
color="blue"
label={t`Quantity` + `: ${stockitem.quantity}`}
visible={!stockitem.serial}
/>,
<DetailsBadge
color="blue"
label={t`Batch Code` + `: ${stockitem.batch}`}
visible={!!stockitem.batch}
/>,
<StatusRenderer
status={stockitem.status}
type={ModelType.stockitem}
options={{ size: 'lg' }}
/>
];
}, [stockitem, instanceQuery]);
return ( return (
<Stack> <Stack>
<LoadingOverlay visible={instanceQuery.isFetching} /> <LoadingOverlay visible={instanceQuery.isFetching} />
@ -449,11 +480,7 @@ export default function StockDetail() {
title={t`Stock Item`} title={t`Stock Item`}
subtitle={stockitem.part_detail?.full_name} subtitle={stockitem.part_detail?.full_name}
imageUrl={stockitem.part_detail?.thumbnail} imageUrl={stockitem.part_detail?.thumbnail}
detail={ badges={stockBadges}
<Alert color="teal" title="Stock Item">
<Text>Quantity: {stockitem.quantity ?? 'idk'}</Text>
</Alert>
}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);

View File

@ -91,6 +91,7 @@ export type InvenTreeTableProps<T = any> = {
onRowClick?: (record: T, index: number, event: any) => void; onRowClick?: (record: T, index: number, event: any) => void;
onCellClick?: DataTableCellClickHandler<T>; onCellClick?: DataTableCellClickHandler<T>;
modelType?: ModelType; modelType?: ModelType;
modelField?: string;
}; };
/** /**
@ -515,18 +516,22 @@ export function InvenTreeTable<T = any>({
if (props.onRowClick) { if (props.onRowClick) {
// If a custom row click handler is provided, use that // If a custom row click handler is provided, use that
props.onRowClick(record, index, event); props.onRowClick(record, index, event);
} else if (tableProps.modelType && record?.pk) { } else if (tableProps.modelType) {
// If a model type is provided, navigate to the detail view for that model const pk = record?.[tableProps.modelField ?? 'pk'];
let url = getDetailUrl(tableProps.modelType, record.pk);
// Should it be opened in a new tab? if (pk) {
if (event?.ctrlKey || event?.shiftKey) { // If a model type is provided, navigate to the detail view for that model
// Open in a new tab let url = getDetailUrl(tableProps.modelType, pk);
url = `/${base_url}${url}`;
window.open(url, '_blank'); // Should it be opened in a new tab?
} else { if (event?.ctrlKey || event?.shiftKey) {
// Navigate internally // Open in a new tab
navigate(url); url = `/${base_url}${url}`;
window.open(url, '_blank');
} else {
// Navigate internally
navigate(url);
}
} }
} }
}, },

View File

@ -83,7 +83,8 @@ export function UsedInTable({
sub_part_detail: true sub_part_detail: true
}, },
tableFilters: tableFilters, tableFilters: tableFilters,
modelType: ModelType.part modelType: ModelType.part,
modelField: 'part'
}} }}
/> />
); );

View File

@ -234,6 +234,10 @@ export default function ParametricPartTable({
{ {
accessor: 'IPN', accessor: 'IPN',
sortable: true sortable: true
},
{
accessor: 'total_in_stock',
sortable: true
} }
]; ];