From cef5d4d9c0be230e580cb7a0dd426a08fa474d5b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 23 Feb 2026 19:46:17 +1100 Subject: [PATCH] [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 --- CHANGELOG.md | 2 + src/frontend/lib/types/Filters.tsx | 2 +- src/frontend/src/hooks/UseFilterSet.tsx | 32 +++++++-- src/frontend/src/hooks/UseTable.tsx | 29 ++++++-- .../AdminCenter/CurrencyManagementPanel.tsx | 2 +- .../AdminCenter/UnitManagementPanel.tsx | 2 +- src/frontend/src/pages/build/BuildDetail.tsx | 1 + .../src/pages/company/CompanyDetail.tsx | 1 + .../src/pages/purchasing/PurchasingIndex.tsx | 2 + src/frontend/src/pages/sales/SalesIndex.tsx | 1 + src/frontend/src/tables/Filter.tsx | 42 ++++++++++++ .../src/tables/FilterSelectDrawer.tsx | 66 ++++++++++++++++--- .../src/tables/InvenTreeTableHeader.tsx | 16 ++--- .../src/tables/build/BuildOrderTable.tsx | 9 ++- .../src/tables/company/CompanyTable.tsx | 11 +++- .../src/tables/general/BarcodeScanTable.tsx | 2 +- src/frontend/src/tables/part/PartTable.tsx | 11 +++- .../src/tables/part/PartVariantTable.tsx | 1 + .../src/tables/plugin/PluginErrorTable.tsx | 4 +- .../purchasing/ManufacturerPartTable.tsx | 24 ++++++- .../tables/purchasing/PurchaseOrderTable.tsx | 9 ++- .../tables/purchasing/SupplierPartTable.tsx | 29 +++++++- .../src/tables/sales/ReturnOrderTable.tsx | 13 +++- .../src/tables/sales/SalesOrderTable.tsx | 9 ++- .../src/tables/settings/ApiTokenTable.tsx | 2 +- .../src/tables/settings/EmailTable.tsx | 2 +- .../src/tables/stock/StockItemTable.tsx | 29 +++++++- src/frontend/tests/pages/pui_build.spec.ts | 2 + src/frontend/tests/pages/pui_company.spec.ts | 7 ++ src/frontend/tests/pages/pui_part.spec.ts | 10 +-- .../tests/pages/pui_purchase_order.spec.ts | 8 +++ src/frontend/tests/pui_exporting.spec.ts | 2 +- src/frontend/tests/pui_printing.spec.ts | 2 +- 33 files changed, 331 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c135487674..b821f59bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. [#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. diff --git a/src/frontend/lib/types/Filters.tsx b/src/frontend/lib/types/Filters.tsx index 1ef44a3a84..53d5a563ca 100644 --- a/src/frontend/lib/types/Filters.tsx +++ b/src/frontend/lib/types/Filters.tsx @@ -39,7 +39,7 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text' | 'api'; */ export type TableFilter = { name: string; - label: string; + label?: string; description?: string; type?: TableFilterType; choices?: TableFilterChoice[]; diff --git a/src/frontend/src/hooks/UseFilterSet.tsx b/src/frontend/src/hooks/UseFilterSet.tsx index 7638b91532..9d0300cb46 100644 --- a/src/frontend/src/hooks/UseFilterSet.tsx +++ b/src/frontend/src/hooks/UseFilterSet.tsx @@ -1,21 +1,43 @@ import type { FilterSetState, TableFilter } from '@lib/types/Filters'; 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) - const [activeFilters, setActiveFilters] = useLocalStorage({ + const [storedFilters, setStoredFilters] = useLocalStorage< + TableFilter[] | null + >({ key: `inventree-filterset-${filterKey}`, - defaultValue: [], + defaultValue: null, sync: 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 const clearActiveFilters = useCallback(() => { - setActiveFilters([]); + setStoredFilters([]); }, []); + const setActiveFilters = useCallback( + (filters: TableFilter[]) => { + setStoredFilters(filters); + }, + [setStoredFilters] + ); + return { filterKey, activeFilters, diff --git a/src/frontend/src/hooks/UseTable.tsx b/src/frontend/src/hooks/UseTable.tsx index 79d10e87f4..49a2c50569 100644 --- a/src/frontend/src/hooks/UseTable.tsx +++ b/src/frontend/src/hooks/UseTable.tsx @@ -2,17 +2,28 @@ import { randomId } from '@mantine/hooks'; import { useCallback, useMemo, useState } from 'react'; 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 { useFilterSet } from './UseFilterSet'; +export type TableStateExtraProps = { + idAccessor?: string; + initialFilters?: TableFilter[]; +}; + /** * A custom hook for managing the state of an component. * * 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 generateTableName() { return `${tableName.replaceAll('-', '')}-${randomId()}`; @@ -38,7 +49,10 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { [generateTableName] ); - const filterSet: FilterSetState = useFilterSet(`table-${tableName}`); + const filterSet: FilterSetState = useFilterSet( + `table-${tableName}`, + tableProps.initialFilters + ); // Array of expanded records const [expandedRecords, setExpandedRecords] = useState([]); @@ -59,7 +73,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { // Array of selected primary key values const selectedIds = useMemo( - () => selectedRecords.map((r) => r[idAccessor || 'pk']), + () => selectedRecords.map((r) => r[tableProps.idAccessor || 'pk']), [selectedRecords] ); @@ -89,7 +103,7 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { // Find the matching record in the table const index = _records.findIndex( - (r) => r[idAccessor || 'pk'] === record.pk + (r) => r[tableProps.idAccessor || 'pk'] === record.pk ); if (index >= 0) { @@ -106,6 +120,11 @@ export function useTable(tableName: string, idAccessor = 'pk'): TableState { [records] ); + const idAccessor = useMemo( + () => tableProps.idAccessor || 'pk', + [tableProps.idAccessor] + ); + const [isLoading, setIsLoading] = useState(false); return { diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx index a0972b445c..7e1ca77049 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/CurrencyManagementPanel.tsx @@ -20,7 +20,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable'; export function CurrencyTable({ setInfo }: Readonly<{ setInfo: (info: any) => void }>) { - const table = useTable('currency', 'currency'); + const table = useTable('currency', { idAccessor: 'currency' }); const columns = useMemo(() => { return [ { diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx index 3df935a820..658d84840f 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/UnitManagementPanel.tsx @@ -11,7 +11,7 @@ import { InvenTreeTable } from '../../../../tables/InvenTreeTable'; import CustomUnitsTable from '../../../../tables/settings/CustomUnitsTable'; function AllUnitTable() { - const table = useTable('all-units', 'name'); + const table = useTable('all-units', { idAccessor: 'name' }); const columns = useMemo(() => { return [ { diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index f0542f3f9b..8f8bf0e60e 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -469,6 +469,7 @@ export default function BuildDetail() { tableName='build-consumed' showLocation={false} allowReturn + defaultInStock={null} params={{ consumed_by: id }} diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index ebc20200cf..bc51806281 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -248,6 +248,7 @@ export default function CompanyDetail(props: Readonly) { tableName='assigned-stock' showLocation={false} allowReturn + defaultInStock={null} params={{ customer: company.pk }} /> ) : ( diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index 706c7e737d..a4a2f70d47 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -108,6 +108,7 @@ export default function PurchasingIndex() { icon: , content: ( @@ -157,6 +158,7 @@ export default function PurchasingIndex() { icon: , content: ( diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 0fe4b8a31c..d225081ec4 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -141,6 +141,7 @@ export default function SalesIndex() { icon: , content: ( diff --git a/src/frontend/src/tables/Filter.tsx b/src/frontend/src/tables/Filter.tsx index 9970c72add..f093cb94f2 100644 --- a/src/frontend/src/tables/Filter.tsx +++ b/src/frontend/src/tables/Filter.tsx @@ -3,6 +3,7 @@ import { t } from '@lingui/core/macro'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; +import { isTrue } from '@lib/functions/Conversion'; import type { TableFilter, TableFilterChoice } from '@lib/types/Filters'; import type { StatusCodeInterface, @@ -14,6 +15,47 @@ import { } from '../states/GlobalStatusState'; 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 * @param filter - TableFilter object diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx index eecacf9431..a737974403 100644 --- a/src/frontend/src/tables/FilterSelectDrawer.tsx +++ b/src/frontend/src/tables/FilterSelectDrawer.tsx @@ -28,17 +28,46 @@ import type { import { IconCheck } from '@tabler/icons-react'; import { StandaloneField } from '../components/forms/StandaloneField'; 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 ( + + + {filter.label ?? filterDisplayLabel(filter.name, filters)} + + + {filter.displayValue ?? + filterDisplayValue(filter.name, filter.value, filters)} + + + ); +} /* * Render a single table filter item */ function FilterItem({ flt, - filterSet + filterSet, + availableFilters }: Readonly<{ flt: TableFilter; filterSet: FilterSetState; + availableFilters?: TableFilter[]; }>) { const removeFilter = useCallback(() => { const newFilters = filterSet.activeFilters.filter( @@ -47,17 +76,29 @@ function FilterItem({ filterSet.setActiveFilters(newFilters); }, [flt]); + // Find the matching filter definition + const filterProps: TableFilter | undefined = useMemo(() => { + return availableFilters?.find((f) => f.name === flt.name); + }, [availableFilters, flt]); + return ( - {flt.label} - {flt.description} + {flt.label ?? filterProps?.label ?? flt.name} + {flt.description ?? filterProps?.description} - {flt.displayValue ?? flt.value} - - + + {flt.displayValue ?? + filterDisplayValue(flt.name, flt.value, availableFilters)} + + + @@ -174,10 +215,10 @@ function FilterAddGroup({ return ( availableFilters ?.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) => ({ value: flt.name, - label: flt.label, + label: flt.label ?? flt.name, description: flt.description })) ?? [] ); @@ -317,7 +358,12 @@ export function FilterSelectDrawer({ {hasFilters && filterSet.activeFilters?.map((f) => ( - + ))} {addFilter && ( diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx index e052936137..d5d6b45c27 100644 --- a/src/frontend/src/tables/InvenTreeTableHeader.tsx +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -9,7 +9,6 @@ import { Paper, Space, Stack, - Text, Tooltip } from '@mantine/core'; import { @@ -37,7 +36,7 @@ import { StylishText } from '../components/items/StylishText'; import useDataExport from '../hooks/UseDataExport'; import { useDeleteApiFormModal } from '../hooks/UseForm'; import { TableColumnSelect } from './ColumnSelect'; -import { FilterSelectDrawer } from './FilterSelectDrawer'; +import { FilterPreview, FilterSelectDrawer } from './FilterSelectDrawer'; /** * Render a composite header for an InvenTree table @@ -272,15 +271,10 @@ export default function InvenTreeTableHeader({ {t`Active Filters`} {tableState.filterSet.activeFilters?.map((filter) => ( - - {filter.label} - {filter.displayValue} - + ))} diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 242a81ef4e..77f071df84 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -64,7 +64,14 @@ export function BuildOrderTable({ salesOrderId?: number; }>) { 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(() => { return [ diff --git a/src/frontend/src/tables/company/CompanyTable.tsx b/src/frontend/src/tables/company/CompanyTable.tsx index ce02a7756d..e3c6125344 100644 --- a/src/frontend/src/tables/company/CompanyTable.tsx +++ b/src/frontend/src/tables/company/CompanyTable.tsx @@ -29,13 +29,22 @@ import { InvenTreeTable } from '../InvenTreeTable'; * based on the provided filter parameters */ export function CompanyTable({ + companyType, params, path }: Readonly<{ + companyType?: string; params?: any; path?: string; }>) { - const table = useTable('company'); + const table = useTable(`company-${companyType ?? 'index'}`, { + initialFilters: [ + { + name: 'active', + value: 'true' + } + ] + }); const navigate = useNavigate(); const user = useUserState(); diff --git a/src/frontend/src/tables/general/BarcodeScanTable.tsx b/src/frontend/src/tables/general/BarcodeScanTable.tsx index d13b5ec5f4..60b2546e73 100644 --- a/src/frontend/src/tables/general/BarcodeScanTable.tsx +++ b/src/frontend/src/tables/general/BarcodeScanTable.tsx @@ -26,7 +26,7 @@ export default function BarcodeScanTable({ const navigate = useNavigate(); const user = useUserState(); - const table = useTable('barcode-scan-results', 'id'); + const table = useTable('barcode-scan-results', { idAccessor: 'id' }); const tableColumns: TableColumn[] = useMemo(() => { return [ diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index 7093149492..e208b4035c 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -338,17 +338,26 @@ export function PartListTable({ enableImport = true, basePartInstance, props, + tableName = 'part-list', defaultPartData }: Readonly<{ enableImport?: boolean; props?: InvenTreeTableProps; basePartInstance?: any; + tableName?: string; defaultPartData?: any; }>) { const tableColumns = useMemo(() => partTableColumns(), []); const tableFilters = useMemo(() => partTableFilters(), []); - const table = useTable('part-list'); + const table = useTable(tableName ?? 'part-list', { + initialFilters: [ + { + name: 'active', + value: 'true' + } + ] + }); const user = useUserState(); const globalSettings = useGlobalSettingsState(); diff --git a/src/frontend/src/tables/part/PartVariantTable.tsx b/src/frontend/src/tables/part/PartVariantTable.tsx index 845e654b3b..2822476b59 100644 --- a/src/frontend/src/tables/part/PartVariantTable.tsx +++ b/src/frontend/src/tables/part/PartVariantTable.tsx @@ -43,6 +43,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) { ancestor: part.pk } }} + tableName='part-variants' basePartInstance={part} defaultPartData={{ ...part, diff --git a/src/frontend/src/tables/plugin/PluginErrorTable.tsx b/src/frontend/src/tables/plugin/PluginErrorTable.tsx index a7172f7071..4f3d5212da 100644 --- a/src/frontend/src/tables/plugin/PluginErrorTable.tsx +++ b/src/frontend/src/tables/plugin/PluginErrorTable.tsx @@ -19,7 +19,9 @@ export interface PluginRegistryErrorI { * Table displaying list of plugin registry errors */ export default function PluginErrorTable() { - const table = useTable('registryErrors', 'id'); + const table = useTable('registryErrors', { + idAccessor: 'id' + }); const registryErrorTableColumns: TableColumn[] = useMemo( diff --git a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx index d18f99dc87..dd1aaaa99c 100644 --- a/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx +++ b/src/frontend/src/tables/purchasing/ManufacturerPartTable.tsx @@ -54,7 +54,29 @@ export function ManufacturerPartTable({ return tId; }, [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(); diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx index 81fe9db967..34c1f37ae6 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderTable.tsx @@ -60,7 +60,14 @@ export function PurchaseOrderTable({ supplierPartId?: number; externalBuildId?: number; }>) { - const table = useTable('purchase-order'); + const table = useTable('purchase-order', { + initialFilters: [ + { + name: 'outstanding', + value: 'true' + } + ] + }); const user = useUserState(); const tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx index ecdc42cf0b..9afc553d52 100644 --- a/src/frontend/src/tables/purchasing/SupplierPartTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPartTable.tsx @@ -54,7 +54,34 @@ export function SupplierPartTable({ partId?: number; supplierId?: number; }>): 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(); diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index f01dedfbf9..dfd0362e90 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -56,7 +56,18 @@ export function ReturnOrderTable({ partId?: 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 tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/sales/SalesOrderTable.tsx b/src/frontend/src/tables/sales/SalesOrderTable.tsx index c1e9b0124a..c87e079767 100644 --- a/src/frontend/src/tables/sales/SalesOrderTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderTable.tsx @@ -58,7 +58,14 @@ export function SalesOrderTable({ partId?: 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 tableFilters: TableFilter[] = useMemo(() => { diff --git a/src/frontend/src/tables/settings/ApiTokenTable.tsx b/src/frontend/src/tables/settings/ApiTokenTable.tsx index ccb953a4ce..9736dded12 100644 --- a/src/frontend/src/tables/settings/ApiTokenTable.tsx +++ b/src/frontend/src/tables/settings/ApiTokenTable.tsx @@ -48,7 +48,7 @@ export function ApiTokenTable({ return []; }, [only_myself]); - const table = useTable('api-tokens', 'id'); + const table = useTable('api-tokens', { idAccessor: 'id' }); const tableColumns = useMemo(() => { const cols = [ diff --git a/src/frontend/src/tables/settings/EmailTable.tsx b/src/frontend/src/tables/settings/EmailTable.tsx index 2ea50d835d..87526a2e27 100644 --- a/src/frontend/src/tables/settings/EmailTable.tsx +++ b/src/frontend/src/tables/settings/EmailTable.tsx @@ -61,7 +61,7 @@ export function EmailTable() { ]; }, []); - const table = useTable('emails', 'pk'); + const table = useTable('emails', { idAccessor: 'pk' }); const [selectedEmailId, setSelectedEmailId] = useState(''); diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 26e0245319..5f04978063 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -315,6 +315,8 @@ export function StockItemTable({ showLocation = true, showPricing = true, allowReturn = false, + initialFilters, + defaultInStock = true, tableName = 'stockitems' }: Readonly<{ params?: any; @@ -322,9 +324,34 @@ export function StockItemTable({ showLocation?: boolean; showPricing?: boolean; allowReturn?: boolean; + defaultInStock?: boolean | null; + initialFilters?: TableFilter[]; 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 settings = useGlobalSettingsState(); diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts index 67d517dfde..0a3a759d51 100644 --- a/src/frontend/tests/pages/pui_build.spec.ts +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -723,6 +723,8 @@ test('Build Order - External', async ({ browser }) => { await navigate(page, 'manufacturing/build-order/26/details'); await loadTab(page, 'External Orders'); + await clearTableFilters(page); + await page.getByRole('cell', { name: 'PO0017' }).waitFor(); await page.getByRole('cell', { name: 'PO0018' }).waitFor(); }); diff --git a/src/frontend/tests/pages/pui_company.spec.ts b/src/frontend/tests/pages/pui_company.spec.ts index 0d5834f1aa..a7f4545639 100644 --- a/src/frontend/tests/pages/pui_company.spec.ts +++ b/src/frontend/tests/pages/pui_company.spec.ts @@ -15,21 +15,28 @@ test('Company', async ({ browser }) => { await navigate(page, 'company/1/details'); await page.getByLabel('Details').getByText('DigiKey Electronics').waitFor(); await page.getByRole('cell', { name: 'https://www.digikey.com/' }).waitFor(); + await loadTab(page, 'Supplied Parts'); await page .getByRole('cell', { name: 'RR05P100KDTR-ND', exact: true }) .waitFor(); + await loadTab(page, 'Purchase Orders'); + await clearTableFilters(page); await page.getByRole('cell', { name: 'Molex connectors' }).first().waitFor(); + await loadTab(page, 'Stock Items'); await page .getByRole('cell', { name: 'Blue plastic enclosure' }) .first() .waitFor(); + await loadTab(page, 'Contacts'); await page.getByRole('cell', { name: 'jimmy.mcleod@digikey.com' }).waitFor(); + await loadTab(page, 'Addresses'); await page.getByRole('cell', { name: 'Carla Tunnel' }).waitFor(); + await loadTab(page, 'Attachments'); await loadTab(page, 'Notes'); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index eef7e89e91..cb4fd3a9f7 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -66,14 +66,16 @@ test('Parts - Tabs', 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 page.getByText('Hammond Manufacturing').click(); - await loadTab(page, 'Parameters'); - await loadTab(page, 'Suppliers'); - await loadTab(page, 'Attachments'); + + // Wait for manufacturer part page to load await page.getByText('1551ACLR - 1551ACLR').waitFor(); + await loadTab(page, 'Parameters'); + await loadTab(page, 'Attachments'); }); test('Parts - Supplier Parts', async ({ browser }) => { diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts index f78fa30a02..c8ff3f9393 100644 --- a/src/frontend/tests/pages/pui_purchase_order.spec.ts +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -25,6 +25,14 @@ test('Purchasing - Index', async ({ browser }) => { await showCalendarView(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 await loadTab(page, 'Suppliers'); await showParametricView(page); diff --git a/src/frontend/tests/pui_exporting.spec.ts b/src/frontend/tests/pui_exporting.spec.ts index 68ef9dc60c..70fbd7a9ad 100644 --- a/src/frontend/tests/pui_exporting.spec.ts +++ b/src/frontend/tests/pui_exporting.spec.ts @@ -33,7 +33,7 @@ test('Exporting - Orders', async ({ browser }) => { await page.getByText('Process completed successfully').waitFor(); // 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 openExportDialog(page); await page.getByRole('button', { name: 'Export', exact: true }).click(); diff --git a/src/frontend/tests/pui_printing.spec.ts b/src/frontend/tests/pui_printing.spec.ts index abd25ce299..f149949187 100644 --- a/src/frontend/tests/pui_printing.spec.ts +++ b/src/frontend/tests/pui_printing.spec.ts @@ -62,7 +62,7 @@ test('Printing - Report Printing', async ({ browser }) => { await loadTab(page, 'Purchase Orders'); await activateTableView(page); - await page.getByRole('cell', { name: 'PO0009' }).click(); + await page.getByRole('cell', { name: 'PO0013' }).click(); // Select "print report" await page.getByLabel('action-menu-printing-actions').click();