2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 19:20:55 +00:00

Table default cols (#9868)

* Work in progress

- Seems to reset the columns on page refresh
- Probably related to the useLocalStorage hook

* Do not overwrite until the tablestate is loaded

* Prevent table fetch until data has been loaded from localStorage

* Improved persistance

* Adjust default column visibility

* Adjust playwright test
This commit is contained in:
Oliver
2025-06-26 15:27:26 +10:00
committed by GitHub
parent 8b4f9efa44
commit c283beedb3
21 changed files with 197 additions and 72 deletions

View File

@ -52,7 +52,7 @@ export type TableState = {
hasSelectedRecords: boolean;
setSelectedRecords: (records: any[]) => void;
clearSelectedRecords: () => void;
hiddenColumns: string[];
hiddenColumns: string[] | null;
setHiddenColumns: (columns: string[]) => void;
searchTerm: string;
setSearchTerm: (term: string) => void;
@ -62,6 +62,7 @@ export type TableState = {
setPage: (page: number) => void;
pageSize: number;
setPageSize: (pageSize: number) => void;
storedDataLoaded: boolean;
records: any[];
setRecords: (records: any[]) => void;
updateRecord: (record: any) => void;

View File

@ -65,19 +65,36 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
// Total record count
const [recordCount, setRecordCount] = useState<number>(0);
const [pageSizeLoaded, setPageSizeLoaded] = useState<boolean>(false);
// Pagination data
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useLocalStorage<number>({
key: 'inventree-table-page-size',
defaultValue: 25
defaultValue: 25,
deserialize: (value: string | undefined) => {
setPageSizeLoaded(true);
return value === undefined ? 25 : JSON.parse(value);
}
});
const [hiddenColumnsLoaded, setHiddenColumnsLoaded] =
useState<boolean>(false);
// A list of hidden columns, saved to local storage
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[] | null>({
key: `inventree-hidden-table-columns-${tableName}`,
defaultValue: []
defaultValue: null,
deserialize: (value) => {
setHiddenColumnsLoaded(true);
return value === undefined ? null : JSON.parse(value);
}
});
const storedDataLoaded = useMemo(() => {
return pageSizeLoaded && hiddenColumnsLoaded;
}, [pageSizeLoaded, hiddenColumnsLoaded]);
// Search term
const [searchTerm, setSearchTerm] = useState<string>('');
@ -137,6 +154,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
setPage,
pageSize,
setPageSize,
storedDataLoaded,
records,
setRecords,
updateRecord,

View File

@ -9,7 +9,8 @@ import type { ApiFormFieldType } from '@lib/types/Forms';
* @param ordering - The key in the record to sort by (defaults to accessor)
* @param sortable - Whether the column is sortable
* @param switchable - Whether the column is switchable
* @param hidden - Whether the column is hidden
* @param defaultVisible - Whether the column is visible by default (defaults to true)
* @param hidden - Whether the column is hidden (forced hidden, cannot be toggled by the user))
* @param editable - Whether the value of this column can be edited
* @param definition - Optional field definition for the column
* @param render - A custom render function
@ -31,6 +32,7 @@ export type TableColumnProps<T = any> = {
sortable?: boolean;
switchable?: boolean;
hidden?: boolean;
defaultVisible?: boolean;
editable?: boolean;
definition?: ApiFormFieldType;
render?: (record: T, index?: number) => any;

View File

@ -105,6 +105,7 @@ export function LinkColumn(props: TableColumnProps): TableColumn {
return {
accessor: 'link',
sortable: false,
defaultVisible: false,
render: (record: any) => {
const url = resolveItem(record, props.accessor ?? 'link');

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import { Box, type MantineStyleProp, Stack } from '@mantine/core';
import { Box, type MantineStyleProp, Skeleton, Stack } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import {
type ContextMenuItemOptions,
@ -186,7 +186,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
// Request OPTIONS data from the API, before we load the table
const tableOptionQuery = useQuery({
enabled: !!url && !tableData,
enabled: !!url && !tableData && tableState.storedDataLoaded,
queryKey: [
'options',
url,
@ -273,6 +273,30 @@ export function InvenTreeTable<T extends Record<string, any>>({
return tableProps.enableSelection || tableProps.enableBulkDelete || false;
}, [tableProps]);
useEffect(() => {
// On first table render, "hide" any default hidden columns
if (tableProps.enableColumnSwitching == false) {
return;
}
// A "null" value indicates that the initial "hidden" columns have not been set
if (tableState.storedDataLoaded && tableState.hiddenColumns == null) {
const columnNames: string[] = columns
.filter((col) => {
// Find any switchable columns which are hidden by default
return col.switchable != false && col.defaultVisible == false;
})
.map((col) => col.accessor);
tableState.setHiddenColumns(columnNames);
}
}, [
columns,
tableProps.enableColumnSwitching,
tableState.hiddenColumns,
tableState.storedDataLoaded
]);
// Check if any columns are switchable (can be hidden)
const hasSwitchableColumns: boolean = useMemo(() => {
if (props.enableColumnSwitching == false) {
@ -300,15 +324,21 @@ export function InvenTreeTable<T extends Record<string, any>>({
// Update column visibility when hiddenColumns change
const dataColumns: any = useMemo(() => {
const cols: TableColumn[] = columns
.filter((col) => col?.hidden != true)
.map((col) => {
let hidden: boolean = col.hidden ?? false;
let cols: TableColumn[] = columns.filter((col) => col?.hidden != true);
if (col.switchable ?? true) {
hidden = tableState.hiddenColumns.includes(col.accessor);
if (!tableState.storedDataLoaded) {
cols = cols.filter((col) => col?.defaultVisible != false);
}
cols = cols.map((col) => {
// If the column is *not* switchable, it is always visible
// Otherwise, check if it is "default hidden"
const hidden: boolean =
col.switchable == false
? false
: (tableState.hiddenColumns?.includes(col.accessor) ?? false);
return {
...col,
hidden: hidden,
@ -342,7 +372,8 @@ export function InvenTreeTable<T extends Record<string, any>>({
fieldNames,
tableProps.rowActions,
tableState.hiddenColumns,
tableState.selectedRecords
tableState.selectedRecords,
tableState.storedDataLoaded
]);
// Callback when column visibility is toggled
@ -583,7 +614,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
tableState.filterSet.activeFilters,
tableState.searchTerm
],
enabled: !!url && !tableData,
enabled: !!url && !tableData && tableState.storedDataLoaded,
queryFn: fetchTableData,
refetchOnMount: true
});
@ -784,6 +815,10 @@ export function InvenTreeTable<T extends Record<string, any>>({
);
}, [tableProps.onCellClick, tableProps.onRowClick, tableProps.modelType]);
if (!tableState.storedDataLoaded) {
return <Skeleton w='100%' h='100%' animate />;
}
return (
<>
<Stack gap='xs'>

View File

@ -145,6 +145,7 @@ export function BomTable({
},
{
accessor: 'substitutes',
defaultVisible: false,
render: (row) => {
const substitutes = row.substitutes ?? [];
@ -162,21 +163,24 @@ export function BomTable({
}
},
BooleanColumn({
accessor: 'optional'
accessor: 'optional',
defaultVisible: false
}),
BooleanColumn({
accessor: 'consumable'
accessor: 'consumable',
defaultVisible: false
}),
BooleanColumn({
accessor: 'allow_variants'
accessor: 'allow_variants',
defaultVisible: false
}),
BooleanColumn({
accessor: 'inherited'
// TODO: Custom renderer for this column
// TODO: See bom.js for existing implementation
accessor: 'inherited',
defaultVisible: false
}),
BooleanColumn({
accessor: 'validated'
accessor: 'validated',
defaultVisible: false
}),
{
accessor: 'price_range',
@ -184,6 +188,7 @@ export function BomTable({
ordering: 'pricing_max',
sortable: true,
switchable: true,
defaultVisible: false,
render: (record: any) =>
formatPriceRange(record.pricing_min, record.pricing_max)
},

View File

@ -45,7 +45,8 @@ export function UsedInTable({
{
accessor: 'part_detail.revision',
title: t`Revision`,
sortable: true
sortable: true,
defaultVisible: false
},
DescriptionColumn({
accessor: 'part_detail.description'

View File

@ -340,33 +340,39 @@ export default function BuildLineTable({
BooleanColumn({
accessor: 'bom_item_detail.optional',
ordering: 'optional',
hidden: hasOutput
hidden: hasOutput,
defaultVisible: false
}),
BooleanColumn({
accessor: 'bom_item_detail.consumable',
ordering: 'consumable',
hidden: hasOutput
hidden: hasOutput,
defaultVisible: false
}),
BooleanColumn({
accessor: 'bom_item_detail.allow_variants',
ordering: 'allow_variants',
hidden: hasOutput
hidden: hasOutput,
defaultVisible: false
}),
BooleanColumn({
accessor: 'bom_item_detail.inherited',
ordering: 'inherited',
title: t`Gets Inherited`,
hidden: hasOutput
hidden: hasOutput,
defaultVisible: false
}),
BooleanColumn({
accessor: 'part_detail.trackable',
ordering: 'trackable',
hidden: hasOutput
hidden: hasOutput,
defaultVisible: false
}),
{
accessor: 'bom_item_detail.quantity',
sortable: true,
title: t`Unit Quantity`,
defaultVisible: false,
ordering: 'unit_quantity',
render: (record: any) => {
return (
@ -383,6 +389,7 @@ export default function BuildLineTable({
accessor: 'quantity',
title: t`Required Quantity`,
sortable: true,
defaultVisible: false,
switchable: false,
render: (record: any) => {
// If a build output is specified, use the provided quantity

View File

@ -82,7 +82,8 @@ export function BuildOrderTable({
{
accessor: 'part_detail.revision',
title: t`Revision`,
sortable: true
sortable: true,
defaultVisible: false
},
{
accessor: 'title',
@ -101,16 +102,20 @@ export function BuildOrderTable({
)
},
StatusColumn({ model: ModelType.build }),
ProjectCodeColumn({}),
ProjectCodeColumn({
defaultVisible: false
}),
{
accessor: 'level',
sortable: true,
switchable: true,
hidden: !parentBuildId
hidden: !parentBuildId,
defaultVisible: false
},
{
accessor: 'priority',
sortable: true
sortable: true,
defaultVisible: false
},
BooleanColumn({
accessor: 'external',
@ -119,8 +124,12 @@ export function BuildOrderTable({
switchable: true,
hidden: !globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
}),
CreationDateColumn({}),
StartDateColumn({}),
CreationDateColumn({
defaultVisible: false
}),
StartDateColumn({
defaultVisible: false
}),
TargetDateColumn({}),
DateColumn({
accessor: 'completion_date',

View File

@ -368,15 +368,19 @@ export default function ParametricPartTable({
const partColumns: TableColumn[] = [
{
accessor: 'name',
title: t`Part`,
sortable: true,
switchable: false,
noWrap: true,
render: (record: any) => PartColumn({ part: record })
},
DescriptionColumn({}),
DescriptionColumn({
defaultVisible: false
}),
{
accessor: 'IPN',
sortable: true
sortable: true,
defaultVisible: false
},
{
accessor: 'total_in_stock',

View File

@ -65,6 +65,7 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
{
accessor: 'structural',
sortable: true,
defaultVisible: false,
render: (record: any) => {
return <YesNoButton value={record.structural} />;
}

View File

@ -48,7 +48,8 @@ export function PartParameterTable({
{
accessor: 'part_detail.IPN',
sortable: false,
switchable: true
switchable: true,
defaultVisible: false
},
{
accessor: 'template_detail.name',

View File

@ -61,7 +61,8 @@ function partTableColumns(): TableColumn[] {
{
accessor: 'default_location',
sortable: true,
render: (record: any) => record.default_location_detail?.pathstring
render: (record: any) => record.default_location_detail?.pathstring,
defaultVisible: false
},
{
accessor: 'total_in_stock',
@ -167,6 +168,7 @@ function partTableColumns(): TableColumn[] {
title: t`Price Range`,
sortable: true,
ordering: 'pricing_max',
defaultVisible: false,
render: (record: any) =>
formatPriceRange(record.pricing_min, record.pricing_max)
},

View File

@ -152,6 +152,7 @@ export function PurchaseOrderLineItemTable({
accessor: 'build_order',
title: t`Build Order`,
sortable: true,
defaultVisible: false,
render: (record: any) => {
if (record.build_order_detail) {
return (
@ -219,7 +220,8 @@ export function PurchaseOrderLineItemTable({
{
accessor: 'supplier_part_detail.packaging',
sortable: false,
title: t`Packaging`
title: t`Packaging`,
defaultVisible: false
},
{
accessor: 'supplier_part_detail.pack_quantity',
@ -236,13 +238,15 @@ export function PurchaseOrderLineItemTable({
LinkColumn({
accessor: 'supplier_part_detail.link',
title: t`Supplier Link`,
sortable: false
sortable: false,
defaultVisible: false
}),
{
accessor: 'mpn',
ordering: 'MPN',
title: t`Manufacturer Code`,
sortable: true
sortable: true,
defaultVisible: false
},
CurrencyColumn({
accessor: 'purchase_price',

View File

@ -123,10 +123,18 @@ export function PurchaseOrderTable({
},
LineItemsProgressColumn(),
StatusColumn({ model: ModelType.purchaseorder }),
ProjectCodeColumn({}),
CreationDateColumn({}),
CreatedByColumn({}),
StartDateColumn({}),
ProjectCodeColumn({
defaultVisible: false
}),
CreationDateColumn({
defaultVisible: false
}),
CreatedByColumn({
defaultVisible: false
}),
StartDateColumn({
defaultVisible: false
}),
TargetDateColumn({}),
CompletionDateColumn({
accessor: 'complete_date'

View File

@ -100,7 +100,8 @@ export function SupplierPartTable({
accessor: 'active',
title: t`Active`,
sortable: true,
switchable: true
switchable: true,
defaultVisible: false
}),
{
accessor: 'in_stock',
@ -108,7 +109,8 @@ export function SupplierPartTable({
},
{
accessor: 'packaging',
sortable: true
sortable: true,
defaultVisible: false
},
{
accessor: 'pack_quantity',
@ -141,7 +143,7 @@ export function SupplierPartTable({
{
accessor: 'available',
sortable: true,
defaultVisible: false,
render: (record: any) => {
const extra = [];

View File

@ -129,10 +129,18 @@ export function ReturnOrderTable({
DescriptionColumn({}),
LineItemsProgressColumn(),
StatusColumn({ model: ModelType.returnorder }),
ProjectCodeColumn({}),
CreationDateColumn({}),
CreatedByColumn({}),
StartDateColumn({}),
ProjectCodeColumn({
defaultVisible: false
}),
CreationDateColumn({
defaultVisible: false
}),
CreatedByColumn({
defaultVisible: false
}),
StartDateColumn({
defaultVisible: false
}),
TargetDateColumn({}),
CompletionDateColumn({
accessor: 'complete_date'

View File

@ -166,10 +166,18 @@ export function SalesOrderTable({
)
},
StatusColumn({ model: ModelType.salesorder }),
ProjectCodeColumn({}),
CreationDateColumn({}),
CreatedByColumn({}),
StartDateColumn({}),
ProjectCodeColumn({
defaultVisible: false
}),
CreationDateColumn({
defaultVisible: false
}),
CreatedByColumn({
defaultVisible: false
}),
StartDateColumn({
defaultVisible: false
}),
TargetDateColumn({}),
ShipmentDateColumn({}),
ResponsibleColumn({}),

View File

@ -64,7 +64,8 @@ function stockItemTableColumns({
{
accessor: 'part_detail.revision',
title: t`Revision`,
sortable: true
sortable: true,
defaultVisible: false
},
DescriptionColumn({
accessor: 'part_detail.description'
@ -228,6 +229,7 @@ function stockItemTableColumns({
{
accessor: 'purchase_order',
title: t`Purchase Order`,
defaultVisible: false,
render: (record: any) => {
return record.purchase_order_reference;
}
@ -235,12 +237,14 @@ function stockItemTableColumns({
{
accessor: 'SKU',
title: t`Supplier Part`,
sortable: true
sortable: true,
defaultVisible: false
},
{
accessor: 'MPN',
title: t`Manufacturer Part`,
sortable: true
sortable: true,
defaultVisible: false
},
{
accessor: 'purchase_price',
@ -248,6 +252,7 @@ function stockItemTableColumns({
sortable: true,
switchable: true,
hidden: !showPricing,
defaultVisible: false,
render: (record: any) =>
formatCurrency(record.purchase_price, {
currency: record.purchase_price_currency
@ -273,13 +278,15 @@ function stockItemTableColumns({
},
{
accessor: 'packaging',
sortable: true
sortable: true,
defaultVisible: false
},
DateColumn({
title: t`Expiry Date`,
accessor: 'expiry_date',
hidden: !useGlobalSettingsState.getState().isSet('STOCK_ENABLE_EXPIRY')
hidden: !useGlobalSettingsState.getState().isSet('STOCK_ENABLE_EXPIRY'),
defaultVisible: false
}),
DateColumn({
title: t`Last Updated`,

View File

@ -85,10 +85,12 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
sortable: true
},
BooleanColumn({
accessor: 'structural'
accessor: 'structural',
defaultVisible: false
}),
BooleanColumn({
accessor: 'external'
accessor: 'external',
defaultVisible: false
}),
{
accessor: 'location_type',

View File

@ -364,7 +364,6 @@ test('Purchase Orders - Receive Items', async ({ browser }) => {
await clearTableFilters(page);
await page.getByRole('cell', { name: 'my-batch-code' }).first().waitFor();
await page.getByRole('cell', { name: 'bucket' }).first().waitFor();
});
test('Purchase Orders - Duplicate', async ({ browser }) => {