mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
770dbb9c35
commit
db1a2f9015
@ -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>
|
||||
|
@ -163,6 +163,7 @@ const icons = {
|
||||
link: IconLink,
|
||||
responsible: IconUserStar,
|
||||
pricing: IconCurrencyDollar,
|
||||
total_price: IconCurrencyDollar,
|
||||
currency: IconCurrencyDollar,
|
||||
stocktake: IconClipboardList,
|
||||
user: IconUser,
|
||||
|
20
src/frontend/src/functions/navigation.tsx
Normal file
20
src/frontend/src/functions/navigation.tsx
Normal 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);
|
||||
}
|
||||
};
|
@ -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);
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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={() => {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -419,6 +419,7 @@ export function BomTable({
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
modelType: ModelType.part,
|
||||
modelField: 'sub_part',
|
||||
rowActions: rowActions
|
||||
}}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user