2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-25 16:17:58 +00:00

[UI] Default table filters (#11405)

* Support "default filters" for table views

- User overrides apply in preference
- Only when there is no stored value (null)

* Correctly handle partially constructed filters

- Reverse lookup on available filter set

* Add default filters for order tables

* Default filters for company tables

* More default filters

* Add some more default filters

* Bump CHANGELOG

* build fix

* Tweaks for playwright testing

* Tweak playwright test

* Improve test flexibility
This commit is contained in:
Oliver
2026-02-23 19:46:17 +11:00
committed by GitHub
parent d48643319c
commit cef5d4d9c0
33 changed files with 331 additions and 53 deletions

View File

@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
[#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default.
[#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs. [#11222](https://github.com/inventree/InvenTree/pull/11222) adds support for data import using natural keys, allowing for easier association of related objects without needing to know their internal database IDs.
[#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API. [#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.

View File

@@ -39,7 +39,7 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api';
*/ */
export type TableFilter = { export type TableFilter = {
name: string; name: string;
label: string; label?: string;
description?: string; description?: string;
type?: TableFilterType; type?: TableFilterType;
choices?: TableFilterChoice[]; choices?: TableFilterChoice[];

View File

@@ -1,21 +1,43 @@
import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import type { FilterSetState, TableFilter } from '@lib/types/Filters';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
export function useFilterSet(filterKey: string): FilterSetState { export function useFilterSet(
filterKey: string,
initialFilters?: TableFilter[]
): FilterSetState {
// Array of active filters (saved to local storage) // Array of active filters (saved to local storage)
const [activeFilters, setActiveFilters] = useLocalStorage<TableFilter[]>({ const [storedFilters, setStoredFilters] = useLocalStorage<
TableFilter[] | null
>({
key: `inventree-filterset-${filterKey}`, key: `inventree-filterset-${filterKey}`,
defaultValue: [], defaultValue: null,
sync: false, sync: false,
getInitialValueInEffect: false getInitialValueInEffect: false
}); });
const activeFilters: TableFilter[] = useMemo(() => {
if (storedFilters == null) {
// If there are no stored filters, set initial values
const filters = initialFilters || [];
setStoredFilters(filters);
return filters;
}
return storedFilters || [];
}, [storedFilters]);
// Callback to clear all active filters from the table // Callback to clear all active filters from the table
const clearActiveFilters = useCallback(() => { const clearActiveFilters = useCallback(() => {
setActiveFilters([]); setStoredFilters([]);
}, []); }, []);
const setActiveFilters = useCallback(
(filters: TableFilter[]) => {
setStoredFilters(filters);
},
[setStoredFilters]
);
return { return {
filterKey, filterKey,
activeFilters, activeFilters,

View File

@@ -2,17 +2,28 @@ import { randomId } from '@mantine/hooks';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import type { FilterSetState } from '@lib/types/Filters'; import type { FilterSetState, TableFilter } from '@lib/types/Filters';
import type { TableState } from '@lib/types/Tables'; import type { TableState } from '@lib/types/Tables';
import { useFilterSet } from './UseFilterSet'; import { useFilterSet } from './UseFilterSet';
export type TableStateExtraProps = {
idAccessor?: string;
initialFilters?: TableFilter[];
};
/** /**
* A custom hook for managing the state of an <InvenTreeTable> component. * A custom hook for managing the state of an <InvenTreeTable> component.
* *
* Refer to the TableState type definition for more information. * Refer to the TableState type definition for more information.
*/ */
export function useTable(tableName: string, idAccessor = 'pk'): TableState { export function useTable(
tableName: string,
tableProps: TableStateExtraProps = {
idAccessor: 'pk',
initialFilters: []
}
): TableState {
// Function to generate a new ID (to refresh the table) // Function to generate a new ID (to refresh the table)
function generateTableName() { function generateTableName() {
return `${tableName.replaceAll('-', '')}-${randomId()}`; return `${tableName.replaceAll('-', '')}-${randomId()}`;
@@ -38,7 +49,10 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
[generateTableName] [generateTableName]
); );
const filterSet: FilterSetState = useFilterSet(`table-${tableName}`); const filterSet: FilterSetState = useFilterSet(
`table-${tableName}`,
tableProps.initialFilters
);
// Array of expanded records // Array of expanded records
const [expandedRecords, setExpandedRecords] = useState<any[]>([]); const [expandedRecords, setExpandedRecords] = useState<any[]>([]);
@@ -59,7 +73,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
// Array of selected primary key values // Array of selected primary key values
const selectedIds = useMemo( const selectedIds = useMemo(
() => selectedRecords.map((r) => r[idAccessor || 'pk']), () => selectedRecords.map((r) => r[tableProps.idAccessor || 'pk']),
[selectedRecords] [selectedRecords]
); );
@@ -89,7 +103,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
// Find the matching record in the table // Find the matching record in the table
const index = _records.findIndex( const index = _records.findIndex(
(r) => r[idAccessor || 'pk'] === record.pk (r) => r[tableProps.idAccessor || 'pk'] === record.pk
); );
if (index >= 0) { if (index >= 0) {
@@ -106,6 +120,11 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState {
[records] [records]
); );
const idAccessor = useMemo(
() => tableProps.idAccessor || 'pk',
[tableProps.idAccessor]
);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
return { return {

View File

@@ -20,7 +20,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
export function CurrencyTable({ export function CurrencyTable({
setInfo setInfo
}: Readonly<{ setInfo: (info: any) => void }>) { }: Readonly<{ setInfo: (info: any) => void }>) {
const table = useTable('currency', 'currency'); const table = useTable('currency', { idAccessor: 'currency' });
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {

View File

@@ -11,7 +11,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable';
import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable'; import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable';
function AllUnitTable() { function AllUnitTable() {
const table = useTable('all-units', 'name'); const table = useTable('all-units', { idAccessor: 'name' });
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {

View File

@@ -469,6 +469,7 @@ export default function BuildDetail() {
tableName='build-consumed' tableName='build-consumed'
showLocation={false} showLocation={false}
allowReturn allowReturn
defaultInStock={null}
params={{ params={{
consumed_by: id consumed_by: id
}} }}

View File

@@ -248,6 +248,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
tableName='assigned-stock' tableName='assigned-stock'
showLocation={false} showLocation={false}
allowReturn allowReturn
defaultInStock={null}
params={{ customer: company.pk }} params={{ customer: company.pk }}
/> />
) : ( ) : (

View File

@@ -108,6 +108,7 @@ export default function PurchasingIndex() {
icon: <IconTable />, icon: <IconTable />,
content: ( content: (
<CompanyTable <CompanyTable
companyType='supplier'
path='purchasing/supplier' path='purchasing/supplier'
params={{ is_supplier: true }} params={{ is_supplier: true }}
/> />
@@ -157,6 +158,7 @@ export default function PurchasingIndex() {
icon: <IconTable />, icon: <IconTable />,
content: ( content: (
<CompanyTable <CompanyTable
companyType='manufacturer'
path='purchasing/manufacturer' path='purchasing/manufacturer'
params={{ is_manufacturer: true }} params={{ is_manufacturer: true }}
/> />

View File

@@ -141,6 +141,7 @@ export default function SalesIndex() {
icon: <IconTable />, icon: <IconTable />,
content: ( content: (
<CompanyTable <CompanyTable
companyType='customer'
path='sales/customer' path='sales/customer'
params={{ is_customer: true }} params={{ is_customer: true }}
/> />

View File

@@ -3,6 +3,7 @@ import { t } from '@lingui/core/macro';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import { isTrue } from '@lib/functions/Conversion';
import type { TableFilter, TableFilterChoice } from '@lib/types/Filters'; import type { TableFilter, TableFilterChoice } from '@lib/types/Filters';
import type { import type {
StatusCodeInterface, StatusCodeInterface,
@@ -14,6 +15,47 @@ import {
} from '../states/GlobalStatusState'; } from '../states/GlobalStatusState';
import { useGlobalSettingsState } from '../states/SettingsStates'; import { useGlobalSettingsState } from '../states/SettingsStates';
// Determine the appropriate display label for a given filter, based on its name and the list of available filters
export function filterDisplayLabel(
name: string,
filters?: TableFilter[]
): string {
const filter = filters?.find((f) => f.name === name);
return filter?.label ?? name;
}
// Determine the appropriate display value for a filter, based on its type and value
// This is useful for recreating a display value if we only have a name:value pair
export function filterDisplayValue(
name: string,
value: any,
filters?: TableFilter[]
) {
const filterDef = filters?.find((f) => f.name === name);
if (!filterDef) {
return value;
}
if (!filterDef.type || filterDef.type == 'boolean') {
return isTrue(value) ? t`Yes` : t`No`;
}
if (filterDef.type === 'choice' && filterDef.choices) {
const choice = filterDef.choices.find((c) => c.value === value);
return choice ? choice.label : value;
}
if (filterDef.type === 'choice' && filterDef.choiceFunction) {
const choices = filterDef.choiceFunction();
const choice = choices.find((c) => c.value === value);
return choice ? choice.label : value;
}
// No obvious match - return the raw value
return value;
}
/** /**
* Return list of available filter options for a given filter * Return list of available filter options for a given filter
* @param filter - TableFilter object * @param filter - TableFilter object

View File

@@ -28,17 +28,46 @@ import type {
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import { StandaloneField } from '../components/forms/StandaloneField'; import { StandaloneField } from '../components/forms/StandaloneField';
import { StylishText } from '../components/items/StylishText'; import { StylishText } from '../components/items/StylishText';
import { getTableFilterOptions } from './Filter'; import {
filterDisplayLabel,
filterDisplayValue,
getTableFilterOptions
} from './Filter';
/*
* Render a preview of a single filter
*/
export function FilterPreview({
filter,
filters
}: Readonly<{
filter: TableFilter;
filters?: TableFilter[];
}>) {
return (
<Group key={filter.name} justify='space-between' gap='xl' wrap='nowrap'>
<Text size='sm'>
{filter.label ?? filterDisplayLabel(filter.name, filters)}
</Text>
<Text size='xs'>
{filter.displayValue ??
filterDisplayValue(filter.name, filter.value, filters)}
</Text>
</Group>
);
}
/* /*
* Render a single table filter item * Render a single table filter item
*/ */
function FilterItem({ function FilterItem({
flt, flt,
filterSet filterSet,
availableFilters
}: Readonly<{ }: Readonly<{
flt: TableFilter; flt: TableFilter;
filterSet: FilterSetState; filterSet: FilterSetState;
availableFilters?: TableFilter[];
}>) { }>) {
const removeFilter = useCallback(() => { const removeFilter = useCallback(() => {
const newFilters = filterSet.activeFilters.filter( const newFilters = filterSet.activeFilters.filter(
@@ -47,17 +76,29 @@ function FilterItem({
filterSet.setActiveFilters(newFilters); filterSet.setActiveFilters(newFilters);
}, [flt]); }, [flt]);
// Find the matching filter definition
const filterProps: TableFilter | undefined = useMemo(() => {
return availableFilters?.find((f) => f.name === flt.name);
}, [availableFilters, flt]);
return ( return (
<Paper p='sm' shadow='sm' radius='xs'> <Paper p='sm' shadow='sm' radius='xs'>
<Group justify='space-between' key={flt.name} wrap='nowrap'> <Group justify='space-between' key={flt.name} wrap='nowrap'>
<Stack gap='xs'> <Stack gap='xs'>
<Text size='sm'>{flt.label}</Text> <Text size='sm'>{flt.label ?? filterProps?.label ?? flt.name}</Text>
<Text size='xs'>{flt.description}</Text> <Text size='xs'>{flt.description ?? filterProps?.description}</Text>
</Stack> </Stack>
<Group justify='right'> <Group justify='right'>
<Badge>{flt.displayValue ?? flt.value}</Badge> <Badge>
<Tooltip label={t`Remove filter`} withinPortal={true}> {flt.displayValue ??
<CloseButton size='md' color='red' onClick={removeFilter} /> filterDisplayValue(flt.name, flt.value, availableFilters)}
</Badge>
<Tooltip
label={t`Remove filter`}
withinPortal={true}
position='top-end'
>
<CloseButton size='md' c='red' onClick={removeFilter} />
</Tooltip> </Tooltip>
</Group> </Group>
</Group> </Group>
@@ -174,10 +215,10 @@ function FilterAddGroup({
return ( return (
availableFilters availableFilters
?.filter((flt) => !activeFilterNames.includes(flt.name)) ?.filter((flt) => !activeFilterNames.includes(flt.name))
?.sort((a, b) => a.label.localeCompare(b.label)) ?.sort((a, b) => (a.label ?? a.name).localeCompare(b.label ?? b.name))
?.map((flt) => ({ ?.map((flt) => ({
value: flt.name, value: flt.name,
label: flt.label, label: flt.label ?? flt.name,
description: flt.description description: flt.description
})) ?? [] })) ?? []
); );
@@ -317,7 +358,12 @@ export function FilterSelectDrawer({
<Stack gap='xs'> <Stack gap='xs'>
{hasFilters && {hasFilters &&
filterSet.activeFilters?.map((f) => ( filterSet.activeFilters?.map((f) => (
<FilterItem key={f.name} flt={f} filterSet={filterSet} /> <FilterItem
key={f.name}
flt={f}
filterSet={filterSet}
availableFilters={availableFilters}
/>
))} ))}
{addFilter && ( {addFilter && (
<Stack gap='xs'> <Stack gap='xs'>

View File

@@ -9,7 +9,6 @@ import {
Paper, Paper,
Space, Space,
Stack, Stack,
Text,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { import {
@@ -37,7 +36,7 @@ import { StylishText } from '../components/items/StylishText';
import useDataExport from '../hooks/UseDataExport'; import useDataExport from '../hooks/UseDataExport';
import { useDeleteApiFormModal } from '../hooks/UseForm'; import { useDeleteApiFormModal } from '../hooks/UseForm';
import { TableColumnSelect } from './ColumnSelect'; import { TableColumnSelect } from './ColumnSelect';
import { FilterSelectDrawer } from './FilterSelectDrawer'; import { FilterPreview, FilterSelectDrawer } from './FilterSelectDrawer';
/** /**
* Render a composite header for an InvenTree table * Render a composite header for an InvenTree table
@@ -272,15 +271,10 @@ export default function InvenTreeTableHeader({
<StylishText size='md'>{t`Active Filters`}</StylishText> <StylishText size='md'>{t`Active Filters`}</StylishText>
<Divider /> <Divider />
{tableState.filterSet.activeFilters?.map((filter) => ( {tableState.filterSet.activeFilters?.map((filter) => (
<Group <FilterPreview
key={filter.name} filter={filter}
justify='space-between' filters={tableProps.tableFilters}
gap='xl' />
wrap='nowrap'
>
<Text size='sm'>{filter.label}</Text>
<Text size='xs'>{filter.displayValue}</Text>
</Group>
))} ))}
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -64,7 +64,14 @@ export function BuildOrderTable({
salesOrderId?: number; salesOrderId?: number;
}>) { }>) {
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();
const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index'); const table = useTable(!!partId ? 'buildorder-part' : 'buildorder-index', {
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
});
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
return [ return [

View File

@@ -29,13 +29,22 @@ import { InvenTreeTable } from '../InvenTreeTable';
* based on the provided filter parameters * based on the provided filter parameters
*/ */
export function CompanyTable({ export function CompanyTable({
companyType,
params, params,
path path
}: Readonly<{ }: Readonly<{
companyType?: string;
params?: any; params?: any;
path?: string; path?: string;
}>) { }>) {
const table = useTable('company'); const table = useTable(`company-${companyType ?? 'index'}`, {
initialFilters: [
{
name: 'active',
value: 'true'
}
]
});
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState(); const user = useUserState();

View File

@@ -26,7 +26,7 @@ export default function BarcodeScanTable({
const navigate = useNavigate(); const navigate = useNavigate();
const user = useUserState(); const user = useUserState();
const table = useTable('barcode-scan-results', 'id'); const table = useTable('barcode-scan-results', { idAccessor: 'id' });
const tableColumns: TableColumn[] = useMemo(() => { const tableColumns: TableColumn[] = useMemo(() => {
return [ return [

View File

@@ -338,17 +338,26 @@ export function PartListTable({
enableImport = true, enableImport = true,
basePartInstance, basePartInstance,
props, props,
tableName = 'part-list',
defaultPartData defaultPartData
}: Readonly<{ }: Readonly<{
enableImport?: boolean; enableImport?: boolean;
props?: InvenTreeTableProps; props?: InvenTreeTableProps;
basePartInstance?: any; basePartInstance?: any;
tableName?: string;
defaultPartData?: any; defaultPartData?: any;
}>) { }>) {
const tableColumns = useMemo(() => partTableColumns(), []); const tableColumns = useMemo(() => partTableColumns(), []);
const tableFilters = useMemo(() => partTableFilters(), []); const tableFilters = useMemo(() => partTableFilters(), []);
const table = useTable('part-list'); const table = useTable(tableName ?? 'part-list', {
initialFilters: [
{
name: 'active',
value: 'true'
}
]
});
const user = useUserState(); const user = useUserState();
const globalSettings = useGlobalSettingsState(); const globalSettings = useGlobalSettingsState();

View File

@@ -43,6 +43,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
ancestor: part.pk ancestor: part.pk
} }
}} }}
tableName='part-variants'
basePartInstance={part} basePartInstance={part}
defaultPartData={{ defaultPartData={{
...part, ...part,

View File

@@ -19,7 +19,9 @@ export interface PluginRegistryErrorI {
* Table displaying list of plugin registry errors * Table displaying list of plugin registry errors
*/ */
export default function PluginErrorTable() { export default function PluginErrorTable() {
const table = useTable('registryErrors', 'id'); const table = useTable('registryErrors', {
idAccessor: 'id'
});
const registryErrorTableColumns: TableColumn<PluginRegistryErrorI>[] = const registryErrorTableColumns: TableColumn<PluginRegistryErrorI>[] =
useMemo( useMemo(

View File

@@ -54,7 +54,29 @@ export function ManufacturerPartTable({
return tId; return tId;
}, [manufacturerId, partId]); }, [manufacturerId, partId]);
const table = useTable(tableId); const initialFilters = useMemo(() => {
const filters: TableFilter[] = [];
if (!manufacturerId) {
filters.push({
name: 'manufacturer_active',
value: 'true'
});
}
if (!partId) {
filters.push({
name: 'part_active',
value: 'true'
});
}
return filters;
}, [manufacturerId, partId]);
const table = useTable(tableId, {
initialFilters: initialFilters
});
const user = useUserState(); const user = useUserState();

View File

@@ -60,7 +60,14 @@ export function PurchaseOrderTable({
supplierPartId?: number; supplierPartId?: number;
externalBuildId?: number; externalBuildId?: number;
}>) { }>) {
const table = useTable('purchase-order'); const table = useTable('purchase-order', {
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
});
const user = useUserState(); const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {

View File

@@ -54,7 +54,34 @@ export function SupplierPartTable({
partId?: number; partId?: number;
supplierId?: number; supplierId?: number;
}>): ReactNode { }>): ReactNode {
const table = useTable('supplierparts'); const initialFilters = useMemo(() => {
const filters: TableFilter[] = [
{
name: 'active',
value: 'true'
}
];
if (!supplierId) {
filters.push({
name: 'supplier_active',
value: 'true'
});
}
if (!partId) {
filters.push({
name: 'part_active',
value: 'true'
});
}
return filters;
}, [supplierId, partId]);
const table = useTable('supplierparts', {
initialFilters: initialFilters
});
const user = useUserState(); const user = useUserState();

View File

@@ -56,7 +56,18 @@ export function ReturnOrderTable({
partId?: number; partId?: number;
customerId?: number; customerId?: number;
}>) { }>) {
const table = useTable(!!partId ? 'returnorders-part' : 'returnorders-index'); const table = useTable(
!!partId ? 'returnorders-part' : 'returnorders-index',
{
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
}
);
const user = useUserState(); const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {

View File

@@ -58,7 +58,14 @@ export function SalesOrderTable({
partId?: number; partId?: number;
customerId?: number; customerId?: number;
}>) { }>) {
const table = useTable(!!partId ? 'salesorder-part' : 'salesorder-index'); const table = useTable(!!partId ? 'salesorder-part' : 'salesorder-index', {
initialFilters: [
{
name: 'outstanding',
value: 'true'
}
]
});
const user = useUserState(); const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {

View File

@@ -48,7 +48,7 @@ export function ApiTokenTable({
return []; return [];
}, [only_myself]); }, [only_myself]);
const table = useTable('api-tokens', 'id'); const table = useTable('api-tokens', { idAccessor: 'id' });
const tableColumns = useMemo(() => { const tableColumns = useMemo(() => {
const cols = [ const cols = [

View File

@@ -61,7 +61,7 @@ export function EmailTable() {
]; ];
}, []); }, []);
const table = useTable('emails', 'pk'); const table = useTable('emails', { idAccessor: 'pk' });
const [selectedEmailId, setSelectedEmailId] = useState<string>(''); const [selectedEmailId, setSelectedEmailId] = useState<string>('');

View File

@@ -315,6 +315,8 @@ export function StockItemTable({
showLocation = true, showLocation = true,
showPricing = true, showPricing = true,
allowReturn = false, allowReturn = false,
initialFilters,
defaultInStock = true,
tableName = 'stockitems' tableName = 'stockitems'
}: Readonly<{ }: Readonly<{
params?: any; params?: any;
@@ -322,9 +324,34 @@ export function StockItemTable({
showLocation?: boolean; showLocation?: boolean;
showPricing?: boolean; showPricing?: boolean;
allowReturn?: boolean; allowReturn?: boolean;
defaultInStock?: boolean | null;
initialFilters?: TableFilter[];
tableName: string; tableName: string;
}>) { }>) {
const table = useTable(tableName); const initialStockFilters: TableFilter[] = useMemo(() => {
if (!!initialFilters) {
return initialFilters;
}
const filters: TableFilter[] = [];
// Optionally set the default "in_stock" filter
// Typically, we default to only displaying "in_stock" items,
// but this can be overridden by the caller if required
if (defaultInStock != undefined && defaultInStock != null) {
filters.push({
name: 'in_stock',
value: defaultInStock ? 'true' : 'false'
});
}
return filters;
}, [defaultInStock, initialFilters]);
const table = useTable(tableName, {
initialFilters: initialStockFilters
});
const user = useUserState(); const user = useUserState();
const settings = useGlobalSettingsState(); const settings = useGlobalSettingsState();

View File

@@ -723,6 +723,8 @@ test('Build Order - External', async ({ browser }) => {
await navigate(page, 'manufacturing/build-order/26/details'); await navigate(page, 'manufacturing/build-order/26/details');
await loadTab(page, 'External Orders'); await loadTab(page, 'External Orders');
await clearTableFilters(page);
await page.getByRole('cell', { name: 'PO0017' }).waitFor(); await page.getByRole('cell', { name: 'PO0017' }).waitFor();
await page.getByRole('cell', { name: 'PO0018' }).waitFor(); await page.getByRole('cell', { name: 'PO0018' }).waitFor();
}); });

View File

@@ -15,21 +15,28 @@ test('Company', async ({ browser }) => {
await navigate(page, 'company/1/details'); await navigate(page, 'company/1/details');
await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor(); await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor();
await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor(); await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor();
await loadTab(page, 'Supplied Parts'); await loadTab(page, 'Supplied Parts');
await page await page
.getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true }) .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true })
.waitFor(); .waitFor();
await loadTab(page, 'Purchase Orders'); await loadTab(page, 'Purchase Orders');
await clearTableFilters(page);
await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor(); await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor();
await loadTab(page, 'Stock Items'); await loadTab(page, 'Stock Items');
await page await page
.getByRole('cell', { name: 'Blue plastic enclosure' }) .getByRole('cell', { name: 'Blue plastic enclosure' })
.first() .first()
.waitFor(); .waitFor();
await loadTab(page, 'Contacts'); await loadTab(page, 'Contacts');
await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor(); await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor();
await loadTab(page, 'Addresses'); await loadTab(page, 'Addresses');
await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor(); await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor();
await loadTab(page, 'Attachments'); await loadTab(page, 'Attachments');
await loadTab(page, 'Notes'); await loadTab(page, 'Notes');

View File

@@ -66,14 +66,16 @@ test('Parts - Tabs', async ({ browser }) => {
}); });
test('Parts - Manufacturer Parts', async ({ browser }) => { test('Parts - Manufacturer Parts', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/84/suppliers' }); const page = await doCachedLogin(browser, { url: 'part/84/' });
// Load the "suppliers" tab
await loadTab(page, 'Suppliers'); await loadTab(page, 'Suppliers');
await page.getByText('Hammond Manufacturing').click(); await page.getByText('Hammond Manufacturing').click();
await loadTab(page, 'Parameters');
await loadTab(page, 'Suppliers'); // Wait for manufacturer part page to load
await loadTab(page, 'Attachments');
await page.getByText('1551ACLR - 1551ACLR').waitFor(); await page.getByText('1551ACLR - 1551ACLR').waitFor();
await loadTab(page, 'Parameters');
await loadTab(page, 'Attachments');
}); });
test('Parts - Supplier Parts', async ({ browser }) => { test('Parts - Supplier Parts', async ({ browser }) => {

View File

@@ -25,6 +25,14 @@ test('Purchasing - Index', async ({ browser }) => {
await showCalendarView(page); await showCalendarView(page);
await showTableView(page); await showTableView(page);
// Check default filters are applied
// By default, only outstanding orders are visible
await page.getByText(/1 - \d+ \/ \d+/).waitFor();
// Clearing the filters, more orders should be visible
await clearTableFilters(page);
await page.getByText(/1 - 1\d \/ 1\d/).waitFor();
// Suppliers tab // Suppliers tab
await loadTab(page, 'Suppliers'); await loadTab(page, 'Suppliers');
await showParametricView(page); await showParametricView(page);

View File

@@ -33,7 +33,7 @@ test('Exporting - Orders', async ({ browser }) => {
await page.getByText('Process completed successfully').waitFor(); await page.getByText('Process completed successfully').waitFor();
// Download list of purchase order items // Download list of purchase order items
await page.getByRole('cell', { name: 'PO0011' }).click(); await page.getByRole('cell', { name: 'PO0014' }).click();
await loadTab(page, 'Line Items'); await loadTab(page, 'Line Items');
await openExportDialog(page); await openExportDialog(page);
await page.getByRole('button', { name: 'Export', exact: true }).click(); await page.getByRole('button', { name: 'Export', exact: true }).click();

View File

@@ -62,7 +62,7 @@ test('Printing - Report Printing', async ({ browser }) => {
await loadTab(page, 'Purchase Orders'); await loadTab(page, 'Purchase Orders');
await activateTableView(page); await activateTableView(page);
await page.getByRole('cell', { name: 'PO0009' }).click(); await page.getByRole('cell', { name: 'PO0013' }).click();
// Select "print report" // Select "print report"
await page.getByLabel('action-menu-printing-actions').click(); await page.getByLabel('action-menu-printing-actions').click();