mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-16 09:18:10 +00:00
Make parametric table generic
This commit is contained in:
@@ -111,8 +111,6 @@ export enum ApiEndpoints {
|
||||
|
||||
// Part API endpoints
|
||||
part_list = 'part/',
|
||||
part_parameter_list = 'part/parameter/',
|
||||
part_parameter_template_list = 'part/parameter/template/',
|
||||
part_thumbs_list = 'part/thumbs/',
|
||||
part_pricing = 'part/:id/pricing/',
|
||||
part_requirements = 'part/:id/requirements/',
|
||||
|
||||
@@ -33,18 +33,18 @@ export const ModelInformationDict: ModelDict = {
|
||||
admin_url: '/part/part/',
|
||||
icon: 'part'
|
||||
},
|
||||
parameter: {
|
||||
label: () => t`Parameter`,
|
||||
label_multiple: () => t`Parameters`,
|
||||
api_endpoint: ApiEndpoints.parameter_list,
|
||||
icon: 'list_details'
|
||||
},
|
||||
parametertemplate: {
|
||||
label: () => t`Parameter Template`,
|
||||
label_multiple: () => t`Parameter Templates`,
|
||||
api_endpoint: ApiEndpoints.parameter_template_list
|
||||
},
|
||||
partparametertemplate: {
|
||||
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'
|
||||
api_endpoint: ApiEndpoints.parameter_template_list,
|
||||
admin_url: '/common/parametertemplate/',
|
||||
icon: 'list'
|
||||
},
|
||||
parttesttemplate: {
|
||||
label: () => t`Part Test Template`,
|
||||
|
||||
@@ -6,7 +6,6 @@ export enum ModelType {
|
||||
supplierpart = 'supplierpart',
|
||||
manufacturerpart = 'manufacturerpart',
|
||||
partcategory = 'partcategory',
|
||||
partparametertemplate = 'partparametertemplate',
|
||||
parttesttemplate = 'parttesttemplate',
|
||||
projectcode = 'projectcode',
|
||||
stockitem = 'stockitem',
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-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';
|
||||
|
||||
/**
|
||||
@@ -224,97 +220,6 @@ export function partCategoryFields({
|
||||
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 {
|
||||
return {
|
||||
part: {
|
||||
|
||||
@@ -312,7 +312,11 @@ export default function CategoryDetail() {
|
||||
name: 'parameters',
|
||||
label: t`Part Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
content: <ParametricPartTable categoryId={id} />
|
||||
content: (
|
||||
<>
|
||||
<ParametricPartTable categoryId={id} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
],
|
||||
[category, id]
|
||||
|
||||
409
src/frontend/src/tables/general/ParametricDataTable.tsx
Normal file
409
src/frontend/src/tables/general/ParametricDataTable.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 { 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 { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { usePartParameterFields } from '../../forms/PartForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { DescriptionColumn, PartColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
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>
|
||||
);
|
||||
}
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function ParametricPartTable({
|
||||
categoryId
|
||||
}: Readonly<{
|
||||
categoryId?: any;
|
||||
}>) {
|
||||
const api = useApi();
|
||||
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(() => {
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'active',
|
||||
@@ -367,8 +32,8 @@ export default function ParametricPartTable({
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
const partColumns: TableColumn[] = [
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
part: '',
|
||||
switchable: false
|
||||
@@ -386,43 +51,19 @@ export default function ParametricPartTable({
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
|
||||
return [...partColumns, ...parameterColumns];
|
||||
}, [parameterColumns]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{addParameter.modal}
|
||||
{editParameter.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.part_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
tableFilters: tableFilters,
|
||||
params: {
|
||||
category: categoryId,
|
||||
cascade: 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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.part}
|
||||
endpoint={ApiEndpoints.part_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
category: categoryId,
|
||||
cascade: true,
|
||||
category_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user