2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-12-16 17:28:11 +00:00

Make parametric table generic

This commit is contained in:
Oliver Walters
2025-11-25 03:26:18 +00:00
parent 262bbd5cf3
commit a65df8b1cb
8 changed files with 442 additions and 486 deletions

View File

@@ -111,8 +111,6 @@ export enum ApiEndpoints {
// Part API endpoints // Part API endpoints
part_list = 'part/', part_list = 'part/',
part_parameter_list = 'part/parameter/',
part_parameter_template_list = 'part/parameter/template/',
part_thumbs_list = 'part/thumbs/', part_thumbs_list = 'part/thumbs/',
part_pricing = 'part/:id/pricing/', part_pricing = 'part/:id/pricing/',
part_requirements = 'part/:id/requirements/', part_requirements = 'part/:id/requirements/',

View File

@@ -33,18 +33,18 @@ export const ModelInformationDict: ModelDict = {
admin_url: '/part/part/', admin_url: '/part/part/',
icon: 'part' icon: 'part'
}, },
parameter: {
label: () => t`Parameter`,
label_multiple: () => t`Parameters`,
api_endpoint: ApiEndpoints.parameter_list,
icon: 'list_details'
},
parametertemplate: { parametertemplate: {
label: () => t`Parameter Template`, label: () => t`Parameter Template`,
label_multiple: () => t`Parameter Templates`, label_multiple: () => t`Parameter Templates`,
api_endpoint: ApiEndpoints.parameter_template_list api_endpoint: ApiEndpoints.parameter_template_list,
}, admin_url: '/common/parametertemplate/',
partparametertemplate: { icon: 'list'
label: () => t`Part Parameter Template`,
label_multiple: () => t`Part Parameter Templates`,
url_overview: '/settings/admin/part-parameters',
url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiEndpoints.part_parameter_template_list,
icon: 'test_templates'
}, },
parttesttemplate: { parttesttemplate: {
label: () => t`Part Test Template`, label: () => t`Part Test Template`,

View File

@@ -6,7 +6,6 @@ export enum ModelType {
supplierpart = 'supplierpart', supplierpart = 'supplierpart',
manufacturerpart = 'manufacturerpart', manufacturerpart = 'manufacturerpart',
partcategory = 'partcategory', partcategory = 'partcategory',
partparametertemplate = 'partparametertemplate',
parttesttemplate = 'parttesttemplate', parttesttemplate = 'parttesttemplate',
projectcode = 'projectcode', projectcode = 'projectcode',
stockitem = 'stockitem', stockitem = 'stockitem',

View File

@@ -1,11 +1,7 @@
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react'; import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { useApi } from '../contexts/ApiContext';
import { useGlobalSettingsState } from '../states/SettingsStates'; import { useGlobalSettingsState } from '../states/SettingsStates';
/** /**
@@ -224,97 +220,6 @@ export function partCategoryFields({
return fields; return fields;
} }
export function usePartParameterFields({
editTemplate
}: {
editTemplate?: boolean;
}): ApiFormFieldSet {
const api = useApi();
// Valid field choices
const [choices, setChoices] = useState<any[]>([]);
// Field type for "data" input
const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>(
'string'
);
return useMemo(() => {
return {
part: {
disabled: true
},
template: {
disabled: editTemplate == false,
onValueChange: (value: any, record: any) => {
// Adjust the type of the "data" field based on the selected template
if (record?.checkbox) {
// This is a "checkbox" field
setChoices([]);
setFieldType('boolean');
} else if (record?.choices) {
const _choices: string[] = record.choices.split(',');
if (_choices.length > 0) {
setChoices(
_choices.map((choice) => {
return {
display_name: choice.trim(),
value: choice.trim()
};
})
);
setFieldType('choice');
} else {
setChoices([]);
setFieldType('string');
}
} else if (record?.selectionlist) {
api
.get(
apiUrl(ApiEndpoints.selectionlist_detail, record.selectionlist)
)
.then((res) => {
setChoices(
res.data.choices.map((item: any) => {
return {
value: item.value,
display_name: item.label
};
})
);
setFieldType('choice');
});
} else {
setChoices([]);
setFieldType('string');
}
}
},
data: {
type: fieldType,
field_type: fieldType,
choices: fieldType === 'choice' ? choices : undefined,
default: fieldType === 'boolean' ? false : undefined,
adjustValue: (value: any) => {
// Coerce boolean value into a string (required by backend)
let v: string = value.toString().trim();
if (fieldType === 'boolean') {
if (v.toLowerCase() !== 'true') {
v = 'false';
}
}
return v;
}
},
note: {}
};
}, [editTemplate, fieldType, choices]);
}
export function partStocktakeFields(): ApiFormFieldSet { export function partStocktakeFields(): ApiFormFieldSet {
return { return {
part: { part: {

View File

@@ -312,7 +312,11 @@ export default function CategoryDetail() {
name: 'parameters', name: 'parameters',
label: t`Part Parameters`, label: t`Part Parameters`,
icon: <IconListDetails />, icon: <IconListDetails />,
content: <ParametricPartTable categoryId={id} /> content: (
<>
<ParametricPartTable categoryId={id} />
</>
)
} }
], ],
[category, id] [category, id]

View File

@@ -0,0 +1,409 @@
import { cancelEvent } from '@lib/functions/Events';
import {
ApiEndpoints,
type ApiFormFieldSet,
ModelType,
UserRoles,
YesNoButton,
apiUrl,
formatDecimal,
getDetailUrl,
navigateToLink
} from '@lib/index';
import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { Group } from '@mantine/core';
import { useHover } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useApi } from '../../contexts/ApiContext';
import { useParameterFields } from '../../forms/CommonForms';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
import {
PARAMETER_FILTER_OPERATORS,
ParameterFilter
} from './ParametricDataTableFilters';
// Render an individual parameter cell
function ParameterCell({
record,
template,
canEdit
}: Readonly<{
record: any;
template: any;
canEdit: boolean;
}>) {
const { hovered, ref } = useHover();
// Find matching template parameter
const parameter = useMemo(() => {
return record.parameters?.find((p: any) => p.template == template.pk);
}, [record, template]);
const extra: any[] = [];
// Format the value for display
const value: ReactNode = useMemo(() => {
let v: any = parameter?.data;
// Handle boolean values
if (template?.checkbox && v != undefined) {
v = <YesNoButton value={parameter.data} />;
}
return v;
}, [parameter, template]);
if (
template.units &&
parameter &&
parameter.data_numeric &&
parameter.data_numeric != parameter.data
) {
const numeric = formatDecimal(parameter.data_numeric, { digits: 15 });
extra.push(`${numeric} [${template.units}]`);
}
if (hovered && canEdit) {
extra.push(t`Click to edit`);
}
return (
<div>
<Group grow ref={ref} justify='space-between'>
<Group grow>
<TableHoverCard
value={value ?? '-'}
extra={extra}
icon={hovered && canEdit ? 'edit' : 'info'}
title={template.name}
/>
</Group>
</Group>
</div>
);
}
/**
* A table which displays parametric data for generic model types.
* The table can be extended by passing in additional column, filters, and actions.
*/
export default function ParametricDataTable({
modelType,
endpoint,
queryParams,
customFilters,
customColumns
}: {
modelType: ModelType;
endpoint: ApiEndpoints | string;
queryParams?: Record<string, any>;
customFilters?: TableFilter[];
customColumns?: TableColumn[];
}) {
const api = useApi();
const table = useTable(`parametric-data-${modelType}`);
const user = useUserState();
const navigate = useNavigate();
// Fetch all active parameter templates for the given model type
const parameterTemplates = useQuery({
queryKey: ['parameter-templates', modelType],
queryFn: async () => {
return api
.get(apiUrl(ApiEndpoints.parameter_template_list), {
params: {
active: true,
for_model: modelType
}
})
.then((response) => response.data);
},
refetchOnMount: true
});
/* Store filters against selected part parameters.
* These are stored in the format:
* {
* parameter_1: {
* '=': 'value1',
* '<': 'value2',
* ...
* },
* parameter_2: {
* '=': 'value3',
* },
* ...
* }
*
* Which allows multiple filters to be applied against each parameter template.
*/
const [parameterFilters, setParameterFilters] = useState<any>({});
/* Remove filters for a specific parameter template
* - If no operator is specified, remove all filters for this template
* - If an operator is specified, remove filters for that operator only
*/
const clearParameterFilter = useCallback(
(templateId: number, operator?: string) => {
const filterName = `parameter_${templateId}`;
if (!operator) {
// If no operator is specified, remove all filters for this template
setParameterFilters((prev: any) => {
const newFilters = { ...prev };
// Remove any filters that match the template ID
Object.keys(newFilters).forEach((key: string) => {
if (key == filterName) {
delete newFilters[key];
}
});
return newFilters;
});
return;
}
// An operator is specified, so we remove filters for that operator only
setParameterFilters((prev: any) => {
const filters = { ...prev };
const paramFilters = filters[filterName] || {};
if (paramFilters[operator] !== undefined) {
// Remove the specific operator filter
delete paramFilters[operator];
}
return {
...filters,
[filterName]: paramFilters
};
});
table.refreshTable();
},
[setParameterFilters, table.refreshTable]
);
/**
* Add (or update) a filter for a specific parameter template.
* @param templateId - The ID of the parameter template to filter on.
* @param value - The value to filter by.
* @param operator - The operator to use for filtering (e.g., '=', '<', '>', etc.).
*/
const addParameterFilter = useCallback(
(templateId: number, value: string, operator: string) => {
const filterName = `parameter_${templateId}`;
const filterValue = value?.toString().trim() ?? '';
if (filterValue.length > 0) {
setParameterFilters((prev: any) => {
const filters = { ...prev };
const paramFilters = filters[filterName] || {};
paramFilters[operator] = filterValue;
return {
...filters,
[filterName]: paramFilters
};
});
table.refreshTable();
}
},
[setParameterFilters, clearParameterFilter, table.refreshTable]
);
// Construct the query filters for the table based on the parameter filters
const parametricQueryFilters = useMemo(() => {
const filters: Record<string, string> = {};
Object.keys(parameterFilters).forEach((key: string) => {
const paramFilters: any = parameterFilters[key];
Object.keys(paramFilters).forEach((operator: string) => {
const name = `${key}${PARAMETER_FILTER_OPERATORS[operator] || ''}`;
const value = paramFilters[operator];
filters[name] = value;
});
});
return filters;
}, [parameterFilters]);
const [selectedInstance, setSelectedInstance] = useState<number>(-1);
const [selectedTemplate, setSelectedTemplate] = useState<number>(-1);
const [selectedParameter, setSelectedParameter] = useState<number>(-1);
const parameterFields: ApiFormFieldSet = useParameterFields({
modelType: ModelType.part,
modelId: selectedInstance
});
const addParameter = useCreateApiFormModal({
url: ApiEndpoints.parameter_list,
title: t`Add Parameter`,
fields: useMemo(() => ({ ...parameterFields }), [parameterFields]),
focus: 'data',
onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedInstance, parameter);
},
initialData: {
part: selectedInstance,
template: selectedTemplate
}
});
const editParameter = useEditApiFormModal({
url: ApiEndpoints.parameter_list,
title: t`Edit Parameter`,
pk: selectedParameter,
fields: useMemo(() => ({ ...parameterFields }), [parameterFields]),
focus: 'data',
onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedInstance, parameter);
}
});
// Update a single parameter record in the table
const updateParameterRecord = useCallback(
(part: number, parameter: any) => {
const records = table.records;
const recordIndex = records.findIndex((record: any) => record.pk == part);
if (recordIndex < 0) {
// No matching part: reload the entire table
table.refreshTable();
return;
}
const parameterIndex = records[recordIndex].parameters.findIndex(
(p: any) => p.pk == parameter.pk
);
if (parameterIndex < 0) {
// No matching parameter - append new parameter
records[recordIndex].parameters.push(parameter);
} else {
records[recordIndex].parameters[parameterIndex] = parameter;
}
table.updateRecord(records[recordIndex]);
},
[table.records, table.updateRecord]
);
const parameterColumns: TableColumn[] = useMemo(() => {
const data = parameterTemplates?.data || [];
return data.map((template: any) => {
let title = template.name;
if (template.units) {
title += ` [${template.units}]`;
}
const filters = parameterFilters[`parameter_${template.pk}`] || {};
return {
accessor: `parameter_${template.pk}`,
title: title,
sortable: true,
extra: {
template: template.pk
},
render: (record: any) => (
<ParameterCell
record={record}
template={template}
canEdit={user.hasChangeRole(UserRoles.part)}
/>
),
filtering: Object.keys(filters).length > 0,
filter: ({ close }: { close: () => void }) => {
return (
<ParameterFilter
template={template}
filters={parameterFilters[`parameter_${template.pk}`] || {}}
setFilter={addParameterFilter}
clearFilter={clearParameterFilter}
closeFilter={close}
/>
);
}
};
});
}, [user, parameterTemplates.data, parameterFilters]);
// Callback function when a parameter cell is clicked
const onParameterClick = useCallback((template: number, instance: any) => {
setSelectedTemplate(template);
setSelectedInstance(instance.pk);
const parameter = instance.parameters?.find(
(p: any) => p.template == template
);
if (parameter) {
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
addParameter.open();
}
}, []);
const tableFilters: TableFilter[] = useMemo(() => {
return [...(customFilters || [])];
}, [customFilters]);
const tableColumns: TableColumn[] = useMemo(() => {
return [...(customColumns || []), ...parameterColumns];
}, [customColumns, parameterColumns]);
return (
<>
{addParameter.modal}
{editParameter.modal}
<InvenTreeTable
url={apiUrl(endpoint)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: true,
tableFilters: tableFilters,
params: {
...queryParams,
parameters: true,
...parametricQueryFilters
},
onCellClick: ({ event, record, index, column, columnIndex }) => {
cancelEvent(event);
// Is this a "parameter" cell?
if (column?.accessor?.toString()?.startsWith('parameter_')) {
const col = column as any;
onParameterClick(col.extra.template, record);
} else if (record?.pk) {
// Navigate through to the detail page
const url = getDetailUrl(modelType, record.pk);
navigateToLink(url, navigate, event);
}
}
}}
/>
</>
);
}

