mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[PUI] Table refactor (#8395)
* Refactor table header items out into new file * Improve BomItem API query * Allow table header to be removed entirely * revert BomTable * Re-add "box" component * Reimplement partlocked attribute * Fix for PartDetail - Revert to proper panels * Updated playwright tests * Additional tests
This commit is contained in:
parent
801b32e4e3
commit
871cd905f1
@ -1708,6 +1708,10 @@ class BomItemSerializer(
|
|||||||
'sub_part__stock_items__sales_order_allocations',
|
'sub_part__stock_items__sales_order_allocations',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queryset = queryset.select_related(
|
||||||
|
'part__pricing_data', 'sub_part__pricing_data'
|
||||||
|
)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'substitutes', 'substitutes__part__stock_items'
|
'substitutes', 'substitutes__part__stock_items'
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { randomId, useLocalStorage } from '@mantine/hooks';
|
import { randomId, useLocalStorage } from '@mantine/hooks';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { SetURLSearchParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { TableFilter } from '../tables/Filter';
|
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.
|
* 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.
|
* 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)
|
* 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
|
* 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
|
* hiddenColumns: An array of hidden column names
|
||||||
|
* setHiddenColumns: A function to set the hidden columns
|
||||||
* searchTerm: The current search term for the table
|
* 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 = {
|
export type TableState = {
|
||||||
tableKey: string;
|
tableKey: string;
|
||||||
refreshTable: () => void;
|
refreshTable: () => void;
|
||||||
activeFilters: TableFilter[];
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
setIsLoading: (value: boolean) => void;
|
setIsLoading: (value: boolean) => void;
|
||||||
|
activeFilters: TableFilter[];
|
||||||
setActiveFilters: (filters: TableFilter[]) => void;
|
setActiveFilters: (filters: TableFilter[]) => void;
|
||||||
clearActiveFilters: () => void;
|
clearActiveFilters: () => void;
|
||||||
|
queryFilters: URLSearchParams;
|
||||||
|
setQueryFilters: SetURLSearchParams;
|
||||||
|
clearQueryFilters: () => void;
|
||||||
expandedRecords: any[];
|
expandedRecords: any[];
|
||||||
setExpandedRecords: (records: any[]) => void;
|
setExpandedRecords: (records: any[]) => void;
|
||||||
isRowExpanded: (pk: number) => boolean;
|
isRowExpanded: (pk: number) => boolean;
|
||||||
@ -42,8 +71,6 @@ export type TableState = {
|
|||||||
records: any[];
|
records: any[];
|
||||||
setRecords: (records: any[]) => void;
|
setRecords: (records: any[]) => void;
|
||||||
updateRecord: (record: 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()}`;
|
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<string>(generateTableName());
|
const [tableKey, setTableKey] = useState<string>(generateTableName());
|
||||||
|
|
||||||
// Callback used to refresh (reload) the table
|
// Callback used to refresh (reload) the table
|
||||||
@ -145,8 +179,6 @@ export function useTable(tableName: string): TableState {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const [editable, setEditable] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tableKey,
|
tableKey,
|
||||||
refreshTable,
|
refreshTable,
|
||||||
@ -155,6 +187,9 @@ export function useTable(tableName: string): TableState {
|
|||||||
activeFilters,
|
activeFilters,
|
||||||
setActiveFilters,
|
setActiveFilters,
|
||||||
clearActiveFilters,
|
clearActiveFilters,
|
||||||
|
queryFilters,
|
||||||
|
setQueryFilters,
|
||||||
|
clearQueryFilters,
|
||||||
expandedRecords,
|
expandedRecords,
|
||||||
setExpandedRecords,
|
setExpandedRecords,
|
||||||
isRowExpanded,
|
isRowExpanded,
|
||||||
@ -175,8 +210,6 @@ export function useTable(tableName: string): TableState {
|
|||||||
setPageSize,
|
setPageSize,
|
||||||
records,
|
records,
|
||||||
setRecords,
|
setRecords,
|
||||||
updateRecord,
|
updateRecord
|
||||||
editable,
|
|
||||||
setEditable
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -625,8 +625,10 @@ export default function PartDetail() {
|
|||||||
label: t`Bill of Materials`,
|
label: t`Bill of Materials`,
|
||||||
icon: <IconListTree />,
|
icon: <IconListTree />,
|
||||||
hidden: !part.assembly,
|
hidden: !part.assembly,
|
||||||
content: (
|
content: part?.pk ? (
|
||||||
<BomTable partId={part.pk ?? -1} partLocked={part?.locked == true} />
|
<BomTable partId={part.pk ?? -1} partLocked={part?.locked == true} />
|
||||||
|
) : (
|
||||||
|
<Skeleton />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,21 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { Box, Stack } from '@mantine/core';
|
||||||
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 { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
@ -23,20 +7,11 @@ import {
|
|||||||
DataTableRowExpansionProps,
|
DataTableRowExpansionProps,
|
||||||
DataTableSortStatus
|
DataTableSortStatus
|
||||||
} from 'mantine-datatable';
|
} from 'mantine-datatable';
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
Fragment,
|
import { useNavigate } from 'react-router-dom';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState
|
|
||||||
} from 'react';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { Boundary } from '../components/Boundary';
|
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 { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { resolveItem } from '../functions/conversion';
|
import { resolveItem } from '../functions/conversion';
|
||||||
@ -44,16 +19,12 @@ import { cancelEvent } from '../functions/events';
|
|||||||
import { extractAvailableFields, mapFields } from '../functions/forms';
|
import { extractAvailableFields, mapFields } from '../functions/forms';
|
||||||
import { navigateToLink } from '../functions/navigation';
|
import { navigateToLink } from '../functions/navigation';
|
||||||
import { getDetailUrl } from '../functions/urls';
|
import { getDetailUrl } from '../functions/urls';
|
||||||
import { useDeleteApiFormModal } from '../hooks/UseForm';
|
|
||||||
import { TableState } from '../hooks/UseTable';
|
import { TableState } from '../hooks/UseTable';
|
||||||
import { useLocalState } from '../states/LocalState';
|
import { useLocalState } from '../states/LocalState';
|
||||||
import { TableColumn } from './Column';
|
import { TableColumn } from './Column';
|
||||||
import { TableColumnSelect } from './ColumnSelect';
|
|
||||||
import { DownloadAction } from './DownloadAction';
|
|
||||||
import { TableFilter } from './Filter';
|
import { TableFilter } from './Filter';
|
||||||
import { FilterSelectDrawer } from './FilterSelectDrawer';
|
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
||||||
import { RowAction, RowActions } from './RowActions';
|
import { RowAction, RowActions } from './RowActions';
|
||||||
import { TableSearchInput } from './Search';
|
|
||||||
|
|
||||||
const defaultPageSize: number = 25;
|
const defaultPageSize: number = 25;
|
||||||
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
|
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
|
||||||
@ -84,6 +55,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 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 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 modelType: ModelType - The model type for the table
|
||||||
|
* @param noHeader: boolean - Hide the table header
|
||||||
*/
|
*/
|
||||||
export type InvenTreeTableProps<T = any> = {
|
export type InvenTreeTableProps<T = any> = {
|
||||||
params?: any;
|
params?: any;
|
||||||
@ -113,6 +85,7 @@ export type InvenTreeTableProps<T = any> = {
|
|||||||
modelType?: ModelType;
|
modelType?: ModelType;
|
||||||
rowStyle?: (record: T, index: number) => any;
|
rowStyle?: (record: T, index: number) => any;
|
||||||
modelField?: string;
|
modelField?: string;
|
||||||
|
noHeader?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,9 +135,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
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
|
// Construct table filters - note that we can introspect filter labels from column names
|
||||||
const filters: TableFilter[] = useMemo(() => {
|
const filters: TableFilter[] = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@ -286,7 +256,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
|
|
||||||
// Update column visibility when hiddenColumns change
|
// Update column visibility when hiddenColumns change
|
||||||
const dataColumns: any = useMemo(() => {
|
const dataColumns: any = useMemo(() => {
|
||||||
let cols = columns
|
let cols: TableColumn[] = columns
|
||||||
.filter((col) => col?.hidden != true)
|
.filter((col) => col?.hidden != true)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
let hidden: boolean = col.hidden ?? false;
|
let hidden: boolean = col.hidden ?? false;
|
||||||
@ -298,6 +268,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
hidden: hidden,
|
hidden: hidden,
|
||||||
|
noWrap: true,
|
||||||
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`
|
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -344,18 +315,22 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter list visibility
|
|
||||||
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Reset the pagination state when the search term changes
|
// Reset the pagination state when the search term changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tableState.setPage(1);
|
tableState.setPage(1);
|
||||||
}, [tableState.searchTerm]);
|
}, [tableState.searchTerm]);
|
||||||
|
|
||||||
|
// Data Sorting
|
||||||
|
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<T>>({
|
||||||
|
columnAccessor: tableProps.defaultSortColumn ?? '',
|
||||||
|
direction: 'asc'
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct query filters for the current table
|
* Construct query filters for the current table
|
||||||
*/
|
*/
|
||||||
function getTableFilters(paginate: boolean = false) {
|
const getTableFilters = useCallback(
|
||||||
|
(paginate: boolean = false) => {
|
||||||
let queryParams = {
|
let queryParams = {
|
||||||
...tableProps.params
|
...tableProps.params
|
||||||
};
|
};
|
||||||
@ -368,8 +343,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allow override of filters based on URL query parameters
|
// Allow override of filters based on URL query parameters
|
||||||
if (urlQueryParams) {
|
if (tableState.queryFilters) {
|
||||||
for (let [key, value] of urlQueryParams) {
|
for (let [key, value] of tableState.queryFilters) {
|
||||||
queryParams[key] = value;
|
queryParams[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,30 +374,19 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return queryParams;
|
return queryParams;
|
||||||
}
|
},
|
||||||
|
[
|
||||||
// Data download callback
|
tableProps.params,
|
||||||
function downloadData(fileFormat: string) {
|
tableProps.enablePagination,
|
||||||
// Download entire dataset (no pagination)
|
tableState.activeFilters,
|
||||||
let queryParams = getTableFilters(false);
|
tableState.queryFilters,
|
||||||
|
tableState.searchTerm,
|
||||||
// Specify file format
|
tableState.pageSize,
|
||||||
queryParams.export = fileFormat;
|
tableState.setPageSize,
|
||||||
|
sortStatus,
|
||||||
let downloadUrl = api.getUri({
|
getOrderingTerm
|
||||||
url: url,
|
]
|
||||||
params: queryParams
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// Download file in a new window (to force download)
|
|
||||||
window.open(downloadUrl, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data Sorting
|
|
||||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus<T>>({
|
|
||||||
columnAccessor: tableProps.defaultSortColumn ?? '',
|
|
||||||
direction: 'asc'
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableKey: string = tableState.tableKey.split('-')[0];
|
const tableKey: string = tableState.tableKey.split('-')[0];
|
||||||
@ -538,7 +502,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
// Refetch data when the query parameters change
|
// Refetch data when the query parameters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch();
|
refetch();
|
||||||
}, [urlQueryParams]);
|
}, [tableState.queryFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tableState.setIsLoading(
|
tableState.setIsLoading(
|
||||||
@ -559,35 +523,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const deleteRecords = useDeleteApiFormModal({
|
|
||||||
url: url,
|
|
||||||
title: t`Delete Selected Items`,
|
|
||||||
preFormContent: (
|
|
||||||
<Alert
|
|
||||||
color="red"
|
|
||||||
title={t`Are you sure you want to delete the selected items?`}
|
|
||||||
>
|
|
||||||
{t`This action cannot be undone`}
|
|
||||||
</Alert>
|
|
||||||
),
|
|
||||||
initialData: {
|
|
||||||
items: tableState.selectedIds
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
items: {
|
|
||||||
hidden: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onFormSuccess: () => {
|
|
||||||
tableState.clearSelectedRecords();
|
|
||||||
tableState.refreshTable();
|
|
||||||
|
|
||||||
if (props.afterBulkDelete) {
|
|
||||||
props.afterBulkDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Callback when a cell is clicked
|
// Callback when a cell is clicked
|
||||||
const handleCellClick = useCallback(
|
const handleCellClick = useCallback(
|
||||||
({
|
({
|
||||||
@ -672,122 +607,24 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{deleteRecords.modal}
|
<Stack gap="xs">
|
||||||
{tableProps.enableFilters && (filters.length ?? 0) > 0 && (
|
{!tableProps.noHeader && (
|
||||||
<Boundary label="table-filter-drawer">
|
<Boundary label={`InvenTreeTableHeader-${tableState.tableKey}`}>
|
||||||
<FilterSelectDrawer
|
<InvenTreeTableHeader
|
||||||
availableFilters={filters}
|
tableUrl={url}
|
||||||
tableState={tableState}
|
tableState={tableState}
|
||||||
opened={filtersVisible}
|
tableProps={tableProps}
|
||||||
onClose={() => setFiltersVisible(false)}
|
hasSwitchableColumns={hasSwitchableColumns}
|
||||||
|
columns={dataColumns}
|
||||||
|
filters={filters}
|
||||||
|
toggleColumn={toggleColumn}
|
||||||
/>
|
/>
|
||||||
</Boundary>
|
</Boundary>
|
||||||
)}
|
)}
|
||||||
<Boundary label={`InvenTreeTable-${tableState.tableKey}`}>
|
<Boundary label={`InvenTreeTable-${tableState.tableKey}`}>
|
||||||
<Stack gap="sm">
|
|
||||||
<Group justify="apart" grow wrap="nowrap">
|
|
||||||
<Group justify="left" key="custom-actions" gap={5} wrap="nowrap">
|
|
||||||
<PrintingActions
|
|
||||||
items={tableState.selectedIds}
|
|
||||||
modelType={tableProps.modelType}
|
|
||||||
enableLabels={tableProps.enableLabels}
|
|
||||||
enableReports={tableProps.enableReports}
|
|
||||||
/>
|
|
||||||
{(tableProps.barcodeActions?.length ?? 0) > 0 && (
|
|
||||||
<ButtonMenu
|
|
||||||
key="barcode-actions"
|
|
||||||
icon={<IconBarcode />}
|
|
||||||
label={t`Barcode Actions`}
|
|
||||||
tooltip={t`Barcode Actions`}
|
|
||||||
actions={tableProps.barcodeActions ?? []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tableProps.enableBulkDelete && (
|
|
||||||
<ActionButton
|
|
||||||
disabled={!tableState.hasSelectedRecords}
|
|
||||||
icon={<IconTrash />}
|
|
||||||
color="red"
|
|
||||||
tooltip={t`Delete selected records`}
|
|
||||||
onClick={() => {
|
|
||||||
deleteRecords.open();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tableProps.tableActions?.map((group, idx) => (
|
|
||||||
<Fragment key={idx}>{group}</Fragment>
|
|
||||||
))}
|
|
||||||
</Group>
|
|
||||||
<Space />
|
|
||||||
<Group justify="right" gap={5} wrap="nowrap">
|
|
||||||
{tableProps.enableSearch && (
|
|
||||||
<TableSearchInput
|
|
||||||
searchCallback={(term: string) =>
|
|
||||||
tableState.setSearchTerm(term)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tableProps.enableRefresh && (
|
|
||||||
<ActionIcon variant="transparent" aria-label="table-refresh">
|
|
||||||
<Tooltip label={t`Refresh data`}>
|
|
||||||
<IconRefresh
|
|
||||||
onClick={() => {
|
|
||||||
refetch();
|
|
||||||
tableState.clearSelectedRecords();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
{hasSwitchableColumns && (
|
|
||||||
<TableColumnSelect
|
|
||||||
columns={dataColumns}
|
|
||||||
onToggleColumn={toggleColumn}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{urlQueryParams.size > 0 && (
|
|
||||||
<ActionIcon
|
|
||||||
variant="transparent"
|
|
||||||
color="red"
|
|
||||||
aria-label="table-clear-query-filters"
|
|
||||||
>
|
|
||||||
<Tooltip label={t`Clear custom query filters`}>
|
|
||||||
<IconFilterCancel
|
|
||||||
onClick={() => {
|
|
||||||
setUrlQueryParams({});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
{tableProps.enableFilters && filters.length > 0 && (
|
|
||||||
<Indicator
|
|
||||||
size="xs"
|
|
||||||
label={tableState.activeFilters?.length ?? 0}
|
|
||||||
disabled={tableState.activeFilters?.length == 0}
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
variant="transparent"
|
|
||||||
aria-label="table-select-filters"
|
|
||||||
>
|
|
||||||
<Tooltip label={t`Table Filters`}>
|
|
||||||
<IconFilter
|
|
||||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ActionIcon>
|
|
||||||
</Indicator>
|
|
||||||
)}
|
|
||||||
{tableProps.enableDownload && (
|
|
||||||
<DownloadAction
|
|
||||||
key="download-action"
|
|
||||||
downloadCallback={downloadData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Box pos="relative">
|
<Box pos="relative">
|
||||||
<DataTable
|
<DataTable
|
||||||
withTableBorder
|
withTableBorder={!tableProps.noHeader}
|
||||||
withColumnBorders
|
withColumnBorders
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
@ -814,6 +651,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
records={tableState.records}
|
records={tableState.records}
|
||||||
columns={dataColumns}
|
columns={dataColumns}
|
||||||
onCellClick={handleCellClick}
|
onCellClick={handleCellClick}
|
||||||
|
noHeader={tableProps.noHeader ?? false}
|
||||||
defaultColumnProps={{
|
defaultColumnProps={{
|
||||||
noWrap: true,
|
noWrap: true,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
@ -825,8 +663,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
|||||||
{...optionalParams}
|
{...optionalParams}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
|
||||||
</Boundary>
|
</Boundary>
|
||||||
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
239
src/frontend/src/tables/InvenTreeTableHeader.tsx
Normal file
239
src/frontend/src/tables/InvenTreeTableHeader.tsx
Normal file
@ -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<any>;
|
||||||
|
hasSwitchableColumns: boolean;
|
||||||
|
columns: any;
|
||||||
|
filters: TableFilter[];
|
||||||
|
toggleColumn: (column: string) => void;
|
||||||
|
}) {
|
||||||
|
// Filter list visibility
|
||||||
|
const [filtersVisible, setFiltersVisible] = useState<boolean>(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: (
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
title={t`Are you sure you want to delete the selected items?`}
|
||||||
|
>
|
||||||
|
{t`This action cannot be undone`}
|
||||||
|
</Alert>
|
||||||
|
),
|
||||||
|
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 && (
|
||||||
|
<Boundary label={`InvenTreeTableFilterDrawer-${tableState.tableKey}`}>
|
||||||
|
<FilterSelectDrawer
|
||||||
|
availableFilters={filters}
|
||||||
|
tableState={tableState}
|
||||||
|
opened={filtersVisible}
|
||||||
|
onClose={() => setFiltersVisible(false)}
|
||||||
|
/>
|
||||||
|
</Boundary>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group justify="apart" grow wrap="nowrap">
|
||||||
|
<Group justify="left" key="custom-actions" gap={5} wrap="nowrap">
|
||||||
|
<PrintingActions
|
||||||
|
items={tableState.selectedIds}
|
||||||
|
modelType={tableProps.modelType}
|
||||||
|
enableLabels={tableProps.enableLabels}
|
||||||
|
enableReports={tableProps.enableReports}
|
||||||
|
/>
|
||||||
|
{(tableProps.barcodeActions?.length ?? 0) > 0 && (
|
||||||
|
<ButtonMenu
|
||||||
|
key="barcode-actions"
|
||||||
|
icon={<IconBarcode />}
|
||||||
|
label={t`Barcode Actions`}
|
||||||
|
tooltip={t`Barcode Actions`}
|
||||||
|
actions={tableProps.barcodeActions ?? []}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tableProps.enableBulkDelete && (
|
||||||
|
<ActionButton
|
||||||
|
disabled={!tableState.hasSelectedRecords}
|
||||||
|
icon={<IconTrash />}
|
||||||
|
color="red"
|
||||||
|
tooltip={t`Delete selected records`}
|
||||||
|
onClick={() => {
|
||||||
|
deleteRecords.open();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tableProps.tableActions?.map((group, idx) => (
|
||||||
|
<Fragment key={idx}>{group}</Fragment>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
<Space />
|
||||||
|
<Group justify="right" gap={5} wrap="nowrap">
|
||||||
|
{tableProps.enableSearch && (
|
||||||
|
<TableSearchInput
|
||||||
|
searchCallback={(term: string) => tableState.setSearchTerm(term)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tableProps.enableRefresh && (
|
||||||
|
<ActionIcon variant="transparent" aria-label="table-refresh">
|
||||||
|
<Tooltip label={t`Refresh data`}>
|
||||||
|
<IconRefresh
|
||||||
|
onClick={() => {
|
||||||
|
tableState.refreshTable();
|
||||||
|
tableState.clearSelectedRecords();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
{hasSwitchableColumns && (
|
||||||
|
<TableColumnSelect
|
||||||
|
columns={columns}
|
||||||
|
onToggleColumn={toggleColumn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tableState.queryFilters.size > 0 && (
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
color="red"
|
||||||
|
aria-label="table-clear-query-filters"
|
||||||
|
>
|
||||||
|
<Tooltip label={t`Clear custom query filters`}>
|
||||||
|
<IconFilterCancel
|
||||||
|
onClick={() => {
|
||||||
|
tableState.clearQueryFilters();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
{tableProps.enableFilters && filters.length > 0 && (
|
||||||
|
<Indicator
|
||||||
|
size="xs"
|
||||||
|
label={tableState.activeFilters?.length ?? 0}
|
||||||
|
disabled={tableState.activeFilters?.length == 0}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
aria-label="table-select-filters"
|
||||||
|
>
|
||||||
|
<Tooltip label={t`Table Filters`}>
|
||||||
|
<IconFilter
|
||||||
|
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionIcon>
|
||||||
|
</Indicator>
|
||||||
|
)}
|
||||||
|
{tableProps.enableDownload && (
|
||||||
|
<DownloadAction
|
||||||
|
key="download-action"
|
||||||
|
downloadCallback={downloadData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,11 +18,45 @@ test('Pages - Part - Locking', async ({ page }) => {
|
|||||||
await page.getByLabel('part-lock-icon').waitFor();
|
await page.getByLabel('part-lock-icon').waitFor();
|
||||||
await page.getByText('Part is Locked', { exact: true }).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
|
// Check the "parameters" tab also
|
||||||
await page.getByRole('tab', { name: 'Parameters' }).click();
|
await page.getByRole('tab', { name: 'Parameters' }).click();
|
||||||
await page.getByText('Part parameters cannot be').waitFor();
|
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 }) => {
|
test('Pages - Part - Pricing (Nothing, BOM)', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user