mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 03:55:41 +00:00
[React] Update part parameters table (#5731)
* Implement simple "PartVariantTable" component - Not yet nested - More work needed for table nesting * Fix issue rendering same image multiple times - Use useId hook to generate random key * Update PartParameter list API endpoint - Allow part_detail extra field - Add FilterSet class - Allow filter to include variants * Update PartParameterTable - Display part column - Allow returned parts to include templates of base part - Hide actions for templated parameters * Fix some code smells
This commit is contained in:
@ -11,8 +11,9 @@ import {
|
||||
Overlay,
|
||||
Stack
|
||||
} from '@mantine/core';
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
|
||||
@ -22,8 +23,10 @@ import { api } from '../../App';
|
||||
export function ApiImage(props: ImageProps) {
|
||||
const [image, setImage] = useState<string>('');
|
||||
|
||||
const queryKey = useId();
|
||||
|
||||
const imgQuery = useQuery({
|
||||
queryKey: ['image', props.src],
|
||||
queryKey: ['image', queryKey, props.src],
|
||||
enabled: props.src != undefined && props.src != null && props.src != '',
|
||||
queryFn: async () => {
|
||||
if (!props.src) {
|
||||
@ -47,7 +50,7 @@ export function ApiImage(props: ImageProps) {
|
||||
});
|
||||
},
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -11,7 +11,7 @@ export function Thumbnail({
|
||||
alt = t`Thumbnail`,
|
||||
size = 20
|
||||
}: {
|
||||
src: string;
|
||||
src?: string | undefined;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
}) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Text, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Group, Text, Tooltip } from '@mantine/core';
|
||||
import { IconTextPlus } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '../../../functions/forms';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
import { Thumbnail } from '../../images/Thumbnail';
|
||||
import { YesNoButton } from '../../items/YesNoButton';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
@ -22,12 +23,36 @@ export function PartParameterTable({ partId }: { partId: any }) {
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'part',
|
||||
title: t`Part`,
|
||||
switchable: true,
|
||||
sortable: true,
|
||||
render: function (record: any) {
|
||||
let part = record?.part_detail ?? {};
|
||||
|
||||
return (
|
||||
<Group spacing="xs" align="left" noWrap={true}>
|
||||
<Thumbnail
|
||||
src={part?.thumbnail || part?.image}
|
||||
alt={part?.name}
|
||||
size={24}
|
||||
/>
|
||||
<Text>{part?.full_name}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Parameter`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record) => record.template_detail?.name
|
||||
render: (record) => {
|
||||
let variant = String(partId) != String(record.part);
|
||||
|
||||
return <Text italic={variant}>{record.template_detail?.name}</Text>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'description',
|
||||
@ -65,54 +90,62 @@ export function PartParameterTable({ partId }: { partId: any }) {
|
||||
render: (record) => record.template_detail?.units
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [partId]);
|
||||
|
||||
// Callback for row actions
|
||||
// TODO: Adjust based on user permissions
|
||||
const rowActions = useCallback((record: any) => {
|
||||
let actions = [];
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
// Actions not allowed for "variant" rows
|
||||
if (String(partId) != String(record.part)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
actions.push({
|
||||
title: t`Edit`,
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
name: 'edit-part-parameter',
|
||||
url: ApiPaths.part_parameter_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit Part Parameter`,
|
||||
fields: {
|
||||
part: {
|
||||
hidden: true
|
||||
let actions = [];
|
||||
|
||||
actions.push({
|
||||
title: t`Edit`,
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
name: 'edit-part-parameter',
|
||||
url: ApiPaths.part_parameter_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit Part Parameter`,
|
||||
fields: {
|
||||
part: {
|
||||
hidden: true
|
||||
},
|
||||
template: {},
|
||||
data: {}
|
||||
},
|
||||
template: {},
|
||||
data: {}
|
||||
},
|
||||
successMessage: t`Part parameter updated`,
|
||||
onFormSuccess: refreshTable
|
||||
});
|
||||
}
|
||||
});
|
||||
successMessage: t`Part parameter updated`,
|
||||
onFormSuccess: refreshTable
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
actions.push({
|
||||
title: t`Delete`,
|
||||
color: 'red',
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
name: 'delete-part-parameter',
|
||||
url: ApiPaths.part_parameter_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete Part Parameter`,
|
||||
successMessage: t`Part parameter deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove this parameter?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
actions.push({
|
||||
title: t`Delete`,
|
||||
color: 'red',
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
name: 'delete-part-parameter',
|
||||
url: ApiPaths.part_parameter_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete Part Parameter`,
|
||||
successMessage: t`Part parameter deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove this parameter?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
return actions;
|
||||
},
|
||||
[partId]
|
||||
);
|
||||
|
||||
const addParameter = useCallback(() => {
|
||||
if (!partId) {
|
||||
@ -160,9 +193,17 @@ export function PartParameterTable({ partId }: { partId: any }) {
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
customActionGroups: tableActions,
|
||||
customFilters: [
|
||||
{
|
||||
name: 'include_variants',
|
||||
label: t`Include Variants`,
|
||||
type: 'boolean'
|
||||
}
|
||||
],
|
||||
params: {
|
||||
part: partId,
|
||||
template_detail: true
|
||||
template_detail: true,
|
||||
part_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -3,8 +3,6 @@ import { Group, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { editPart } from '../../../functions/forms/PartForms';
|
||||
import { notYetImplemented } from '../../../functions/notifications';
|
||||
import { shortenString } from '../../../functions/tables';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
@ -12,7 +10,6 @@ import { Thumbnail } from '../../images/Thumbnail';
|
||||
import { TableColumn } from '../Column';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
* Construct a list of columns for the part table
|
||||
@ -193,33 +190,6 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
|
||||
const { tableKey, refreshTable } = useTableRefresh('part');
|
||||
|
||||
// Callback function for generating set of row actions
|
||||
function partTableRowActions(record: any): RowAction[] {
|
||||
let actions: RowAction[] = [];
|
||||
|
||||
actions.push({
|
||||
title: t`Edit`,
|
||||
onClick: () => {
|
||||
editPart({
|
||||
part_id: record.pk,
|
||||
callback: () => {
|
||||
// TODO: Reload the table, somehow?
|
||||
notYetImplemented();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
actions.push({
|
||||
title: t`Detail`,
|
||||
onClick: () => {
|
||||
navigate(`/part/${record.pk}/`);
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@ -231,10 +201,12 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
...props,
|
||||
enableDownload: true,
|
||||
customFilters: tableFilters,
|
||||
rowActions: partTableRowActions,
|
||||
params: {
|
||||
...props.params,
|
||||
category_detail: true
|
||||
},
|
||||
onRowClick: (record, _index, _event) => {
|
||||
navigate(`/part/${record.pk}/`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
18
src/frontend/src/components/tables/part/PartVariantTable.tsx
Normal file
18
src/frontend/src/components/tables/part/PartVariantTable.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { PartListTable } from './PartTable';
|
||||
|
||||
/**
|
||||
* Display variant parts for thespecified parent part
|
||||
*/
|
||||
export function PartVariantTable({ partId }: { partId: string }) {
|
||||
return (
|
||||
<PartListTable
|
||||
props={{
|
||||
enableDownload: false,
|
||||
customFilters: [],
|
||||
params: {
|
||||
ancestor: partId
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -30,7 +30,7 @@ function stockItemTableColumns(): TableColumn[] {
|
||||
alt={part?.name}
|
||||
size={24}
|
||||
/>
|
||||
<Text>{part.full_name}</Text>
|
||||
<Text>{part?.full_name}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,11 @@ export function shortenString({
|
||||
str,
|
||||
len = 100
|
||||
}: {
|
||||
str: string;
|
||||
str: string | undefined;
|
||||
len?: number;
|
||||
}) {
|
||||
// Ensure that the string is a string
|
||||
str = str ?? '';
|
||||
str = str.toString();
|
||||
|
||||
// If the string is already short enough, return it
|
||||
|
@ -33,6 +33,7 @@ import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||
import { PartParameterTable } from '../../components/tables/part/PartParameterTable';
|
||||
import { PartVariantTable } from '../../components/tables/part/PartVariantTable';
|
||||
import { RelatedPartTable } from '../../components/tables/part/RelatedPartTable';
|
||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
|
||||
@ -56,8 +57,7 @@ export default function PartDetail() {
|
||||
params: {
|
||||
path_detail: true
|
||||
},
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
// Part data panels (recalculate when part data changes)
|
||||
@ -92,7 +92,7 @@ export default function PartDetail() {
|
||||
label: t`Variants`,
|
||||
icon: <IconVersions />,
|
||||
hidden: !part.is_template,
|
||||
content: <PlaceholderPanel />
|
||||
content: <PartVariantTable partId={String(id)} />
|
||||
},
|
||||
{
|
||||
name: 'bom',
|
||||
|
Reference in New Issue
Block a user