Parameter table editing (#6760)

* table updates

- Store page data in tablestate
- Store record count data in  table state

* Expose "records" to table state

* edit or add parameters from ParametricPartTable

- Click on parameter cells to edit / add

* Mark fields as disabled

* Update table

- Display edit icon on hover

* Fix callback
This commit is contained in:
Oliver 2024-03-20 13:10:47 +11:00 committed by GitHub
parent 45ecebaf19
commit 0b7bfdb4a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 264 additions and 69 deletions

View File

@ -28,6 +28,12 @@ export type TableState = {
setHiddenColumns: (columns: string[]) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
recordCount: number;
setRecordCount: (count: number) => void;
page: number;
setPage: (page: number) => void;
records: any[];
setRecords: (records: any[]) => void;
};
/**
@ -71,6 +77,12 @@ export function useTable(tableName: string): TableState {
setSelectedRecords([]);
}, []);
// Total record count
const [recordCount, setRecordCount] = useState<number>(0);
// Pagination data
const [page, setPage] = useState<number>(1);
// A list of hidden columns, saved to local storage
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
key: `inventree-hidden-table-columns-${tableName}`,
@ -80,6 +92,9 @@ export function useTable(tableName: string): TableState {
// Search term
const [searchTerm, setSearchTerm] = useState<string>('');
// Table records
const [records, setRecords] = useState<any[]>([]);
return {
tableKey,
refreshTable,
@ -94,6 +109,12 @@ export function useTable(tableName: string): TableState {
hiddenColumns,
setHiddenColumns,
searchTerm,
setSearchTerm
setSearchTerm,
recordCount,
setRecordCount,
page,
setPage,
records,
setRecords
};
}

View File

@ -16,4 +16,5 @@ export type TableColumn<T = any> = {
ellipsis?: boolean; // Whether the column should be ellipsized
textAlignment?: 'left' | 'center' | 'right'; // The text alignment of the column
cellsStyle?: any; // The style of the cells in the column
extra?: any; // Extra data to pass to the render function
};

View File

@ -14,8 +14,12 @@ import { modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { IconFilter, IconRefresh, IconTrash } from '@tabler/icons-react';
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { dataTagSymbol, useQuery } from '@tanstack/react-query';
import {
DataTable,
DataTableCellClickHandler,
DataTableSortStatus
} from 'mantine-datatable';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../App';
@ -57,6 +61,7 @@ const defaultPageSize: number = 25;
* @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format)
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
* @param onCellClick : (event: any, record: any, recordIndex: number, column: any, columnIndex: number) => void - Callback function when a cell is clicked
*/
export type InvenTreeTableProps<T = any> = {
params?: any;
@ -79,6 +84,7 @@ export type InvenTreeTableProps<T = any> = {
dataFormatter?: (data: any) => any;
rowActions?: (record: T) => RowAction[];
onRowClick?: (record: T, index: number, event: any) => void;
onCellClick?: DataTableCellClickHandler<T>;
};
/**
@ -260,15 +266,12 @@ export function InvenTreeTable<T = any>({
);
}
// Pagination
const [page, setPage] = useState(1);
// Filter list visibility
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
// Reset the pagination state when the search term changes
useEffect(() => {
setPage(1);
tableState.setPage(1);
}, [tableState.searchTerm]);
/*
@ -295,7 +298,7 @@ export function InvenTreeTable<T = any>({
if (tableProps.enablePagination && paginate) {
let pageSize = tableProps.pageSize ?? defaultPageSize;
queryParams.limit = pageSize;
queryParams.offset = (page - 1) * pageSize;
queryParams.offset = (tableState.page - 1) * pageSize;
}
// Ordering
@ -356,7 +359,7 @@ export function InvenTreeTable<T = any>({
);
const handleSortStatusChange = (status: DataTableSortStatus) => {
setPage(1);
tableState.setPage(1);
setSortStatus(status);
};
@ -388,7 +391,7 @@ export function InvenTreeTable<T = any>({
results = [];
}
setRecordCount(response.data?.count ?? results.length);
tableState.setRecordCount(response.data?.count ?? results.length);
return results;
case 400:
@ -420,7 +423,7 @@ export function InvenTreeTable<T = any>({
const { data, isFetching, refetch } = useQuery({
queryKey: [
page,
tableState.page,
props.params,
sortStatus.columnAccessor,
sortStatus.direction,
@ -433,7 +436,10 @@ export function InvenTreeTable<T = any>({
refetchOnMount: true
});
const [recordCount, setRecordCount] = useState<number>(0);
// Update tableState.records when new data received
useEffect(() => {
tableState.setRecords(data ?? []);
}, [data]);
// Callback function to delete the selected records in the table
const deleteSelectedRecords = useCallback(() => {
@ -597,10 +603,10 @@ export function InvenTreeTable<T = any>({
pinLastColumn={tableProps.rowActions != undefined}
idAccessor={tableProps.idAccessor}
minHeight={300}
totalRecords={recordCount}
totalRecords={tableState.recordCount}
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
page={page}
onPageChange={setPage}
page={tableState.page}
onPageChange={tableState.setPage}
sortStatus={sortStatus}
onSortStatusChange={handleSortStatusChange}
selectedRecords={
@ -614,9 +620,10 @@ export function InvenTreeTable<T = any>({
rowExpansion={tableProps.rowExpansion}
fetching={isFetching}
noRecordsText={missingRecordsText}
records={data}
records={tableState.records}
columns={dataColumns}
onRowClick={tableProps.onRowClick}
onCellClick={tableProps.onCellClick}
defaultColumnProps={{
noWrap: true,
textAlignment: 'left',

View File

@ -1,13 +1,22 @@
import { t } from '@lingui/macro';
import { ActionIcon, Group, Text, Tooltip } from '@mantine/core';
import { useHover } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { YesNoButton } from '../../components/items/YesNoButton';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { getDetailUrl } from '../../functions/urls';
import {
useCreateApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -16,6 +25,73 @@ import { DescriptionColumn, PartColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
// Render an individual parameter cell
function ParameterCell({
record,
template,
canEdit,
onEdit
}: {
record: any;
template: any;
canEdit: boolean;
onEdit: () => void;
}) {
const { hovered, ref } = useHover();
// Find matching template parameter
let parameter = record.parameters?.find(
(p: any) => p.template == template.pk
);
let extra: any[] = [];
let value: any = parameter?.data;
if (template?.checkbox && value != undefined) {
value = <YesNoButton value={parameter.data} />;
}
if (
template.units &&
parameter &&
parameter.data_numeric &&
parameter.data_numeric != parameter.data
) {
extra.push(`${parameter.data_numeric} [${template.units}]`);
}
const handleClick = useCallback((event: any) => {
event?.preventDefault();
event?.stopPropagation();
event?.nativeEvent?.stopImmediatePropagation();
onEdit();
}, []);
return (
<div>
<Group grow ref={ref} position="apart">
<Group grow style={{ flex: 1 }}>
<TableHoverCard
value={value ?? '-'}
extra={extra}
title={t`Internal Units`}
/>
</Group>
{hovered && canEdit && (
<div style={{ flex: 0 }}>
<Tooltip label={t`Edit parameter`}>
<ActionIcon size="xs" onClick={handleClick}>
<IconEdit />
</ActionIcon>
</Tooltip>
</div>
)}
</Group>
</div>
);
}
export default function ParametricPartTable({
categoryId
}: {
@ -40,6 +116,73 @@ export default function ParametricPartTable({
refetchOnMount: true
});
const [selectedPart, setSelectedPart] = useState<number>(0);
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
const [selectedParameter, setSelectedParameter] = useState<number>(0);
const partParameterFields: ApiFormFieldSet = useMemo(() => {
return {
part: {
disabled: true
},
template: {
disabled: true
},
data: {}
};
}, []);
const addParameter = useCreateApiFormModal({
url: ApiEndpoints.part_parameter_list,
title: t`Add Part Parameter`,
fields: partParameterFields,
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: partParameterFields,
onFormSuccess: (parameter: any) => {
updateParameterRecord(selectedPart, parameter);
}
});
// Update a single parameter record in the table
const updateParameterRecord = useCallback(
(part: number, parameter: any) => {
let records = table.records;
let partIndex = records.findIndex((record: any) => record.pk == part);
if (partIndex < 0) {
// No matching part: reload the entire table
table.refreshTable();
return;
}
let 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.setRecords(records);
},
[table.records]
);
const parameterColumns: TableColumn[] = useMemo(() => {
let data = categoryParmeters.data ?? [];
@ -54,43 +197,33 @@ export default function ParametricPartTable({
accessor: `parameter_${template.pk}`,
title: title,
sortable: true,
render: (record: any) => {
// Find matching template parameter
let parameter = record.parameters?.find(
(p: any) => p.template == template.pk
);
extra: {
template: template.pk
},
render: (record: any) => (
<ParameterCell
record={record}
template={template}
canEdit={user.hasChangeRole(UserRoles.part)}
onEdit={() => {
setSelectedTemplate(template.pk);
setSelectedPart(record.pk);
let parameter = record.parameters?.find(
(p: any) => p.template == template.pk
);
if (!parameter) {
return '-';
}
let extra: any[] = [];
if (
template.units &&
parameter.data_numeric &&
parameter.data_numeric != parameter.data
) {
extra.push(`${parameter.data_numeric} [${template.units}]`);
}
let value: any = parameter.data;
if (template?.checkbox) {
value = <YesNoButton value={parameter.data} />;
}
return (
<TableHoverCard
value={value}
extra={extra}
title={t`Internal Units`}
/>
);
}
if (parameter) {
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
addParameter.open();
}
}}
/>
)
};
});
}, [categoryParmeters.data]);
}, [user, categoryParmeters.data]);
const tableColumns: TableColumn[] = useMemo(() => {
const partColumns: TableColumn[] = [
@ -111,25 +244,58 @@ export default function ParametricPartTable({
return [...partColumns, ...parameterColumns];
}, [parameterColumns]);
// Callback when a parameter cell is clicked - either edit or add a new parameter
const handleCellClick = useCallback(
(record: any, column: any) => {
let template_id = column?.extra?.template;
if (!template_id) {
return;
}
setSelectedPart(record.pk);
setSelectedTemplate(template_id);
// Find the associated parameter
let parameter = record?.parameters?.find(
(p: any) => p.template == template_id
);
if (parameter) {
// Parameter exists - open edit dialog
setSelectedParameter(parameter.pk);
editParameter.open();
} else {
// Parameter does not exist - create it!
addParameter.open();
}
},
[user]
);
return (
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: false,
params: {
category: categoryId,
cascade: true,
category_detail: true,
parameters: true
},
onRowClick: (record) => {
if (record.pk) {
navigate(getDetailUrl(ModelType.part, record.pk));
<>
{addParameter.modal}
{editParameter.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.part_list)}
tableState={table}
columns={tableColumns}
props={{
enableDownload: false,
params: {
category: categoryId,
cascade: true,
category_detail: true,
parameters: true
},
onRowClick: (record) => {
if (record.pk) {
navigate(getDetailUrl(ModelType.part, record.pk));
}
}
}
}}
/>
}}
/>
</>
);
}