Fix for details links (#7184)

* Fix for details URl

- Do not open as a new link
- Instead, use internal 'navigate'
- Otherwise, triggers a login sequence again
- Major improvement in workflow

* Fix InvenTreeTable

* Refactor

* Handle case where no model available

* Fix default return type

* Use proper mantine table components

* Fix for BomTable click-through

* Details tweaks

* Fix labels

* Implement total price detail

* Cleanup

* Rendering tweaks

* Fix for Details.tsx
This commit is contained in:
Oliver 2024-05-09 12:15:07 +10:00 committed by GitHub
parent 770dbb9c35
commit db1a2f9015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 136 additions and 75 deletions

View File

@ -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<FieldProps>) {
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<FieldProps>) {
}
function TableAnchorValue(props: Readonly<FieldProps>) {
if (props.field_data.external) {
return (
<Anchor
href={`${props.field_value}`}
target={'_blank'}
rel={'noreferrer noopener'}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
<Text>{props.field_value}</Text>
<InvenTreeIcon icon="external" iconProps={{ size: 15 }} />
</span>
</Anchor>
);
}
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<FieldProps>) {
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 (
<Anchor
href={`${props.field_value}`}
target={'_blank'}
rel={'noreferrer noopener'}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
<Text>{props.field_value}</Text>
<InvenTreeIcon icon="external" iconProps={{ size: 15 }} />
</span>
</Anchor>
);
}
let make_link = props.field_data?.link ?? true;
// Construct the "return value" for the fetched data
@ -282,18 +299,14 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
}
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 (
<Suspense fallback={<Skeleton width={200} height={20} radius="xl" />}>
{make_link ? (
<Anchor
href={`/${base_url}${detailUrl}`}
target={data?.external ? '_blank' : undefined}
rel={data?.external ? 'noreferrer noopener' : undefined}
>
<Anchor href="#" onClick={handleLinkClick}>
<Text>{value}</Text>
</Anchor>
) : (
@ -370,25 +383,25 @@ export function DetailsTableField({
const FieldType: any = getFieldType(field.type);
return (
<tr style={{ verticalAlign: 'top' }}>
<td
<Table.Tr style={{ verticalAlign: 'top' }}>
<Table.Td
style={{
gap: '20px',
width: '50'
width: '50',
maxWidth: '50'
}}
>
<InvenTreeIcon icon={(field.icon ?? field.name) as InvenTreeIconType} />
</td>
<td style={{ minWidth: '25%', maxWidth: '65%' }}>
</Table.Td>
<Table.Td style={{ maxWidth: '65%' }}>
<Text>{field.label}</Text>
</td>
<td style={{ width: '100%' }}>
</Table.Td>
<Table.Td style={{}}>
<FieldType field_data={field} field_value={item[field.name]} />
</td>
<td style={{ width: '50' }}>
</Table.Td>
<Table.Td style={{ width: '50' }}>
{field.copy && <CopyField value={item[field.name]} />}
</td>
</tr>
</Table.Td>
</Table.Tr>
);
}
@ -405,19 +418,14 @@ export function DetailsTable({
<Paper p="xs" withBorder radius="xs">
<Stack gap="xs">
{title && <StylishText size="lg">{title}</StylishText>}
<Table
striped
verticalSpacing="sm"
horizontalSpacing="md"
withColumnBorders
>
<tbody>
<Table striped verticalSpacing={5} horizontalSpacing="sm">
<Table.Tbody>
{fields
.filter((field: DetailsField) => !field.hidden)
.map((field: DetailsField, index: number) => (
<DetailsTableField field={field} item={item} key={index} />
))}
</tbody>
</Table.Tbody>
</Table>
</Stack>
</Paper>

View File

@ -163,6 +163,7 @@ const icons = {
link: IconLink,
responsible: IconUserStar,
pricing: IconCurrencyDollar,
total_price: IconCurrencyDollar,
currency: IconCurrencyDollar,
stocktake: IconClipboardList,
user: IconUser,

View File

@ -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);
}
};

View File

@ -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({}: {}) {
/>
<PageDetail
title={t`Part Category`}
detail={<Text>{category.name ?? 'Top level'}</Text>}
subtitle={category?.name}
breadcrumbs={breadcrumbs}
breadcrumbAction={() => {
setTreeOpen(true);

View File

@ -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
});
}
}
];

View File

@ -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
});
}
}
];

View File

@ -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
});
}
}
];

View File

@ -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() {
/>
<PageDetail
title={t`Stock Items`}
detail={<Text>{location.name ?? 'Top level'}</Text>}
subtitle={location?.name}
actions={locationActions}
breadcrumbs={breadcrumbs}
breadcrumbAction={() => {

View File

@ -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<T = any>({
// 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<T = any>({
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);
}
}
},

View File

@ -419,6 +419,7 @@ export function BomTable({
tableActions: tableActions,
tableFilters: tableFilters,
modelType: ModelType.part,
modelField: 'sub_part',
rowActions: rowActions
}}
/>