From 4b54ed3a7d75784905674da0f92b256ebdb2394b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 30 Oct 2024 02:16:13 +0000 Subject: [PATCH 1/9] Refactor table header items out into new file --- src/frontend/src/hooks/UseTable.tsx | 49 ++- src/frontend/src/pages/part/PartBomPanel.tsx | 49 +++ src/frontend/src/pages/part/PartDetail.tsx | 6 +- src/frontend/src/tables/InvenTreeTable.tsx | 392 +++++------------- .../src/tables/InvenTreeTableHeader.tsx | 239 +++++++++++ src/frontend/src/tables/bom/BomTable.tsx | 144 ++++--- 6 files changed, 517 insertions(+), 362 deletions(-) create mode 100644 src/frontend/src/pages/part/PartBomPanel.tsx create mode 100644 src/frontend/src/tables/InvenTreeTableHeader.tsx diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index e8af9ad5a51e..62b012d6cc0b 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -1,5 +1,6 @@ import { randomId, useLocalStorage } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; +import { SetURLSearchParams, useSearchParams } from 'react-router-dom'; import { TableFilter } from '../tables/Filter'; @@ -8,19 +9,47 @@ import { TableFilter } from '../tables/Filter'; * * tableKey: A unique key for the table. When this key changes, the table will be refreshed. * refreshTable: A callback function to externally refresh the table. + * isLoading: A boolean flag to indicate if the table is currently loading data + * setIsLoading: A function to set the isLoading flag * activeFilters: An array of active filters (saved to local storage) + * setActiveFilters: A function to set the active filters + * clearActiveFilters: A function to clear all active filters + * queryFilters: A map of query filters (e.g. ?active=true&overdue=false) passed in the URL + * setQueryFilters: A function to set the query filters + * clearQueryFilters: A function to clear all query filters + * expandedRecords: An array of expanded records (rows) in the table + * setExpandedRecords: A function to set the expanded records + * isRowExpanded: A function to determine if a record is expanded * selectedRecords: An array of selected records (rows) in the table + * selectedIds: An array of primary key values for selected records + * hasSelectedRecords: A boolean flag to indicate if any records are selected + * setSelectedRecords: A function to set the selected records + * clearSelectedRecords: A function to clear all selected records * hiddenColumns: An array of hidden column names + * setHiddenColumns: A function to set the hidden columns * searchTerm: The current search term for the table + * setSearchTerm: A function to set the search term + * recordCount: The total number of records in the table + * setRecordCount: A function to set the record count + * page: The current page number + * setPage: A function to set the current page number + * pageSize: The number of records per page + * setPageSize: A function to set the number of records per page + * records: An array of records (rows) in the table + * setRecords: A function to set the records + * updateRecord: A function to update a single record in the table */ export type TableState = { tableKey: string; refreshTable: () => void; - activeFilters: TableFilter[]; isLoading: boolean; setIsLoading: (value: boolean) => void; + activeFilters: TableFilter[]; setActiveFilters: (filters: TableFilter[]) => void; clearActiveFilters: () => void; + queryFilters: URLSearchParams; + setQueryFilters: SetURLSearchParams; + clearQueryFilters: () => void; expandedRecords: any[]; setExpandedRecords: (records: any[]) => void; isRowExpanded: (pk: number) => boolean; @@ -42,8 +71,6 @@ export type TableState = { records: any[]; setRecords: (records: any[]) => void; updateRecord: (record: any) => void; - editable: boolean; - setEditable: (value: boolean) => void; }; /** @@ -58,6 +85,13 @@ export function useTable(tableName: string): TableState { return `${tableName.replaceAll('-', '')}-${randomId()}`; } + // Extract URL query parameters (e.g. ?active=true&overdue=false) + const [queryFilters, setQueryFilters] = useSearchParams(); + + const clearQueryFilters = useCallback(() => { + setQueryFilters({}); + }, []); + const [tableKey, setTableKey] = useState(generateTableName()); // Callback used to refresh (reload) the table @@ -145,8 +179,6 @@ export function useTable(tableName: string): TableState { const [isLoading, setIsLoading] = useState(false); - const [editable, setEditable] = useState(false); - return { tableKey, refreshTable, @@ -155,6 +187,9 @@ export function useTable(tableName: string): TableState { activeFilters, setActiveFilters, clearActiveFilters, + queryFilters, + setQueryFilters, + clearQueryFilters, expandedRecords, setExpandedRecords, isRowExpanded, @@ -175,8 +210,6 @@ export function useTable(tableName: string): TableState { setPageSize, records, setRecords, - updateRecord, - editable, - setEditable + updateRecord }; } diff --git a/src/frontend/src/pages/part/PartBomPanel.tsx b/src/frontend/src/pages/part/PartBomPanel.tsx new file mode 100644 index 000000000000..027139a77721 --- /dev/null +++ b/src/frontend/src/pages/part/PartBomPanel.tsx @@ -0,0 +1,49 @@ +import { t } from '@lingui/macro'; +import { Alert, Loader, Stack, Text } from '@mantine/core'; +import { IconLock } from '@tabler/icons-react'; +import { useState } from 'react'; + +import ImporterDrawer from '../../components/importer/ImporterDrawer'; +import { useUserState } from '../../states/UserState'; +import { BomTable } from '../../tables/bom/BomTable'; + +export default function PartBomPanel({ part }: { part: any }) { + const user = useUserState(); + + const [importOpened, setImportOpened] = useState(false); + + const [selectedSession, setSelectedSession] = useState( + undefined + ); + + if (!part.pk) { + return ; + } + + return ( + <> + + {part?.locked && ( + } + p="xs" + > + {t`Bill of materials cannot be edited, as the part is locked`} + + )} + + + { + setSelectedSession(undefined); + setImportOpened(false); + // TODO: Refresh / reload the BOM table + }} + /> + + ); +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 78bf9565e530..dcb17507d701 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -85,7 +85,6 @@ import { useUserSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; -import { BomTable } from '../../tables/bom/BomTable'; import { UsedInTable } from '../../tables/bom/UsedInTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { PartParameterTable } from '../../tables/part/PartParameterTable'; @@ -100,6 +99,7 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; import PartAllocationPanel from './PartAllocationPanel'; +import PartBomPanel from './PartBomPanel'; import PartPricingPanel from './PartPricingPanel'; import PartSchedulingDetail from './PartSchedulingDetail'; import PartStocktakeDetail from './PartStocktakeDetail'; @@ -625,9 +625,7 @@ export default function PartDetail() { label: t`Bill of Materials`, icon: , hidden: !part.assembly, - content: ( - - ) + content: }, { name: 'builds', diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 1f9eb01df9b2..2da8ad08d759 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -1,21 +1,4 @@ import { t } from '@lingui/macro'; -import { - ActionIcon, - Alert, - Box, - Group, - Indicator, - Space, - Stack, - Tooltip -} from '@mantine/core'; -import { - IconBarcode, - IconFilter, - IconFilterCancel, - IconRefresh, - IconTrash -} from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { DataTable, @@ -23,20 +6,11 @@ import { DataTableRowExpansionProps, DataTableSortStatus } from 'mantine-datatable'; -import React, { - Fragment, - useCallback, - useEffect, - useMemo, - useState -} from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { api } from '../App'; import { Boundary } from '../components/Boundary'; -import { ActionButton } from '../components/buttons/ActionButton'; -import { ButtonMenu } from '../components/buttons/ButtonMenu'; -import { PrintingActions } from '../components/buttons/PrintingActions'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ModelType } from '../enums/ModelType'; import { resolveItem } from '../functions/conversion'; @@ -44,16 +18,12 @@ import { cancelEvent } from '../functions/events'; import { extractAvailableFields, mapFields } from '../functions/forms'; import { navigateToLink } from '../functions/navigation'; import { getDetailUrl } from '../functions/urls'; -import { useDeleteApiFormModal } from '../hooks/UseForm'; import { TableState } from '../hooks/UseTable'; import { useLocalState } from '../states/LocalState'; import { TableColumn } from './Column'; -import { TableColumnSelect } from './ColumnSelect'; -import { DownloadAction } from './DownloadAction'; import { TableFilter } from './Filter'; -import { FilterSelectDrawer } from './FilterSelectDrawer'; +import InvenTreeTableHeader from './InvenTreeTableHeader'; import { RowAction, RowActions } from './RowActions'; -import { TableSearchInput } from './Search'; const defaultPageSize: number = 25; const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500]; @@ -84,6 +54,7 @@ const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500]; * @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked * @param onCellClick : (event: any, record: any, index: number, column: any, columnIndex: number) => void - Callback function when a cell is clicked * @param modelType: ModelType - The model type for the table + * @param noHeader: boolean - Hide the table header */ export type InvenTreeTableProps = { params?: any; @@ -113,6 +84,7 @@ export type InvenTreeTableProps = { modelType?: ModelType; rowStyle?: (record: T, index: number) => any; modelField?: string; + noHeader?: boolean; }; /** @@ -162,9 +134,6 @@ export function InvenTreeTable>({ const navigate = useNavigate(); - // Extract URL query parameters (e.g. ?active=true&overdue=false) - const [urlQueryParams, setUrlQueryParams] = useSearchParams(); - // Construct table filters - note that we can introspect filter labels from column names const filters: TableFilter[] = useMemo(() => { return ( @@ -344,85 +313,78 @@ export function InvenTreeTable>({ ); } - // Filter list visibility - const [filtersVisible, setFiltersVisible] = useState(false); - // Reset the pagination state when the search term changes useEffect(() => { tableState.setPage(1); }, [tableState.searchTerm]); + // Data Sorting + const [sortStatus, setSortStatus] = useState>({ + columnAccessor: tableProps.defaultSortColumn ?? '', + direction: 'asc' + }); + /* * Construct query filters for the current table */ - function getTableFilters(paginate: boolean = false) { - let queryParams = { - ...tableProps.params - }; - - // Add custom filters - if (tableState.activeFilters) { - tableState.activeFilters.forEach( - (flt) => (queryParams[flt.name] = flt.value) - ); - } + const getTableFilters = useCallback( + (paginate: boolean = false) => { + let queryParams = { + ...tableProps.params + }; - // Allow override of filters based on URL query parameters - if (urlQueryParams) { - for (let [key, value] of urlQueryParams) { - queryParams[key] = value; + // Add custom filters + if (tableState.activeFilters) { + tableState.activeFilters.forEach( + (flt) => (queryParams[flt.name] = flt.value) + ); } - } - - // Add custom search term - if (tableState.searchTerm) { - queryParams.search = tableState.searchTerm; - } - - // Pagination - if (tableProps.enablePagination && paginate) { - let pageSize = tableState.pageSize ?? defaultPageSize; - if (pageSize != tableState.pageSize) tableState.setPageSize(pageSize); - queryParams.limit = pageSize; - queryParams.offset = (tableState.page - 1) * pageSize; - } - - // Ordering - let ordering = getOrderingTerm(); - if (ordering) { - if (sortStatus.direction == 'asc') { - queryParams.ordering = ordering; - } else { - queryParams.ordering = `-${ordering}`; + // Allow override of filters based on URL query parameters + if (tableState.queryFilters) { + for (let [key, value] of tableState.queryFilters) { + queryParams[key] = value; + } } - } - - return queryParams; - } - // Data download callback - function downloadData(fileFormat: string) { - // Download entire dataset (no pagination) - let queryParams = getTableFilters(false); + // Add custom search term + if (tableState.searchTerm) { + queryParams.search = tableState.searchTerm; + } - // Specify file format - queryParams.export = fileFormat; + // Pagination + if (tableProps.enablePagination && paginate) { + let pageSize = tableState.pageSize ?? defaultPageSize; + if (pageSize != tableState.pageSize) tableState.setPageSize(pageSize); + queryParams.limit = pageSize; + queryParams.offset = (tableState.page - 1) * pageSize; + } - let downloadUrl = api.getUri({ - url: url, - params: queryParams - }); + // Ordering + let ordering = getOrderingTerm(); - // Download file in a new window (to force download) - window.open(downloadUrl, '_blank'); - } + if (ordering) { + if (sortStatus.direction == 'asc') { + queryParams.ordering = ordering; + } else { + queryParams.ordering = `-${ordering}`; + } + } - // Data Sorting - const [sortStatus, setSortStatus] = useState>({ - columnAccessor: tableProps.defaultSortColumn ?? '', - direction: 'asc' - }); + return queryParams; + }, + [ + tableProps.params, + tableProps.enablePagination, + tableState.activeFilters, + tableState.queryFilters, + tableState.searchTerm, + tableState.pageSize, + tableState.setPageSize, + sortStatus, + getOrderingTerm + ] + ); useEffect(() => { const tableKey: string = tableState.tableKey.split('-')[0]; @@ -538,7 +500,7 @@ export function InvenTreeTable>({ // Refetch data when the query parameters change useEffect(() => { refetch(); - }, [urlQueryParams]); + }, [tableState.queryFilters]); useEffect(() => { tableState.setIsLoading( @@ -559,35 +521,6 @@ export function InvenTreeTable>({ } }, [data]); - const deleteRecords = useDeleteApiFormModal({ - url: url, - title: t`Delete Selected Items`, - preFormContent: ( - - {t`This action cannot be undone`} - - ), - initialData: { - items: tableState.selectedIds - }, - fields: { - items: { - hidden: true - } - }, - onFormSuccess: () => { - tableState.clearSelectedRecords(); - tableState.refreshTable(); - - if (props.afterBulkDelete) { - props.afterBulkDelete(); - } - } - }); - // Callback when a cell is clicked const handleCellClick = useCallback( ({ @@ -672,160 +605,57 @@ export function InvenTreeTable>({ return ( <> - {deleteRecords.modal} - {tableProps.enableFilters && (filters.length ?? 0) > 0 && ( - - setFiltersVisible(false)} - /> - - )} + + + - - - - - {(tableProps.barcodeActions?.length ?? 0) > 0 && ( - } - label={t`Barcode Actions`} - tooltip={t`Barcode Actions`} - actions={tableProps.barcodeActions ?? []} - /> - )} - {tableProps.enableBulkDelete && ( - } - color="red" - tooltip={t`Delete selected records`} - onClick={() => { - deleteRecords.open(); - }} - /> - )} - {tableProps.tableActions?.map((group, idx) => ( - {group} - ))} - - - - {tableProps.enableSearch && ( - - tableState.setSearchTerm(term) - } - /> - )} - {tableProps.enableRefresh && ( - - - { - refetch(); - tableState.clearSelectedRecords(); - }} - /> - - - )} - {hasSwitchableColumns && ( - - )} - {urlQueryParams.size > 0 && ( - - - { - setUrlQueryParams({}); - }} - /> - - - )} - {tableProps.enableFilters && filters.length > 0 && ( - - - - setFiltersVisible(!filtersVisible)} - /> - - - - )} - {tableProps.enableDownload && ( - - )} - - - - (theme) => ({ - // TODO @SchrodingersGat : Need a better way of handling "wide" cells, - overflow: 'hidden' - }) - }} - {...optionalParams} - /> - - + (theme) => ({ + // TODO @SchrodingersGat : Need a better way of handling "wide" cells, + overflow: 'hidden' + }) + }} + {...optionalParams} + /> ); diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx new file mode 100644 index 000000000000..4cf2deca2031 --- /dev/null +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -0,0 +1,239 @@ +import { t } from '@lingui/macro'; +import { + ActionIcon, + Alert, + Group, + Indicator, + Space, + Tooltip +} from '@mantine/core'; +import { + IconBarcode, + IconFilter, + IconFilterCancel, + IconRefresh, + IconTrash +} from '@tabler/icons-react'; +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Fragment } from 'react/jsx-runtime'; + +import { api } from '../App'; +import { Boundary } from '../components/Boundary'; +import { ActionButton } from '../components/buttons/ActionButton'; +import { ButtonMenu } from '../components/buttons/ButtonMenu'; +import { PrintingActions } from '../components/buttons/PrintingActions'; +import { useDeleteApiFormModal } from '../hooks/UseForm'; +import { TableState } from '../hooks/UseTable'; +import { TableColumnSelect } from './ColumnSelect'; +import { DownloadAction } from './DownloadAction'; +import { TableFilter } from './Filter'; +import { FilterSelectDrawer } from './FilterSelectDrawer'; +import { InvenTreeTableProps } from './InvenTreeTable'; +import { TableSearchInput } from './Search'; + +/** + * Render a composite header for an InvenTree table + */ +export default function InvenTreeTableHeader({ + tableUrl, + tableState, + tableProps, + hasSwitchableColumns, + columns, + filters, + toggleColumn +}: { + tableUrl: string; + tableState: TableState; + tableProps: InvenTreeTableProps; + hasSwitchableColumns: boolean; + columns: any; + filters: TableFilter[]; + toggleColumn: (column: string) => void; +}) { + // Filter list visibility + const [filtersVisible, setFiltersVisible] = useState(false); + + const downloadData = (fileFormat: string) => { + // Download entire dataset (no pagination) + + let queryParams = { + ...tableProps.params + }; + + // Add in active filters + if (tableState.activeFilters) { + tableState.activeFilters.forEach((filter) => { + queryParams[filter.name] = filter.value; + }); + } + + // Allow overriding of query parameters + if (tableState.queryFilters) { + for (let [key, value] of tableState.queryFilters) { + queryParams[key] = value; + } + } + + // Add custom search term + if (tableState.searchTerm) { + queryParams.search = tableState.searchTerm; + } + + // Specify file format + queryParams.export = fileFormat; + + let downloadUrl = api.getUri({ + url: tableUrl, + params: queryParams + }); + + // Download file in a new window (to force download) + window.open(downloadUrl, '_blank'); + }; + + const deleteRecords = useDeleteApiFormModal({ + url: tableUrl, + title: t`Delete Selected Items`, + preFormContent: ( + + {t`This action cannot be undone`} + + ), + initialData: { + items: tableState.selectedIds + }, + fields: { + items: { + hidden: true + } + }, + onFormSuccess: () => { + tableState.clearSelectedRecords(); + tableState.refreshTable(); + + if (tableProps.afterBulkDelete) { + tableProps.afterBulkDelete(); + } + } + }); + + return ( + <> + {deleteRecords.modal} + {tableProps.enableFilters && (filters.length ?? 0) > 0 && ( + + setFiltersVisible(false)} + /> + + )} + + + + + {(tableProps.barcodeActions?.length ?? 0) > 0 && ( + } + label={t`Barcode Actions`} + tooltip={t`Barcode Actions`} + actions={tableProps.barcodeActions ?? []} + /> + )} + {tableProps.enableBulkDelete && ( + } + color="red" + tooltip={t`Delete selected records`} + onClick={() => { + deleteRecords.open(); + }} + /> + )} + {tableProps.tableActions?.map((group, idx) => ( + {group} + ))} + + + + {tableProps.enableSearch && ( + tableState.setSearchTerm(term)} + /> + )} + {tableProps.enableRefresh && ( + + + { + tableState.refreshTable(); + tableState.clearSelectedRecords(); + }} + /> + + + )} + {hasSwitchableColumns && ( + + )} + {tableState.queryFilters.size > 0 && ( + + + { + tableState.clearQueryFilters(); + }} + /> + + + )} + {tableProps.enableFilters && filters.length > 0 && ( + + + + setFiltersVisible(!filtersVisible)} + /> + + + + )} + {tableProps.enableDownload && ( + + )} + + + + ); +} diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index 665510d65aca..3adbb0c8830b 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -1,13 +1,16 @@ import { t } from '@lingui/macro'; -import { Alert, Group, Stack, Text } from '@mantine/core'; +import { ActionIcon, Alert, Group, Stack, Text } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { IconArrowRight, + IconChevronDown, + IconChevronRight, IconCircleCheck, IconFileArrowLeft, IconLock, IconSwitch3 } from '@tabler/icons-react'; +import { DataTableRowExpansionProps } from 'mantine-datatable'; import { ReactNode, useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -62,23 +65,20 @@ function availableStockQuantity(record: any): number { } export function BomTable({ - partId, - partLocked, + part, + simplified, params = {} }: Readonly<{ - partId: number; - partLocked?: boolean; + part: any; + simplified?: boolean; params?: any; }>) { const user = useUserState(); const table = useTable('bom'); const navigate = useNavigate(); - const [importOpened, setImportOpened] = useState(false); - - const [selectedSession, setSelectedSession] = useState( - undefined - ); + const partId = useMemo(() => part.pk, [part]); + const partLocked = useMemo(() => part.locked, [part]); const tableColumns: TableColumn[] = useMemo(() => { return [ @@ -98,17 +98,28 @@ export function BomTable({ return ( part && ( - - } - extra={extra} - title={t`Part Information`} - /> + + {part.assembly && ( + + {table.isRowExpanded(record.pk) ? ( + + ) : ( + + )} + + )} + + } + extra={extra} + title={t`Part Information`} + /> + ) ); } @@ -296,7 +307,7 @@ export function BomTable({ }, NoteColumn({}) ]; - }, [partId, params]); + }, [partId, params, table.isRowExpanded]); const tableFilters: TableFilter[] = useMemo(() => { return [ @@ -378,8 +389,9 @@ export function BomTable({ title: t`Import BOM Data`, fields: importSessionFields, onFormSuccess: (response: any) => { - setSelectedSession(response.pk); - setImportOpened(true); + // TODO: Open import session drawer + // setSelectedSession(response.pk); + // setImportOpened(true); } }); @@ -529,53 +541,47 @@ export function BomTable({ ]; }, [partLocked, user]); + // Control row-expansion for multi-level BOMs + const rowExpansion: DataTableRowExpansionProps = useMemo(() => { + return { + allowMultiple: true, + expandable: ({ record }: { record: any }) => { + return ( + table.isRowExpanded(record.pk) || record.sub_part_detail.assembly + ); + }, + content: ({ record }: { record: any }) => { + return ; + } + }; + }, [table.isRowExpanded]); + return ( <> - {importBomItem.modal} - {newBomItem.modal} - {editBomItem.modal} - {validateBom.modal} - {deleteBomItem.modal} - - {partLocked && ( - } - p="xs" - > - {t`Bill of materials cannot be edited, as the part is locked`} - - )} - - - { - setSelectedSession(undefined); - setImportOpened(false); - table.refreshTable(); + {!simplified && importBomItem.modal} + {!simplified && newBomItem.modal} + {!simplified && editBomItem.modal} + {!simplified && validateBom.modal} + {!simplified && deleteBomItem.modal} + + From 4dfd51a3edc38157a295653bf5045ca11c26bee7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 30 Oct 2024 03:37:07 +0000 Subject: [PATCH 2/9] Improve BomItem API query --- src/backend/InvenTree/part/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index 58675b44b4f6..5ac3d2c54ecd 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -1708,6 +1708,10 @@ def setup_eager_loading(queryset): 'sub_part__stock_items__sales_order_allocations', ) + queryset = queryset.select_related( + 'part__pricing_data', 'sub_part__pricing_data' + ) + queryset = queryset.prefetch_related( 'substitutes', 'substitutes__part__stock_items' ) From 6f16fec4da229db2e4bb0e338c1f0f014d27e682 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 30 Oct 2024 03:49:36 +0000 Subject: [PATCH 3/9] Allow table header to be removed entirely --- src/frontend/src/tables/InvenTreeTable.tsx | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index 2da8ad08d759..f3a09a07e717 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -255,7 +255,7 @@ export function InvenTreeTable>({ // Update column visibility when hiddenColumns change const dataColumns: any = useMemo(() => { - let cols = columns + let cols: TableColumn[] = columns .filter((col) => col?.hidden != true) .map((col) => { let hidden: boolean = col.hidden ?? false; @@ -267,6 +267,7 @@ export function InvenTreeTable>({ return { ...col, hidden: hidden, + noWrap: true, title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}` }; }); @@ -605,20 +606,22 @@ export function InvenTreeTable>({ return ( <> - - - + {!tableProps.noHeader && ( + + + + )} Date: Wed, 30 Oct 2024 03:50:37 +0000 Subject: [PATCH 4/9] revert BomTable --- src/frontend/src/pages/part/PartBomPanel.tsx | 49 ------- src/frontend/src/pages/part/PartDetail.tsx | 4 +- src/frontend/src/tables/bom/BomTable.tsx | 144 +++++++++---------- 3 files changed, 71 insertions(+), 126 deletions(-) delete mode 100644 src/frontend/src/pages/part/PartBomPanel.tsx diff --git a/src/frontend/src/pages/part/PartBomPanel.tsx b/src/frontend/src/pages/part/PartBomPanel.tsx deleted file mode 100644 index 027139a77721..000000000000 --- a/src/frontend/src/pages/part/PartBomPanel.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { t } from '@lingui/macro'; -import { Alert, Loader, Stack, Text } from '@mantine/core'; -import { IconLock } from '@tabler/icons-react'; -import { useState } from 'react'; - -import ImporterDrawer from '../../components/importer/ImporterDrawer'; -import { useUserState } from '../../states/UserState'; -import { BomTable } from '../../tables/bom/BomTable'; - -export default function PartBomPanel({ part }: { part: any }) { - const user = useUserState(); - - const [importOpened, setImportOpened] = useState(false); - - const [selectedSession, setSelectedSession] = useState( - undefined - ); - - if (!part.pk) { - return ; - } - - return ( - <> - - {part?.locked && ( - } - p="xs" - > - {t`Bill of materials cannot be edited, as the part is locked`} - - )} - - - { - setSelectedSession(undefined); - setImportOpened(false); - // TODO: Refresh / reload the BOM table - }} - /> - - ); -} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index dcb17507d701..5627ca25401c 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -85,6 +85,7 @@ import { useUserSettingsState } from '../../states/SettingsState'; import { useUserState } from '../../states/UserState'; +import { BomTable } from '../../tables/bom/BomTable'; import { UsedInTable } from '../../tables/bom/UsedInTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { PartParameterTable } from '../../tables/part/PartParameterTable'; @@ -99,7 +100,6 @@ import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; import PartAllocationPanel from './PartAllocationPanel'; -import PartBomPanel from './PartBomPanel'; import PartPricingPanel from './PartPricingPanel'; import PartSchedulingDetail from './PartSchedulingDetail'; import PartStocktakeDetail from './PartStocktakeDetail'; @@ -625,7 +625,7 @@ export default function PartDetail() { label: t`Bill of Materials`, icon: , hidden: !part.assembly, - content: + content: part.pk ? : }, { name: 'builds', diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx index 3adbb0c8830b..665510d65aca 100644 --- a/src/frontend/src/tables/bom/BomTable.tsx +++ b/src/frontend/src/tables/bom/BomTable.tsx @@ -1,16 +1,13 @@ import { t } from '@lingui/macro'; -import { ActionIcon, Alert, Group, Stack, Text } from '@mantine/core'; +import { Alert, Group, Stack, Text } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { IconArrowRight, - IconChevronDown, - IconChevronRight, IconCircleCheck, IconFileArrowLeft, IconLock, IconSwitch3 } from '@tabler/icons-react'; -import { DataTableRowExpansionProps } from 'mantine-datatable'; import { ReactNode, useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -65,20 +62,23 @@ function availableStockQuantity(record: any): number { } export function BomTable({ - part, - simplified, + partId, + partLocked, params = {} }: Readonly<{ - part: any; - simplified?: boolean; + partId: number; + partLocked?: boolean; params?: any; }>) { const user = useUserState(); const table = useTable('bom'); const navigate = useNavigate(); - const partId = useMemo(() => part.pk, [part]); - const partLocked = useMemo(() => part.locked, [part]); + const [importOpened, setImportOpened] = useState(false); + + const [selectedSession, setSelectedSession] = useState( + undefined + ); const tableColumns: TableColumn[] = useMemo(() => { return [ @@ -98,28 +98,17 @@ export function BomTable({ return ( part && ( - - {part.assembly && ( - - {table.isRowExpanded(record.pk) ? ( - - ) : ( - - )} - - )} - - } - extra={extra} - title={t`Part Information`} - /> - + + } + extra={extra} + title={t`Part Information`} + /> ) ); } @@ -307,7 +296,7 @@ export function BomTable({ }, NoteColumn({}) ]; - }, [partId, params, table.isRowExpanded]); + }, [partId, params]); const tableFilters: TableFilter[] = useMemo(() => { return [ @@ -389,9 +378,8 @@ export function BomTable({ title: t`Import BOM Data`, fields: importSessionFields, onFormSuccess: (response: any) => { - // TODO: Open import session drawer - // setSelectedSession(response.pk); - // setImportOpened(true); + setSelectedSession(response.pk); + setImportOpened(true); } }); @@ -541,47 +529,53 @@ export function BomTable({ ]; }, [partLocked, user]); - // Control row-expansion for multi-level BOMs - const rowExpansion: DataTableRowExpansionProps = useMemo(() => { - return { - allowMultiple: true, - expandable: ({ record }: { record: any }) => { - return ( - table.isRowExpanded(record.pk) || record.sub_part_detail.assembly - ); - }, - content: ({ record }: { record: any }) => { - return ; - } - }; - }, [table.isRowExpanded]); - return ( <> - {!simplified && importBomItem.modal} - {!simplified && newBomItem.modal} - {!simplified && editBomItem.modal} - {!simplified && validateBom.modal} - {!simplified && deleteBomItem.modal} - - + {partLocked && ( + } + p="xs" + > + {t`Bill of materials cannot be edited, as the part is locked`} + + )} + + + { + setSelectedSession(undefined); + setImportOpened(false); + table.refreshTable(); }} /> From 17e28ea8c60c29aab405bce8daf40c605c99c959 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 30 Oct 2024 03:53:46 +0000 Subject: [PATCH 5/9] Re-add "box" component --- src/frontend/src/tables/InvenTreeTable.tsx | 111 +++++++++++---------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/src/frontend/src/tables/InvenTreeTable.tsx b/src/frontend/src/tables/InvenTreeTable.tsx index f3a09a07e717..99d5bca5fa55 100644 --- a/src/frontend/src/tables/InvenTreeTable.tsx +++ b/src/frontend/src/tables/InvenTreeTable.tsx @@ -1,4 +1,5 @@ import { t } from '@lingui/macro'; +import { Box, Stack } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import { DataTable, @@ -606,60 +607,64 @@ export function InvenTreeTable>({ return ( <> - {!tableProps.noHeader && ( - - + + {!tableProps.noHeader && ( + + + + )} + + + (theme) => ({ + // TODO @SchrodingersGat : Need a better way of handling "wide" cells, + overflow: 'hidden' + }) + }} + {...optionalParams} + /> + - )} - - (theme) => ({ - // TODO @SchrodingersGat : Need a better way of handling "wide" cells, - overflow: 'hidden' - }) - }} - {...optionalParams} - /> - + ); } From b8342cb4cda15824d80aaaa6a8098d2d5cc9ae99 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 30 Oct 2024 04:04:30 +0000 Subject: [PATCH 6/9] Reimplement partlocked attribute --- src/frontend/src/pages/part/PartDetail.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 5627ca25401c..c429f2f695cc 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -618,7 +618,11 @@ export default function PartDetail() { label: t`Allocations`, icon: , hidden: !part.component && !part.salable, - content: part.pk ? : + content: part?.pk ? ( + + ) : ( + + ) }, { name: 'bom', From 3f87e7041bd777c50ecd5810360373b114a6e130 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 1 Nov 2024 00:06:06 +1100 Subject: [PATCH 7/9] Fix for PartDetail - Revert to proper panels --- src/frontend/src/pages/part/PartDetail.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index c429f2f695cc..3a8baf259d16 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -618,18 +618,18 @@ export default function PartDetail() { label: t`Allocations`, icon: , hidden: !part.component && !part.salable, - content: part?.pk ? ( - - ) : ( - - ) + content: part.pk ? : }, { name: 'bom', label: t`Bill of Materials`, icon: , hidden: !part.assembly, - content: part.pk ? : + content: part?.pk ? ( + + ) : ( + + ) }, { name: 'builds', From 3f135c80b909fb99ed51efc2c05bdaa11ecefbe7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 1 Nov 2024 00:06:35 +1100 Subject: [PATCH 8/9] Updated playwright tests --- src/frontend/tests/pages/pui_part.spec.ts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 0c3f7e713718..3722246ad50f 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -23,6 +23,35 @@ test('Pages - Part - Locking', async ({ page }) => { await page.getByText('Part parameters cannot be').waitFor(); }); +test('Pages - Part - Allocations', async ({ page }) => { + await doQuickLogin(page); + + // Let's look at the allocations for a single stock item + await page.goto(`${baseUrl}/stock/item/324/`); + await page.getByRole('tab', { name: 'Allocations' }).click(); + + await page.getByRole('button', { name: 'Build Order Allocations' }).waitFor(); + await page.getByRole('cell', { name: 'Making some blue chairs' }).waitFor(); + await page.getByRole('cell', { name: 'Making tables for SO 0003' }).waitFor(); + + // Let's look at the allocations for the entire part + await page.getByRole('tab', { name: 'Details' }).click(); + await page.getByRole('link', { name: 'Leg' }).click(); + + await page.getByRole('tab', { name: 'Part Details' }).click(); + await page.getByText('660 / 760').waitFor(); + + await page.getByRole('tab', { name: 'Allocations' }).click(); + + // Number of table records + await page.getByText('1 - 4 / 4').waitFor(); + await page.getByRole('cell', { name: 'Making red square tables' }).waitFor(); + + // Navigate through to the build order + await page.getByRole('cell', { name: 'BO0007' }).click(); + await page.getByRole('tab', { name: 'Build Details' }).waitFor(); +}); + test('Pages - Part - Pricing (Nothing, BOM)', async ({ page }) => { await doQuickLogin(page); From 5ff155e2e2d5846568ce2e856a0705999f36f3ff Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 1 Nov 2024 00:08:03 +1100 Subject: [PATCH 9/9] Additional tests --- src/frontend/tests/pages/pui_part.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 3722246ad50f..4b85d6386876 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -18,6 +18,11 @@ test('Pages - Part - Locking', async ({ page }) => { await page.getByLabel('part-lock-icon').waitFor(); await page.getByText('Part is Locked', { exact: true }).waitFor(); + // Check expected "badge" values + await page.getByText('In Stock: 13').waitFor(); + await page.getByText('Required: 10').waitFor(); + await page.getByText('In Production: 50').waitFor(); + // Check the "parameters" tab also await page.getByRole('tab', { name: 'Parameters' }).click(); await page.getByText('Part parameters cannot be').waitFor();