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:
@@ -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.
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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<TableFilter[]>({
|
||||
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,
|
||||
|
||||
@@ -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 <InvenTreeTable> 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<any[]>([]);
|
||||
@@ -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<boolean>(false);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -469,6 +469,7 @@ export default function BuildDetail() {
|
||||
tableName='build-consumed'
|
||||
showLocation={false}
|
||||
allowReturn
|
||||
defaultInStock={null}
|
||||
params={{
|
||||
consumed_by: id
|
||||
}}
|
||||
|
||||
@@ -248,6 +248,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
tableName='assigned-stock'
|
||||
showLocation={false}
|
||||
allowReturn
|
||||
defaultInStock={null}
|
||||
params={{ customer: company.pk }}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -108,6 +108,7 @@ export default function PurchasingIndex() {
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
companyType='supplier'
|
||||
path='purchasing/supplier'
|
||||
params={{ is_supplier: true }}
|
||||
/>
|
||||
@@ -157,6 +158,7 @@ export default function PurchasingIndex() {
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
companyType='manufacturer'
|
||||
path='purchasing/manufacturer'
|
||||
params={{ is_manufacturer: true }}
|
||||
/>
|
||||
|
||||
@@ -141,6 +141,7 @@ export default function SalesIndex() {
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
companyType='customer'
|
||||
path='sales/customer'
|
||||
params={{ is_customer: true }}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<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
|
||||
*/
|
||||
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 (
|
||||
<Paper p='sm' shadow='sm' radius='xs'>
|
||||
<Group justify='space-between' key={flt.name} wrap='nowrap'>
|
||||
<Stack gap='xs'>
|
||||
<Text size='sm'>{flt.label}</Text>
|
||||
<Text size='xs'>{flt.description}</Text>
|
||||
<Text size='sm'>{flt.label ?? filterProps?.label ?? flt.name}</Text>
|
||||
<Text size='xs'>{flt.description ?? filterProps?.description}</Text>
|
||||
</Stack>
|
||||
<Group justify='right'>
|
||||
<Badge>{flt.displayValue ?? flt.value}</Badge>
|
||||
<Tooltip label={t`Remove filter`} withinPortal={true}>
|
||||
<CloseButton size='md' color='red' onClick={removeFilter} />
|
||||
<Badge>
|
||||
{flt.displayValue ??
|
||||
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>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -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({
|
||||
<Stack gap='xs'>
|
||||
{hasFilters &&
|
||||
filterSet.activeFilters?.map((f) => (
|
||||
<FilterItem key={f.name} flt={f} filterSet={filterSet} />
|
||||
<FilterItem
|
||||
key={f.name}
|
||||
flt={f}
|
||||
filterSet={filterSet}
|
||||
availableFilters={availableFilters}
|
||||
/>
|
||||
))}
|
||||
{addFilter && (
|
||||
<Stack gap='xs'>
|
||||
|
||||
@@ -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({
|
||||
<StylishText size='md'>{t`Active Filters`}</StylishText>
|
||||
<Divider />
|
||||
{tableState.filterSet.activeFilters?.map((filter) => (
|
||||
<Group
|
||||
key={filter.name}
|
||||
justify='space-between'
|
||||
gap='xl'
|
||||
wrap='nowrap'
|
||||
>
|
||||
<Text size='sm'>{filter.label}</Text>
|
||||
<Text size='xs'>{filter.displayValue}</Text>
|
||||
</Group>
|
||||
<FilterPreview
|
||||
filter={filter}
|
||||
filters={tableProps.tableFilters}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export function PartVariantTable({ part }: Readonly<{ part: any }>) {
|
||||
ancestor: part.pk
|
||||
}
|
||||
}}
|
||||
tableName='part-variants'
|
||||
basePartInstance={part}
|
||||
defaultPartData={{
|
||||
...part,
|
||||
|
||||
@@ -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<PluginRegistryErrorI>[] =
|
||||
useMemo(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -61,7 +61,7 @@ export function EmailTable() {
|
||||
];
|
||||
}, []);
|
||||
|
||||
const table = useTable('emails', 'pk');
|
||||
const table = useTable('emails', { idAccessor: 'pk' });
|
||||
|
||||
const [selectedEmailId, setSelectedEmailId] = useState<string>('');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user