mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
45ecebaf19
commit
0b7bfdb4a4
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user