diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index 778bbdf4b5..2161a3a4dd 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -12,14 +12,15 @@ import { Tooltip } from '@mantine/core'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { Suspense, useMemo } from 'react'; +import { Suspense, useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { api } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; +import { navigateToLink } from '../../functions/navigation'; import { getDetailUrl } from '../../functions/urls'; -import { base_url } from '../../main'; import { apiUrl } from '../../states/ApiState'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { YesNoButton } from '../buttons/YesNoButton'; @@ -183,7 +184,7 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) { function TableStringValue(props: Readonly) { let value = props?.field_value; - if (props.field_data?.value_formatter) { + if (props?.field_data?.value_formatter) { value = props.field_data.value_formatter(); } @@ -222,24 +223,15 @@ function BooleanValue(props: Readonly) { } function TableAnchorValue(props: Readonly) { - if (props.field_data.external) { - return ( - - - {props.field_value} - - - - ); - } + const navigate = useNavigate(); const { data } = useSuspenseQuery({ queryKey: ['detail', props.field_data.model, props.field_value], queryFn: async () => { + if (!props.field_data?.model) { + return {}; + } + const modelDef = getModelInfo(props.field_data.model); if (!modelDef?.api_endpoint) { @@ -255,19 +247,44 @@ function TableAnchorValue(props: Readonly) { case 200: return response.data; default: - return null; + return {}; } }) .catch(() => { - return null; + return {}; }); } }); const detailUrl = useMemo(() => { - return getDetailUrl(props.field_data.model, props.field_value); + return ( + props?.field_data?.model && + getDetailUrl(props.field_data.model, props.field_value) + ); }, [props.field_data.model, props.field_value]); + const handleLinkClick = useCallback( + (event: any) => { + navigateToLink(detailUrl, navigate, event); + }, + [detailUrl] + ); + + if (props.field_data.external) { + return ( + + + {props.field_value} + + + + ); + } + let make_link = props.field_data?.link ?? true; // Construct the "return value" for the fetched data @@ -282,18 +299,14 @@ function TableAnchorValue(props: Readonly) { } if (value === undefined) { - value = data?.name ?? props.field_data?.backup_value ?? 'No name defined'; + value = data?.name ?? props.field_data?.backup_value ?? t`No name defined`; make_link = false; } return ( }> {make_link ? ( - + {value} ) : ( @@ -370,25 +383,25 @@ export function DetailsTableField({ const FieldType: any = getFieldType(field.type); return ( - - + - - + + {field.label} - - + + - - + + {field.copy && } - - + + ); } @@ -405,19 +418,14 @@ export function DetailsTable({ {title && {title}} - - +
+ {fields .filter((field: DetailsField) => !field.hidden) .map((field: DetailsField, index: number) => ( ))} - +
diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index a5812223fb..6484003eb1 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -163,6 +163,7 @@ const icons = { link: IconLink, responsible: IconUserStar, pricing: IconCurrencyDollar, + total_price: IconCurrencyDollar, currency: IconCurrencyDollar, stocktake: IconClipboardList, user: IconUser, diff --git a/src/frontend/src/functions/navigation.tsx b/src/frontend/src/functions/navigation.tsx new file mode 100644 index 0000000000..29cbdba376 --- /dev/null +++ b/src/frontend/src/functions/navigation.tsx @@ -0,0 +1,20 @@ +import { base_url } from '../main'; +import { cancelEvent } from './events'; + +/* + * Navigate to a provided link. + * - If the link is to be opened externally, open it in a new tab. + * - Otherwise, navigate using the provided navigate function. + */ +export const navigateToLink = (link: string, navigate: any, event: any) => { + cancelEvent(event); + + if (event?.ctrlKey || event?.shiftKey) { + // Open the link in a new tab + const url = `/${base_url}${link}`; + window.open(url, '_blank'); + } else { + // Navigate internally + navigate(link); + } +}; diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index ad74f2b959..9db7350807 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -102,7 +102,8 @@ export default function CategoryDetail({}: {}) { type: 'text', name: 'part_count', label: t`Parts`, - icon: 'part' + icon: 'part', + value_formatter: () => category?.part_count || '0' }, { type: 'text', @@ -233,7 +234,7 @@ export default function CategoryDetail({}: {}) { /> {category.name ?? 'Top level'}} + subtitle={category?.name} breadcrumbs={breadcrumbs} breadcrumbAction={() => { setTreeOpen(true); diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index b22314a632..26c11e63b6 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -28,6 +28,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; +import { formatCurrency } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; @@ -151,18 +152,23 @@ export default function PurchaseOrderDetail() { label: t`Completed Shipments`, total: order.shipments, progress: order.completed_shipments - // TODO: Fix this progress bar }, { type: 'text', name: 'currency', - label: t`Order Currency,` + label: t`Order Currency`, + value_formatter: () => + order?.order_currency ?? order?.supplier_detail?.currency }, { type: 'text', - name: 'total_cost', - label: t`Total Cost` - // TODO: Implement this! + name: 'total_price', + label: t`Total Cost`, + value_formatter: () => { + return formatCurrency(order?.total_price, { + currency: order?.order_currency ?? order?.supplier_detail?.currency + }); + } } ]; diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 290683545f..3fe03e7d00 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -23,6 +23,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; +import { formatCurrency } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; @@ -123,13 +124,19 @@ export default function ReturnOrderDetail() { { type: 'text', name: 'currency', - label: t`Order Currency,` + label: t`Order Currency`, + value_formatter: () => + order?.order_currency ?? order?.customer_detail?.currency }, { type: 'text', - name: 'total_cost', - label: t`Total Cost` - // TODO: Implement this! + name: 'total_price', + label: t`Total Cost`, + value_formatter: () => { + return formatCurrency(order?.total_price, { + currency: order?.order_currency ?? order?.customer_detail?.currency + }); + } } ]; diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 833dbfb11e..bdd01d04f4 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -26,6 +26,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; +import { formatCurrency } from '../../defaults/formatters'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; @@ -127,13 +128,19 @@ export default function SalesOrderDetail() { { type: 'text', name: 'currency', - label: t`Order Currency,` + label: t`Order Currency`, + value_formatter: () => + order?.order_currency ?? order?.customer_detail.currency }, { type: 'text', - name: 'total_cost', - label: t`Total Cost` - // TODO: Implement this! + name: 'total_price', + label: t`Total Cost`, + value_formatter: () => { + return formatCurrency(order?.total_price, { + currency: order?.order_currency ?? order?.customer_detail?.currency + }); + } } ]; diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index c96630c90b..9041d6b5d9 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -108,7 +108,8 @@ export default function Stock() { type: 'text', name: 'items', icon: 'stock', - label: t`Stock Items` + label: t`Stock Items`, + value_formatter: () => location?.items || '0' }, { type: 'text', @@ -311,7 +312,7 @@ export default function Stock() { /> {location.name ?? 'Top level'}} + subtitle={location?.name} actions={locationActions} breadcrumbs={breadcrumbs} breadcrumbAction={() => { diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 4a36cb62a5..28208f1d8a 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -25,7 +25,13 @@ import { DataTableCellClickHandler, DataTableSortStatus } from 'mantine-datatable'; -import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + Fragment, + useCallback, + useEffect, + useMemo, + useState +} from 'react'; import { useNavigate } from 'react-router-dom'; import { api } from '../App'; @@ -35,7 +41,9 @@ import { ButtonMenu } from '../components/buttons/ButtonMenu'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ModelType } from '../enums/ModelType'; import { resolveItem } from '../functions/conversion'; +import { cancelEvent } from '../functions/events'; import { extractAvailableFields, mapFields } from '../functions/forms'; +import { navigateToLink } from '../functions/navigation'; import { getDetailUrl } from '../functions/urls'; import { TableState } from '../hooks/UseTable'; import { base_url } from '../main'; @@ -525,7 +533,17 @@ export function InvenTreeTable({ // Callback when a row is clicked const handleRowClick = useCallback( - ({ event, record, index }: { event: any; record: any; index: number }) => { + ({ + event, + record, + index + }: { + event: React.MouseEvent; + record: any; + index: number; + }) => { + cancelEvent(event); + if (props.onRowClick) { // If a custom row click handler is provided, use that props.onRowClick(record, index, event); @@ -536,16 +554,7 @@ export function InvenTreeTable({ if (pk) { // If a model type is provided, navigate to the detail view for that model let url = getDetailUrl(tableProps.modelType, pk); - - // Should it be opened in a new tab? - if (event?.ctrlKey || event?.shiftKey) { - // Open in a new tab - url = `/${base_url}${url}`; - window.open(url, '_blank'); - } else { - // Navigate internally - navigate(url); - } + navigateToLink(url, navigate, event); } } }, diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index b90895a1e8..5fa5ef2120 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -419,6 +419,7 @@ export function BomTable({ tableActions: tableActions, tableFilters: tableFilters, modelType: ModelType.part, + modelField: 'sub_part', rowActions: rowActions }} />