diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index 41303b3147..94d97b5f22 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -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(0); + + // Pagination data + const [page, setPage] = useState(1); + // A list of hidden columns, saved to local storage const [hiddenColumns, setHiddenColumns] = useLocalStorage({ key: `inventree-hidden-table-columns-${tableName}`, @@ -80,6 +92,9 @@ export function useTable(tableName: string): TableState { // Search term const [searchTerm, setSearchTerm] = useState(''); + // Table records + const [records, setRecords] = useState([]); + return { tableKey, refreshTable, @@ -94,6 +109,12 @@ export function useTable(tableName: string): TableState { hiddenColumns, setHiddenColumns, searchTerm, - setSearchTerm + setSearchTerm, + recordCount, + setRecordCount, + page, + setPage, + records, + setRecords }; } diff --git a/src/frontend/src/tables/Column.tsx b/src/frontend/src/tables/Column.tsx index f47a8f32d8..de4d9ac5e7 100644 --- a/src/frontend/src/tables/Column.tsx +++ b/src/frontend/src/tables/Column.tsx @@ -16,4 +16,5 @@ export type TableColumn = { 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 }; diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 5c60711a6f..7d3ba4b598 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -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 = { params?: any; @@ -79,6 +84,7 @@ export type InvenTreeTableProps = { dataFormatter?: (data: any) => any; rowActions?: (record: T) => RowAction[]; onRowClick?: (record: T, index: number, event: any) => void; + onCellClick?: DataTableCellClickHandler; }; /** @@ -260,15 +266,12 @@ export function InvenTreeTable({ ); } - // Pagination - const [page, setPage] = useState(1); - // Filter list visibility const [filtersVisible, setFiltersVisible] = useState(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({ 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({ ); const handleSortStatusChange = (status: DataTableSortStatus) => { - setPage(1); + tableState.setPage(1); setSortStatus(status); }; @@ -388,7 +391,7 @@ export function InvenTreeTable({ 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({ const { data, isFetching, refetch } = useQuery({ queryKey: [ - page, + tableState.page, props.params, sortStatus.columnAccessor, sortStatus.direction, @@ -433,7 +436,10 @@ export function InvenTreeTable({ refetchOnMount: true }); - const [recordCount, setRecordCount] = useState(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({ 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({ 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', diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index c3aac52527..ac208119d3 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -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 = ; + } + + 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 ( +
+ + + + + {hovered && canEdit && ( +
+ + + + + +
+ )} +
+
+ ); +} + export default function ParametricPartTable({ categoryId }: { @@ -40,6 +116,73 @@ export default function ParametricPartTable({ refetchOnMount: true }); + const [selectedPart, setSelectedPart] = useState(0); + const [selectedTemplate, setSelectedTemplate] = useState(0); + const [selectedParameter, setSelectedParameter] = useState(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) => ( + { + 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 = ; - } - - return ( - - ); - } + 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 ( - { - if (record.pk) { - navigate(getDetailUrl(ModelType.part, record.pk)); + <> + {addParameter.modal} + {editParameter.modal} + { + if (record.pk) { + navigate(getDetailUrl(ModelType.part, record.pk)); + } } - } - }} - /> + }} + /> + ); }