View File

@@ -1,353 +1,18 @@
import { t } from '@lingui/core/macro';
import { Group } from '@mantine/core';
import { useHover } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { YesNoButton } from '@lib/components/YesNoButton';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import { cancelEvent } from '@lib/functions/Events';
import { getDetailUrl } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type { TableFilter } from '@lib/types/Filters'; import type { TableFilter } from '@lib/types/Filters';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables'; import type { TableColumn } from '@lib/types/Tables';
import { useApi } from '../../contexts/ApiContext'; import { t } from '@lingui/core/macro';
import { formatDecimal } from '../../defaults/formatters'; import { useMemo } from 'react';
import { usePartParameterFields } from '../../forms/PartForms';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { useUserState } from '../../states/UserState';
import { DescriptionColumn, PartColumn } from '../ColumnRenderers'; import { DescriptionColumn, PartColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import ParametricDataTable from '../general/ParametricDataTable';
import { TableHoverCard } from '../TableHoverCard';
import {
PARAMETER_FILTER_OPERATORS,
ParameterFilter
} from './ParametricPartTableFilters';
// Render an individual parameter cell
function ParameterCell({
record,
template,
canEdit
}: Readonly<{
record: any;
template: any;
canEdit: boolean;
}>) {
const { hovered, ref } = useHover();
// Find matching template parameter
const parameter = useMemo(() => {
return record.parameters?.find((p: any) => p.template == template.pk);
}, [record, template]);
const extra: any[] = [];
// Format the value for display
const value: ReactNode = useMemo(() => {
let v: any = parameter?.data;
// Handle boolean values
if (template?.checkbox && v != undefined) {
v = <YesNoButton value={parameter.data} />;
}
return v;
}, [parameter, template]);
if (
template.units &&
parameter &&
parameter.data_numeric &&
parameter.data_numeric != parameter.data
) {
const numeric = formatDecimal(parameter.data_numeric, { digits: 15 });
extra.push(`${numeric} [${template.units}]`);
}
if (hovered && canEdit) {
extra.push(t`Click to edit`);
}
return (
<div>
<Group grow ref={ref} justify='space-between'>
<Group grow>
<TableHoverCard
value={value ?? '-'}
extra={extra}
icon={hovered && canEdit ? 'edit' : 'info'}
title={template.name}
/>
</Group>
</Group>
</div>
);
}
export default function ParametricPartTable({ export default function ParametricPartTable({
categoryId categoryId
}: Readonly<{ }: Readonly<{
categoryId?: any; categoryId?: any;
}>) { }>) {
const api = useApi(); const customFilters: TableFilter[] = useMemo(() => {
const table = useTable('parametric-parts');
const user = useUserState();
const navigate = useNavigate();
const categoryParameters = useQuery({
queryKey: ['category-parameters', categoryId],
queryFn: async () => {
return api
.get(apiUrl(ApiEndpoints.part_parameter_template_list), {
params: {
category: categoryId
}
})
.then((response) => response.data);
},
refetchOnMount: true
});
/* Store filters against selected part parameters.
* These are stored in the format:
* {
* parameter_1: {
* '=': 'value1',
* '<': 'value2',
* ...
* },
* parameter_2: {
* '=': 'value3',
* },
* ...
* }
*
* Which allows multiple filters to be applied against each parameter template.
*/
const [parameterFilters, setParameterFilters] = useState<any>({});
/* Remove filters for a specific parameter template
* - If no operator is specified, remove all filters for this template
* - If an operator is specified, remove filters for that operator only
*/
const clearParameterFilter = useCallback(
(templateId: number, operator?: string) => {
const filterName = `parameter_${templateId}`;
if (!operator) {
// If no operator is specified, remove all filters for this template
setParameterFilters((prev: any) => {
const newFilters = { ...prev };
// Remove any filters that match the template ID
Object.keys(newFilters).forEach((key: string) => {
if (key == filterName) {
delete newFilters[key];
}
});
return newFilters;
});
return;
}
// An operator is specified, so we remove filters for that operator only
setParameterFilters((prev: any) => {
const filters = { ...prev };
const paramFilters = filters[filterName] || {};
if (paramFilters[operator] !== undefined) {
// Remove the specific operator filter
delete paramFilters[operator];
}
return {
...filters,
[filterName]: paramFilters
};
});
table.refreshTable();
},
[setParameterFilters, table.refreshTable]
);
/**
* Add (or update) a filter for a specific parameter template.
* @param templateId - The ID of the parameter template to filter on.
* @param value - The value to filter by.
* @param operator - The operator to use for filtering (e.g., '=', '<', '>', etc.).
*/
const addParameterFilter = useCallback(
(templateId: number, value: string, operator: string) => {
const filterName = `parameter_${templateId}`;
const filterValue = value?.toString().trim() ?? '';
if (filterValue.length > 0) {
setParameterFilters((prev: any) => {
const filters = { ...prev };
const paramFilters = filters[filterName] || {};
paramFilters[operator] = filterValue;
return {
...filters,
[filterName]: paramFilters
};
});
table.refreshTable();
}
},
[setParameterFilters, clearParameterFilter, table.refreshTable]
);
// Construct the query filters for the table based on the parameter filters
const parametricQueryFilters = useMemo(() => {
const filters: Record<string, string> = {};
Object.keys(parameterFilters).forEach((key: string) => {
const paramFilters: any = parameterFilters[key];
Object.keys(paramFilters).forEach((operator: string) => {
const name = `${key}${PARAMETER_FILTER_OPERATORS[operator] || ''}`;
const value = paramFilters[operator];
filters[name] = value;
});
});
return filters;
}, [parameterFilters]);
const [selectedPart, setSelectedPart] = useState<number>(0);
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
const [selectedParameter, setSelectedParameter] = useState<number>(0);
const partParameterFields: ApiFormFieldSet = usePartParameterFields({
editTemplate: false
});
const addParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list,
title: t`Add Part Parameter`,
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
focus: 'data',
onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedPart, parameter);
},
initialData: {
part: selectedPart,
template: selectedTemplate
}
});
const editParameter = useEditApiFormModal({
url: ApiEndpoints.part_parameter_list,
title: t`Edit Part Parameter`,
pk: selectedParameter,
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
focus: 'data',
onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedPart, parameter);
}
});
// Update a single parameter record in the table
const updateParameterRecord = useCallback(
(part: number, parameter: any) => {
const records = table.records;
const partIndex = records.findIndex((record: any) => record.pk == part);
if (partIndex < 0) {
// No matching part: reload the entire table
table.refreshTable();
return;
}
const parameterIndex = records[partIndex].parameters.findIndex(
(p: any) => p.pk == parameter.pk
);
if (parameterIndex < 0) {
// No matching parameter - append new parameter
records[partIndex].parameters.push(parameter);
} else {
records[partIndex].parameters[parameterIndex] = parameter;
}
table.updateRecord(records[partIndex]);
},
[table.records, table.updateRecord]
);
const parameterColumns: TableColumn[] = useMemo(() => {
const data = categoryParameters?.data || [];
return data.map((template: any) => {
let title = template.name;
if (template.units) {
title += ` [${template.units}]`;
}
const filters = parameterFilters[`parameter_${template.pk}`] || {};
return {
accessor: `parameter_${template.pk}`,
title: title,
sortable: true,
extra: {
template: template.pk
},
render: (record: any) => (
<ParameterCell
record={record}
template={template}
canEdit={user.hasChangeRole(UserRoles.part)}
/>
),
filtering: Object.keys(filters).length > 0,
filter: ({ close }: { close: () => void }) => {
return (
<ParameterFilter
template={template}
filters={parameterFilters[`parameter_${template.pk}`] || {}}
setFilter={addParameterFilter}
clearFilter={clearParameterFilter}
closeFilter={close}
/>
);
}
};
});
}, [user, categoryParameters.data, parameterFilters]);
const onParameterClick = useCallback((template: number, part: any) => {
setSelectedTemplate(template);
setSelectedPart(part.pk);
const parameter = part.parameters?.find((p: any) => p.template == template);
if (parameter) {
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
addParameter.open();
}
}, []);
const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
name: 'active', name: 'active',
@@ -367,8 +32,8 @@ export default function ParametricPartTable({
]; ];
}, []); }, []);
const tableColumns: TableColumn[] = useMemo(() => { const customColumns: TableColumn[] = useMemo(() => {
const partColumns: TableColumn[] = [ return [
PartColumn({ PartColumn({
part: '', part: '',
switchable: false switchable: false
@@ -386,43 +51,19 @@ export default function ParametricPartTable({
sortable: true sortable: true
} }
]; ];
}, []);
return [...partColumns, ...parameterColumns];
}, [parameterColumns]);
return ( return (
<> <ParametricDataTable
{addParameter.modal} modelType={ModelType.part}
{editParameter.modal} endpoint={ApiEndpoints.part_list}
<InvenTreeTable customColumns={customColumns}
url={apiUrl(ApiEndpoints.part_list)} customFilters={customFilters}
tableState={table} queryParams={{
columns={tableColumns}
props={{
enableDownload: true,
tableFilters: tableFilters,
params: {
category: categoryId, category: categoryId,
cascade: true, cascade: true,
category_detail: true, category_detail: true
parameters: true,
...parametricQueryFilters
},
onCellClick: ({ event, record, index, column, columnIndex }) => {
cancelEvent(event);
// Is this a "parameter" cell?
if (column?.accessor?.toString()?.startsWith('parameter_')) {
const col = column as any;
onParameterClick(col.extra.template, record);
} else if (record?.pk) {
// Navigate through to the part detail page
const url = getDetailUrl(ModelType.part, record.pk);
navigateToLink(url, navigate, event);
}
}
}} }}
/> />
</>
); );
} }