2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-11 19:27:02 +00:00

[UI] Table column filters (#12103)

* Expose properties for column based filtering

* Adjust renderers

* Hide filter if name does not match

* Allow multiple filters on same column

* Better formatting

* Add filtering support for multiple tables

* Revert yarn.lock changes

* Fix date input props

* Updated column

* Add filter  to PartTable

* Add playwright tests for new column filters

* Update CHANGELOG

* Updated docs

* Reduce padding

* Update more table filters

* More filter columns

* Adjust playwright test

* Simplify playwright test

* Robustify playwright tests

* Add some delay

* Add some buffer time
This commit is contained in:
Oliver
2026-06-07 12:59:33 +10:00
committed by GitHub
parent a86e94c63d
commit 7f1f2dbad2
33 changed files with 407 additions and 99 deletions
+1
View File
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns.
- [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API) - [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API)
- [#12079](https://github.com/inventree/InvenTree/pull/12079) adds the ability to save filter groups for table and calendar views in the user interface. This allows users to save and reuse commonly used filter configurations, improving the usability and efficiency of the interface. - [#12079](https://github.com/inventree/InvenTree/pull/12079) adds the ability to save filter groups for table and calendar views in the user interface. This allows users to save and reuse commonly used filter configurations, improving the usability and efficiency of the interface.
- [#12077](https://github.com/inventree/InvenTree/pull/12077) adds "tags" fields to multiple new model types and a /api/tag/ endpoint for fetching tags. Also adds the ability to filter various model types by tags. - [#12077](https://github.com/inventree/InvenTree/pull/12077) adds "tags" fields to multiple new model types and a /api/tag/ endpoint for fetching tags. Also adds the ability to filter various model types by tags.
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+17
View File
@@ -158,6 +158,23 @@ Select the "table filters" button to open the filter selection menu
Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view. Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view.
#### Column Filters
Many table columns expose an inline filter icon directly in the column header, providing a quick way to filter by that column without opening the full filter drawer. Columns that support filtering display a small filter icon alongside the column title. The icon is highlighted when a filter for that column is currently active, giving an at-a-glance indication of which columns have active filters.
Clicking the icon opens a compact popover anchored to the column header:
{{ image("concepts/ui_table_column_filter_popover.png", "Column Filter Popover") }}
**Single-filter columns** — for columns linked to one filter (e.g. *Active*, *Has IPN*, *Status*), selecting a value immediately applies the filter and the popover closes automatically.
**Range columns** — for columns that represent a range concept (e.g. *Start Date*, *Target Date*, *Creation Date*), the popover stays open and presents multiple controls — for example *before* and *after* date pickers — so both bounds can be set in a single interaction.
Once a filter is active, the popover shows a badge with the current value and a remove button (red ×) instead of the value picker. Clicking the × clears only that column's filter.
!!! info "Column filters and the filter drawer share the same state"
Filters applied via a column popover appear immediately in the filter drawer's active-filter list, and filters added through the drawer are reflected in the column icons. Clearing all filters from the drawer also removes any filters set via column popovers.
#### Saved Filter Groups #### Saved Filter Groups
Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually. Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually.
@@ -23,7 +23,7 @@ export function TableColumnSelect({
<Menu.Label>{t`Select Columns`}</Menu.Label> <Menu.Label>{t`Select Columns`}</Menu.Label>
<Divider /> <Divider />
{columns {columns
.filter((col) => col.switchable ?? true) .filter((col) => (col.switchable ?? true) && !col.propHidden)
.map((col) => ( .map((col) => (
<Menu.Item key={col.accessor}> <Menu.Item key={col.accessor}>
<Checkbox <Checkbox
+5 -2
View File
@@ -84,7 +84,7 @@ export type TableState = {
* @param editable - Whether the value of this column can be edited * @param editable - Whether the value of this column can be edited
* @param definition - Optional field definition for the column * @param definition - Optional field definition for the column
* @param render - A custom render function * @param render - A custom render function
* @param filter - A custom filter function * @param filter - Filter name (string) to look up from tableFilters and attach an inline icon, or a custom render function for the filter popover
* @param filtering - Whether the column is filterable * @param filtering - Whether the column is filterable
* @param width - The width of the column * @param width - The width of the column
* @param minWidth - The minimum width of the column * @param minWidth - The minimum width of the column
@@ -109,7 +109,10 @@ export type TableColumnProps<T = any> = {
editable?: boolean; editable?: boolean;
definition?: ApiFormFieldType; definition?: ApiFormFieldType;
render?: (record: T, index?: number) => any; render?: (record: T, index?: number) => any;
filter?: any; filter?:
| string
| string[]
| (({ close }: { close: () => void }) => ReactNode);
filtering?: boolean; filtering?: boolean;
width?: number; width?: number;
minWidth?: string | number; minWidth?: string | number;
@@ -117,6 +117,7 @@ export function IPNColumn(props: TableColumnProps): TableColumn {
switchable: true, switchable: true,
title: t`IPN`, title: t`IPN`,
copyable: true, copyable: true,
filter: 'has_ipn',
...props ...props
}; };
} }
@@ -433,6 +434,7 @@ export function BooleanColumn(props: TableColumn): TableColumn {
sortable: true, sortable: true,
switchable: true, switchable: true,
minWidth: '75px', minWidth: '75px',
filter: props.filter ?? props.accessor,
render: (record: any) => ( render: (record: any) => (
<Center> <Center>
<YesNoButton value={resolveItem(record, props.accessor ?? '')} /> <YesNoButton value={resolveItem(record, props.accessor ?? '')} />
@@ -564,6 +566,7 @@ export function ProjectCodeColumn(props: TableColumnProps): TableColumn {
sortable: true, sortable: true,
title: t`Project Code`, title: t`Project Code`,
hidden: !enabled, hidden: !enabled,
filter: 'project_code',
render: (record: any) => { render: (record: any) => {
const project_code = resolveItem( const project_code = resolveItem(
record, record,
@@ -584,6 +587,7 @@ export function StatusColumn(props: StatusColumnProps): TableColumn {
return { return {
accessor: 'status', accessor: 'status',
filter: 'status',
sortable: true, sortable: true,
switchable: true, switchable: true,
minWidth: '50px', minWidth: '50px',
@@ -636,6 +640,7 @@ export function CreatedByColumn(props: TableColumnProps): TableColumn {
accessor: 'created_by', accessor: 'created_by',
ordering: 'created_by', ordering: 'created_by',
title: t`Created By`, title: t`Created By`,
filter: 'created_by',
...props ...props
}); });
} }
@@ -665,6 +670,7 @@ export function ResponsibleColumn(props: TableColumnProps): TableColumn {
accessor: 'responsible_detail', accessor: 'responsible_detail',
ordering: 'responsible', ordering: 'responsible',
title: t`Responsible`, title: t`Responsible`,
filter: 'assigned_to',
...props ...props
}); });
} }
@@ -688,6 +694,7 @@ export function StartDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'start_date', accessor: 'start_date',
title: t`Start Date`, title: t`Start Date`,
filter: ['has_start_date', 'start_date_before', 'start_date_after'],
...props ...props
}); });
} }
@@ -696,6 +703,7 @@ export function TargetDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'target_date', accessor: 'target_date',
title: t`Target Date`, title: t`Target Date`,
filter: ['has_target_date', 'target_date_before', 'target_date_after'],
...props ...props
}); });
} }
@@ -704,6 +712,7 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'creation_date', accessor: 'creation_date',
title: t`Creation Date`, title: t`Creation Date`,
filter: ['created_before', 'created_after'],
...props ...props
}); });
} }
@@ -712,6 +721,7 @@ export function CompletionDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'completion_date', accessor: 'completion_date',
title: t`Completion Date`, title: t`Completion Date`,
filter: ['completed_before', 'completed_after'],
...props ...props
}); });
} }
@@ -720,6 +730,7 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
return DateColumn({ return DateColumn({
accessor: 'shipment_date', accessor: 'shipment_date',
title: t`Shipment Date`, title: t`Shipment Date`,
filter: ['shipment_date_before', 'shipment_date_after'],
...props ...props
}); });
} }
@@ -729,6 +740,7 @@ export function UpdatedAtColumn(props: TableColumnProps): TableColumn {
accessor: 'updated_at', accessor: 'updated_at',
title: t`Updated`, title: t`Updated`,
defaultVisible: false, defaultVisible: false,
filter: ['updated_before', 'updated_after'],
extra: { showTime: true }, extra: { showTime: true },
...props ...props
}); });
+147 -22
View File
@@ -182,15 +182,17 @@ function MultiApiFilterElement({
); );
} }
function FilterElement({ export function FilterElement({
filterName, filterName,
filterProps, filterProps,
valueOptions, valueOptions,
brief = false,
onValueChange onValueChange
}: { }: {
filterName: string; filterName: string;
filterProps: TableFilter; filterProps: TableFilter;
valueOptions: TableFilterChoice[]; valueOptions: TableFilterChoice[];
brief?: boolean;
onValueChange: (value: string | null, displayValue?: any) => void; onValueChange: (value: string | null, displayValue?: any) => void;
}) { }) {
const setDateValue = useCallback( const setDateValue = useCallback(
@@ -226,7 +228,7 @@ function FilterElement({
filters: filterProps.apiFilter, filters: filterProps.apiFilter,
placeholder: t`Select filter value`, placeholder: t`Select filter value`,
model: filterProps.model, model: filterProps.model,
label: t`Select filter value`, label: brief ? undefined : t`Select filter value`,
onValueChange: (value: any, instance: any) => { onValueChange: (value: any, instance: any) => {
if (filterProps.transform) { if (filterProps.transform) {
const choice = filterProps.transform(instance); const choice = filterProps.transform(instance);
@@ -244,9 +246,10 @@ function FilterElement({
case 'text': case 'text':
return ( return (
<TextInput <TextInput
label={t`Value`} label={brief ? undefined : t`Value`}
value={textValue} value={textValue}
placeholder={t`Enter filter value`} placeholder={t`Enter filter value`}
aria-label={`text-filter-${filterName}`}
rightSection={ rightSection={
<ActionIcon <ActionIcon
aria-label='apply-text-filter' aria-label='apply-text-filter'
@@ -256,8 +259,10 @@ function FilterElement({
<IconCheck /> <IconCheck />
</ActionIcon> </ActionIcon>
} }
onChange={(e) => setTextValue(e.currentTarget.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onKeyDown={(e) => { setTextValue(e.currentTarget.value)
}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
onValueChange(textValue); onValueChange(textValue);
} }
@@ -267,9 +272,11 @@ function FilterElement({
case 'date': case 'date':
return ( return (
<DateInput <DateInput
label={t`Value`} aria-label={`date-filter-${filterName}`}
label={brief ? undefined : t`Value`}
placeholder={t`Select date value`} placeholder={t`Select date value`}
onChange={setDateValue} onChange={setDateValue}
popoverProps={{ withinPortal: false }}
/> />
); );
case 'choice': case 'choice':
@@ -278,8 +285,9 @@ function FilterElement({
return ( return (
<Select <Select
data={valueOptions} data={valueOptions}
aria-label={`choice-filter-${filterName}`}
searchable={filterProps.type == 'choice'} searchable={filterProps.type == 'choice'}
label={t`Value`} label={brief ? undefined : t`Value`}
withScrollArea={false} withScrollArea={false}
placeholder={t`Select filter value`} placeholder={t`Select filter value`}
onChange={(value: string | null) => onValueChange(value)} onChange={(value: string | null) => onValueChange(value)}
@@ -289,6 +297,13 @@ function FilterElement({
} }
} }
function getFilterType(filter: TableFilter): TableFilterType {
if (filter.type) return filter.type;
if (filter.apiUrl && filter.model) return 'api';
if (filter.choices || filter.choiceFunction) return 'choice';
return 'boolean';
}
function FilterAddGroup({ function FilterAddGroup({
filterSet, filterSet,
availableFilters availableFilters
@@ -331,19 +346,6 @@ function FilterAddGroup({
return getTableFilterOptions(filter); return getTableFilterOptions(filter);
}, [selectedFilter]); }, [selectedFilter]);
// Determine the filter "type" - if it is not supplied
const getFilterType = (filter: TableFilter): TableFilterType => {
if (filter.type) {
return filter.type;
} else if (filter.apiUrl && filter.model) {
return 'api';
} else if (filter.choices || filter.choiceFunction) {
return 'choice';
} else {
return 'boolean';
}
};
// Extract filter definition // Extract filter definition
const filterProps: TableFilter | undefined = useMemo(() => { const filterProps: TableFilter | undefined = useMemo(() => {
const filter = availableFilters?.find((flt) => flt.name === selectedFilter); const filter = availableFilters?.find((flt) => flt.name === selectedFilter);
@@ -478,6 +480,127 @@ function SavedFilterSets({
); );
} }
/*
* Renders a single filter's title, active-value badge, remove button, and
* value-picker. Used as a building block inside ColumnFilterPopover.
*/
function FilterSection({
filter,
filterSet,
closeOnApply,
close
}: Readonly<{
filter: TableFilter;
filterSet: FilterSetState;
closeOnApply: boolean;
close: () => void;
}>) {
const activeFilter = useMemo(
() => filterSet.activeFilters.find((f) => f.name === filter.name),
[filter.name, filterSet.activeFilters]
);
const filterProps = useMemo(
() => ({ ...filter, type: getFilterType(filter) }),
[filter]
);
const valueOptions = useMemo(
() => getTableFilterOptions(filterProps),
[filterProps]
);
const onValueChange = useCallback(
(value: string | null, displayValue?: any) => {
if (!value) return;
const newFilter: TableFilter = {
...filterProps,
value,
displayValue:
displayValue ?? valueOptions.find((v) => v.value === value)?.label
};
const others = filterSet.activeFilters.filter(
(f) => f.name !== filter.name
);
filterSet.setActiveFilters([...others, newFilter]);
if (closeOnApply) close();
},
[filter.name, filterProps, filterSet, valueOptions, closeOnApply, close]
);
const removeFilter = useCallback(() => {
filterSet.setActiveFilters(
filterSet.activeFilters.filter((f) => f.name !== filter.name)
);
close();
}, [filter.name, filterSet, close]);
return (
<Stack gap='xs'>
<Text size='sm' fw={600}>
{filter.label ?? filter.name}
</Text>
{activeFilter ? (
<Group justify='space-between' wrap='nowrap'>
<Badge color='blue'>
{activeFilter.displayValue ?? activeFilter.value}
</Badge>
<ActionIcon
color='red'
variant='transparent'
size='sm'
onClick={removeFilter}
>
<IconX />
</ActionIcon>
</Group>
) : (
<FilterElement
filterName={filter.name}
filterProps={filterProps}
valueOptions={valueOptions}
brief={true}
onValueChange={onValueChange}
/>
)}
</Stack>
);
}
/*
* Popover content rendered when the user clicks a column's inline filter icon.
* Renders one FilterSection per matched filter, each with its own title, active
* value, and value-picker. Auto-closes on apply only when there is a single
* filter (e.g. date-range columns stay open so both bounds can be set at once).
*/
export function ColumnFilterPopover({
filters,
filterSet,
close
}: Readonly<{
filters: TableFilter[];
filterSet: FilterSetState;
close: () => void;
}>) {
const closeOnApply = filters.length === 1;
return (
<Stack gap={5} p={3}>
{filters.map((filter, index) => (
<Stack gap='xs' key={filter.name}>
{index > 0 && <Divider />}
<FilterSection
filter={filter}
filterSet={filterSet}
closeOnApply={closeOnApply}
close={close}
/>
</Stack>
))}
</Stack>
);
}
export function FilterSelectDrawer({ export function FilterSelectDrawer({
title, title,
availableFilters, availableFilters,
@@ -584,8 +707,10 @@ export function FilterSelectDrawer({
aria-label='filter-group-name' aria-label='filter-group-name'
placeholder={t`Group name`} placeholder={t`Group name`}
value={saveName} value={saveName}
onChange={(e) => setSaveName(e.currentTarget.value)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onKeyDown={(e) => { setSaveName(e.currentTarget.value)
}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') confirmSave(); if (e.key === 'Enter') confirmSave();
if (e.key === 'Escape') setSaving(false); if (e.key === 'Escape') setSaving(false);
}} }}
+53 -7
View File
@@ -38,6 +38,7 @@ import { extractAvailableFields } from '../functions/forms';
import { showApiErrorMessage } from '../functions/notifications'; import { showApiErrorMessage } from '../functions/notifications';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useUserSettingsState } from '../states/SettingsStates'; import { useUserSettingsState } from '../states/SettingsStates';
import { ColumnFilterPopover } from './FilterSelectDrawer';
import InvenTreeTableHeader from './InvenTreeTableHeader'; import InvenTreeTableHeader from './InvenTreeTableHeader';
const ACTIONS_COLUMN_ACCESSOR: string = '--actions--'; const ACTIONS_COLUMN_ACCESSOR: string = '--actions--';
@@ -265,12 +266,12 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
// Prop-level hidden takes priority (e.g. hidden: !hasTrackedItems). // Prop-level hidden takes priority (e.g. hidden: !hasTrackedItems).
// For switchable columns, visibility is driven by tableState.hiddenColumns. // For switchable columns, visibility is driven by tableState.hiddenColumns.
// Non-switchable columns are always visible (unless hidden by props). // Non-switchable columns are always visible (unless hidden by props).
const hidden: boolean = const propHidden: boolean = col.hidden === true;
col.hidden === true const hidden: boolean = propHidden
? true ? true
: col.switchable == false : col.switchable == false
? false ? false
: (tableState.hiddenColumns?.includes(col.accessor) ?? false); : (tableState.hiddenColumns?.includes(col.accessor) ?? false);
// Wrap the render function with CopyableCell if copyable is enabled // Wrap the render function with CopyableCell if copyable is enabled
const originalRender = col.render; const originalRender = col.render;
@@ -300,12 +301,55 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
}; };
} }
// col.filter can be:
// string → single filter name to look up in tableFilters
// string[] → multiple filter names; all matches shown in one popover
// function → direct mantine-datatable render function (e.g. parametric columns)
// undefined → no column filter
const filterNames: string[] =
typeof col.filter === 'string'
? [col.filter]
: Array.isArray(col.filter)
? col.filter
: [];
const namedFilters: TableFilter[] =
filterNames.length > 0
? filters.filter((f) => filterNames.includes(f.name))
: [];
const namedFiltersActive =
namedFilters.length > 0 &&
namedFilters.some((nf) =>
tableState.filterSet.activeFilters.some((af) => af.name === nf.name)
);
// Resolve the final filter prop:
// named string(s) with matches → build popover render function
// named string(s) with no match → undefined (suppress icon)
// function → pass through unchanged (e.g. parametric columns)
const resolvedFilter =
namedFilters.length > 0
? ({ close }: { close: () => void }) => (
<ColumnFilterPopover
filters={namedFilters}
filterSet={tableState.filterSet}
close={close}
/>
)
: filterNames.length > 0
? undefined
: col.filter;
return { return {
...col, ...col,
hidden: hidden, hidden: hidden,
resizable: col.resizable ?? true, resizable: col.resizable ?? true,
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`, title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`,
render: wrappedRender, render: wrappedRender,
filter: resolvedFilter,
filtering: namedFilters.length > 0 ? namedFiltersActive : col.filtering,
propHidden: propHidden,
cellsStyle: (record: any, index: number) => { cellsStyle: (record: any, index: number) => {
const width = (col as any).minWidth ?? 100; const width = (col as any).minWidth ?? 100;
return { return {
@@ -347,10 +391,12 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
return cols; return cols;
}, [ }, [
columns, columns,
filters,
fieldNames, fieldNames,
tableProps.rowActions, tableProps.rowActions,
tableState.hiddenColumns, tableState.hiddenColumns,
tableState.selectedRecords tableState.selectedRecords,
tableState.filterSet.activeFilters
]); ]);
// Callback when column visibility is toggled // Callback when column visibility is toggled
+6 -5
View File
@@ -171,11 +171,6 @@ export function BomTable({
DescriptionColumn({ DescriptionColumn({
accessor: 'sub_part_detail.description' accessor: 'sub_part_detail.description'
}), }),
BooleanColumn({
accessor: 'sub_part_detail.virtual',
defaultVisible: false,
title: t`Virtual Part`
}),
ReferenceColumn({ ReferenceColumn({
switchable: true switchable: true
}), }),
@@ -274,6 +269,12 @@ export function BomTable({
); );
} }
}, },
BooleanColumn({
accessor: 'sub_part_detail.virtual',
filter: 'sub_part_virtual',
defaultVisible: false,
title: t`Virtual Part`
}),
BooleanColumn({ BooleanColumn({
accessor: 'optional', accessor: 'optional',
defaultVisible: false defaultVisible: false
+2 -1
View File
@@ -44,7 +44,8 @@ export function UsedInTable({
return [ return [
PartColumn({ PartColumn({
title: t`Assembly`, title: t`Assembly`,
part: 'part_detail' part: 'part_detail',
filter: ['part_active', 'part_locked']
}), }),
IPNColumn({ IPNColumn({
sortable: true sortable: true
@@ -322,6 +322,7 @@ export default function BuildLineTable({
ordering: 'part', ordering: 'part',
sortable: true, sortable: true,
switchable: false, switchable: false,
filter: ['assembly', 'testable', 'tracked'],
render: (record: any) => { render: (record: any) => {
const hasAllocatedItems = record.allocatedQuantity > 0; const hasAllocatedItems = record.allocatedQuantity > 0;
@@ -356,12 +357,14 @@ export default function BuildLineTable({
BooleanColumn({ BooleanColumn({
accessor: 'bom_item_detail.optional', accessor: 'bom_item_detail.optional',
ordering: 'optional', ordering: 'optional',
filter: 'optional',
hidden: hasOutput, hidden: hasOutput,
defaultVisible: false defaultVisible: false
}), }),
BooleanColumn({ BooleanColumn({
accessor: 'bom_item_detail.consumable', accessor: 'bom_item_detail.consumable',
ordering: 'consumable', ordering: 'consumable',
filter: 'consumable',
hidden: hasOutput, hidden: hasOutput,
defaultVisible: false defaultVisible: false
}), }),
@@ -381,6 +384,7 @@ export default function BuildLineTable({
BooleanColumn({ BooleanColumn({
accessor: 'part_detail.trackable', accessor: 'part_detail.trackable',
ordering: 'trackable', ordering: 'trackable',
filter: 'tracked',
hidden: hasOutput, hidden: hasOutput,
defaultVisible: false defaultVisible: false
}), }),
@@ -457,6 +461,7 @@ export default function BuildLineTable({
{ {
accessor: 'available_stock', accessor: 'available_stock',
sortable: true, sortable: true,
filter: 'available',
switchable: false, switchable: false,
render: renderAvailableColumn render: renderAvailableColumn
}, },
@@ -481,12 +486,14 @@ export default function BuildLineTable({
DecimalColumn({ DecimalColumn({
accessor: 'on_order', accessor: 'on_order',
defaultVisible: false, defaultVisible: false,
filter: 'on_order',
sortable: true sortable: true
}), }),
{ {
accessor: 'allocated', accessor: 'allocated',
switchable: false, switchable: false,
sortable: true, sortable: true,
filter: 'allocated',
hidden: !isActive, hidden: !isActive,
minWidth: 125, minWidth: 125,
render: (record: any) => { render: (record: any) => {
@@ -532,6 +539,7 @@ export default function BuildLineTable({
{ {
accessor: 'consumed', accessor: 'consumed',
sortable: true, sortable: true,
filter: 'consumed',
hidden: !!output?.pk, hidden: !!output?.pk,
minWidth: 125, minWidth: 125,
render: (record: any) => { render: (record: any) => {
@@ -15,8 +15,8 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { import {
BooleanColumn, BooleanColumn,
CompletionDateColumn,
CreationDateColumn, CreationDateColumn,
DateColumn,
DescriptionColumn, DescriptionColumn,
IPNColumn, IPNColumn,
LinkColumn, LinkColumn,
@@ -104,6 +104,7 @@ export function BuildOrderTable({
BooleanColumn({ BooleanColumn({
accessor: 'external', accessor: 'external',
title: t`External`, title: t`External`,
filter: 'external',
sortable: true, sortable: true,
switchable: true, switchable: true,
hidden: !globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS') hidden: !globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
@@ -115,11 +116,7 @@ export function BuildOrderTable({
defaultVisible: false defaultVisible: false
}), }),
TargetDateColumn({}), TargetDateColumn({}),
DateColumn({ CompletionDateColumn({}),
accessor: 'completion_date',
title: t`Completion Date`,
sortable: true
}),
UserColumn({ UserColumn({
accessor: 'issued_by_detail', accessor: 'issued_by_detail',
ordering: 'issued_by', ordering: 'issued_by',
@@ -64,6 +64,7 @@ export function CompanyTable({
DescriptionColumn({}), DescriptionColumn({}),
BooleanColumn({ BooleanColumn({
accessor: 'active', accessor: 'active',
filter: 'active',
title: t`Active`, title: t`Active`,
sortable: true, sortable: true,
switchable: true switchable: true
@@ -54,7 +54,8 @@ export function ParameterTable({
accessor: 'template_detail.name', accessor: 'template_detail.name',
switchable: false, switchable: false,
sortable: true, sortable: true,
ordering: 'name' ordering: 'name',
filter: 'enabled'
}, },
DescriptionColumn({ DescriptionColumn({
accessor: 'template_detail.description' accessor: 'template_detail.description'
@@ -105,6 +106,7 @@ export function ParameterTable({
UserColumn({ UserColumn({
accessor: 'updated_by_detail', accessor: 'updated_by_detail',
ordering: 'updated_by', ordering: 'updated_by',
filter: 'updated_by',
title: t`Updated By` title: t`Updated By`
}) })
]; ];
@@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '@lib/components/AddItemButton'; import { AddItemButton } from '@lib/components/AddItemButton';
import { type RowAction, RowEditAction } from '@lib/components/RowActions'; import { type RowAction, RowEditAction } from '@lib/components/RowActions';
import { YesNoButton } from '@lib/components/YesNoButton';
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 { UserRoles } from '@lib/enums/Roles'; import { UserRoles } from '@lib/enums/Roles';
@@ -23,7 +22,7 @@ import {
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { DescriptionColumn } from '../ColumnRenderers'; import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
/** /**
@@ -64,14 +63,11 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
copyable: true, copyable: true,
sortable: true sortable: true
}, },
{ BooleanColumn({
accessor: 'structural', accessor: 'structural',
sortable: true, sortable: true,
defaultVisible: false, defaultVisible: false
render: (record: any) => { }),
return <YesNoButton value={record.structural} />;
}
},
{ {
accessor: 'part_count', accessor: 'part_count',
sortable: true sortable: true
@@ -29,6 +29,7 @@ export default function PartPurchaseOrdersTable({
ordering: 'order', ordering: 'order',
sortable: true, sortable: true,
switchable: false, switchable: false,
filter: ['pending', 'received'],
title: t`Purchase Order` title: t`Purchase Order`
}), }),
StatusColumn({ StatusColumn({
@@ -36,6 +37,7 @@ export default function PartPurchaseOrdersTable({
sortable: true, sortable: true,
ordering: 'status', ordering: 'status',
title: t`Status`, title: t`Status`,
filter: 'order_status',
model: ModelType.purchaseorder model: ModelType.purchaseorder
}), }),
{ {
+15 -3
View File
@@ -37,6 +37,7 @@ import { useImporterState } from '../../states/ImporterState';
import { useGlobalSettingsState } from '../../states/SettingsStates'; import { useGlobalSettingsState } from '../../states/SettingsStates';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { import {
BooleanColumn,
CategoryColumn, CategoryColumn,
DefaultLocationColumn, DefaultLocationColumn,
DescriptionColumn, DescriptionColumn,
@@ -55,7 +56,8 @@ function partTableColumns(): TableColumn[] {
return [ return [
PartColumn({ PartColumn({
part: '', part: '',
accessor: 'name' accessor: 'name',
filter: ['active', 'locked', 'starred']
}), }),
IPNColumn({ IPNColumn({
accessor: 'IPN' accessor: 'IPN'
@@ -67,7 +69,8 @@ function partTableColumns(): TableColumn[] {
{ {
accessor: 'units', accessor: 'units',
sortable: true, sortable: true,
copyable: true copyable: true,
filter: 'has_units'
}, },
DescriptionColumn({}), DescriptionColumn({}),
CategoryColumn({ CategoryColumn({
@@ -79,7 +82,7 @@ function partTableColumns(): TableColumn[] {
{ {
accessor: 'total_in_stock', accessor: 'total_in_stock',
sortable: true, sortable: true,
filter: ['has_stock', 'low_stock', 'high_stock'],
render: (record) => { render: (record) => {
if (record.virtual) { if (record.virtual) {
return ( return (
@@ -197,10 +200,19 @@ function partTableColumns(): TableColumn[] {
title: t`Price Range`, title: t`Price Range`,
sortable: true, sortable: true,
ordering: 'pricing_max', ordering: 'pricing_max',
filter: 'has_pricing',
defaultVisible: false, defaultVisible: false,
render: (record: any) => render: (record: any) =>
formatPriceRange(record.pricing_min, record.pricing_max) formatPriceRange(record.pricing_min, record.pricing_max)
}, },
BooleanColumn({
accessor: 'assembly',
defaultVisible: false
}),
BooleanColumn({
accessor: 'virtual',
defaultVisible: false
}),
LinkColumn({}) LinkColumn({})
]; ];
} }
@@ -26,7 +26,7 @@ import { useApi } from '../../contexts/ApiContext';
import { formatDate } from '../../defaults/formatters'; import { formatDate } from '../../defaults/formatters';
import { useTestResultFields } from '../../forms/StockForms'; import { useTestResultFields } from '../../forms/StockForms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { LocationColumn, PartColumn } from '../ColumnRenderers'; import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers';
import { import {
BatchFilter, BatchFilter,
HasBatchCodeFilter, HasBatchCodeFilter,
@@ -256,6 +256,7 @@ export default function PartTestResultTable({
title: t`Stock Item`, title: t`Stock Item`,
sortable: true, sortable: true,
switchable: false, switchable: false,
filter: ['in_stock', 'is_building'],
render: (record: any) => { render: (record: any) => {
if (record.serial) { if (record.serial) {
return `# ${record.serial}`; return `# ${record.serial}`;
@@ -284,10 +285,15 @@ export default function PartTestResultTable({
} }
} }
}, },
StatusColumn({
accessor: 'status',
model: ModelType.stockitem
}),
{ {
accessor: 'batch', accessor: 'batch',
title: t`Batch Code`, title: t`Batch Code`,
sortable: true, sortable: true,
filter: ['batch', 'has_batch_code'],
switchable: true, switchable: true,
copyable: true copyable: true
}, },
@@ -86,12 +86,14 @@ export function ManufacturerPartTable({
const tableColumns: TableColumn[] = useMemo(() => { const tableColumns: TableColumn[] = useMemo(() => {
return [ return [
PartColumn({ PartColumn({
switchable: !!partId switchable: !!partId,
filter: 'part_active'
}), }),
IPNColumn({}), IPNColumn({}),
{ {
accessor: 'manufacturer', accessor: 'manufacturer',
sortable: true, sortable: true,
filter: 'manufacturer_active',
render: (record: any) => ( render: (record: any) => (
<CompanyColumn company={record?.manufacturer_detail} /> <CompanyColumn company={record?.manufacturer_detail} />
) )
@@ -93,11 +93,13 @@ export function SupplierPartTable({
return [ return [
PartColumn({ PartColumn({
switchable: !!partId, switchable: !!partId,
part: 'part_detail' part: 'part_detail',
filter: ['part_active']
}), }),
IPNColumn({}), IPNColumn({}),
{ {
accessor: 'supplier', accessor: 'supplier',
filter: 'supplier_active',
sortable: true, sortable: true,
render: (record: any) => ( render: (record: any) => (
<CompanyColumn company={record?.supplier_detail} /> <CompanyColumn company={record?.supplier_detail} />
@@ -179,6 +181,7 @@ export function SupplierPartTable({
accessor: 'available', accessor: 'available',
sortable: true, sortable: true,
defaultVisible: false, defaultVisible: false,
filter: 'has_stock',
render: (record: any) => { render: (record: any) => {
const extra = []; const extra = [];
@@ -159,6 +159,7 @@ export default function SalesOrderShipmentTable({
{ {
accessor: 'checked', accessor: 'checked',
title: t`Checked`, title: t`Checked`,
filter: 'checked',
switchable: true, switchable: true,
sortable: false, sortable: false,
render: (record: any) => <YesNoButton value={!!record.checked_by} /> render: (record: any) => <YesNoButton value={!!record.checked_by} />
@@ -168,12 +169,14 @@ export default function SalesOrderShipmentTable({
title: t`Shipped`, title: t`Shipped`,
switchable: true, switchable: true,
sortable: false, sortable: false,
filter: 'shipped',
render: (record: any) => <YesNoButton value={!!record.shipment_date} /> render: (record: any) => <YesNoButton value={!!record.shipment_date} />
}, },
{ {
accessor: 'delivered', accessor: 'delivered',
title: t`Delivered`, title: t`Delivered`,
switchable: true, switchable: true,
filter: 'delivered',
sortable: false, sortable: false,
render: (record: any) => <YesNoButton value={!!record.delivery_date} /> render: (record: any) => <YesNoButton value={!!record.delivery_date} />
}, },
@@ -63,7 +63,8 @@ function stockItemTableColumns({
return [ return [
PartColumn({ PartColumn({
accessor: 'part', accessor: 'part',
part: 'part_detail' part: 'part_detail',
filter: ['active']
}), }),
IPNColumn({}), IPNColumn({}),
{ {
@@ -79,13 +80,21 @@ function stockItemTableColumns({
accessor: '', accessor: '',
title: t`Stock`, title: t`Stock`,
sortable: true, sortable: true,
ordering: 'stock' ordering: 'stock',
filter: [
'available',
'allocated',
'consumed',
'installed',
'sent_to_customer'
]
}), }),
StatusColumn({ model: ModelType.stockitem }), StatusColumn({ model: ModelType.stockitem }),
{ {
accessor: 'batch', accessor: 'batch',
sortable: true, sortable: true,
copyable: true copyable: true,
filter: ['has_batch_code', 'batch']
}, },
LocationColumn({ LocationColumn({
hidden: !showLocation, hidden: !showLocation,
@@ -151,22 +160,26 @@ function stockItemTableColumns({
DateColumn({ DateColumn({
title: t`Created`, title: t`Created`,
accessor: 'creation_date', accessor: 'creation_date',
sortable: true sortable: true,
filter: ['created_before', 'created_after']
}), }),
DateColumn({ DateColumn({
title: t`Last Updated`, title: t`Last Updated`,
accessor: 'updated' accessor: 'updated',
filter: ['updated_before', 'updated_after']
}), }),
DateColumn({ DateColumn({
title: t`Expiry Date`, title: t`Expiry Date`,
accessor: 'expiry_date', accessor: 'expiry_date',
hidden: !useGlobalSettingsState.getState().isSet('STOCK_ENABLE_EXPIRY'), hidden: !useGlobalSettingsState.getState().isSet('STOCK_ENABLE_EXPIRY'),
defaultVisible: false defaultVisible: false,
filter: ['stale', 'expiry_before', 'expiry_after']
}), }),
DateColumn({ DateColumn({
accessor: 'stocktake_date', accessor: 'stocktake_date',
title: t`Stocktake Date`, title: t`Stocktake Date`,
sortable: true sortable: true,
filter: ['has_stocktake', 'stocktake_before', 'stocktake_after']
}) })
]; ];
} }
@@ -144,6 +144,7 @@ export default function StockItemTestResultTable({
title: t`Test`, title: t`Test`,
switchable: false, switchable: false,
sortable: true, sortable: true,
filter: ['enabled', 'required'],
render: (record: any) => { render: (record: any) => {
const enabled = record.enabled ?? record.template_detail?.enabled; const enabled = record.enabled ?? record.template_detail?.enabled;
const installed = const installed =
@@ -97,6 +97,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
{ {
accessor: 'location_type', accessor: 'location_type',
sortable: false, sortable: false,
filter: ['has_location_type', 'location_type'],
render: (record: any) => record.location_type_detail?.name render: (record: any) => record.location_type_detail?.name
} }
]; ];
+20
View File
@@ -210,3 +210,23 @@ export const expectTableColumnCount = async (page: Page, count: number) => {
const columns = page.locator('table thead tr th'); const columns = page.locator('table thead tr th');
await expect(columns).toHaveCount(count); await expect(columns).toHaveCount(count);
}; };
// Open an "action" associated with the detail view of a page
export const openDetailAction = async (
page: Page,
menuName: string,
actionName: string
) => {
// Ensure idle state first
await page.waitForLoadState('networkidle');
// Click on the "actions" menu
await page.getByRole('button', { name: `action-menu-${menuName}` }).click();
// Click on the specified action
await page
.getByRole('menuitem', {
name: `action-menu-${menuName}-actions-${actionName}`
})
.click();
};
+3 -3
View File
@@ -7,6 +7,7 @@ import {
getRowFromCell, getRowFromCell,
loadTab, loadTab,
navigate, navigate,
openDetailAction,
setTableChoiceFilter, setTableChoiceFilter,
showCalendarView, showCalendarView,
showParametricView, showParametricView,
@@ -40,7 +41,6 @@ test('Build Order - Basic Tests', async ({ browser }) => {
// We have now loaded the "Build Order" table. Check for some expected texts // We have now loaded the "Build Order" table. Check for some expected texts
await page.getByPlaceholder('Search').fill('7'); await page.getByPlaceholder('Search').fill('7');
await page.getByText('On Hold').first().waitFor(); await page.getByText('On Hold').first().waitFor();
await page.getByText('Pending').first().waitFor();
// Load a particular build order // Load a particular build order
await page.getByRole('cell', { name: 'BO0017' }).click(); await page.getByRole('cell', { name: 'BO0017' }).click();
@@ -756,8 +756,8 @@ test('Build Order - Duplicate', async ({ browser }) => {
const page = await doCachedLogin(browser); const page = await doCachedLogin(browser);
await navigate(page, 'manufacturing/build-order/24/details'); await navigate(page, 'manufacturing/build-order/24/details');
await page.getByLabel('action-menu-build-order-').click();
await page.getByLabel('action-menu-build-order-actions-duplicate').click(); await openDetailAction(page, 'build-order', 'duplicate');
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect( await expect(
+3 -3
View File
@@ -5,12 +5,13 @@ import {
clickOnParamFilter, clickOnParamFilter,
loadTab, loadTab,
navigate, navigate,
openDetailAction,
setTableChoiceFilter, setTableChoiceFilter,
showParametricView showParametricView
} from '../helpers.js'; } from '../helpers.js';
import { doCachedLogin } from '../login.js'; import { doCachedLogin } from '../login.js';
test('Company', async ({ browser }) => { test('Company - Basic Tests', async ({ browser }) => {
const page = await doCachedLogin(browser); const page = await doCachedLogin(browser);
await navigate(page, 'company/1/details'); await navigate(page, 'company/1/details');
@@ -42,8 +43,7 @@ test('Company', async ({ browser }) => {
await loadTab(page, 'Notes'); await loadTab(page, 'Notes');
// Let's edit the company details // Let's edit the company details
await page.getByLabel('action-menu-company-actions').click(); await openDetailAction(page, 'company', 'edit');
await page.getByLabel('action-menu-company-actions-edit').click();
await page.getByLabel('text-field-name', { exact: true }).fill(''); await page.getByLabel('text-field-name', { exact: true }).fill('');
await page await page
+7 -5
View File
@@ -9,6 +9,7 @@ import {
getRowFromCell, getRowFromCell,
loadTab, loadTab,
navigate, navigate,
openDetailAction,
setTableChoiceFilter, setTableChoiceFilter,
showParametricView, showParametricView,
showTableView showTableView
@@ -69,6 +70,9 @@ test('Parts - Tabs', async ({ browser }) => {
test('Parts - Image Selection', async ({ browser }) => { test('Parts - Image Selection', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/911/details' }); const page = await doCachedLogin(browser, { url: 'part/911/details' });
await page.waitForLoadState('networkidle');
await page.waitForTimeout(250);
// Select a new image from the available images // Select a new image from the available images
await page await page
.getByRole('tabpanel', { name: 'Part Details' }) .getByRole('tabpanel', { name: 'Part Details' })
@@ -1015,8 +1019,8 @@ test('Parts - Bulk Edit', async ({ browser }) => {
// Edit the category for multiple parts // Edit the category for multiple parts
await page.getByLabel('Select record 1', { exact: true }).click(); await page.getByLabel('Select record 1', { exact: true }).click();
await page.getByLabel('Select record 2', { exact: true }).click(); await page.getByLabel('Select record 2', { exact: true }).click();
await page.getByLabel('action-menu-part-actions').click();
await page.getByLabel('action-menu-part-actions-set-category').click(); await openDetailAction(page, 'part', 'set-category');
await page.getByLabel('related-field-category').fill('rnitu'); await page.getByLabel('related-field-category').fill('rnitu');
await page.waitForTimeout(250); await page.waitForTimeout(250);
@@ -1032,9 +1036,7 @@ test('Parts - Duplicate', async ({ browser }) => {
}); });
// Open "duplicate part" dialog // Open "duplicate part" dialog
await page.getByLabel('action-menu-part-actions').click(); await openDetailAction(page, 'part', 'duplicate');
await page.getByLabel('action-menu-part-actions-duplicate').click();
// Check for expected fields // Check for expected fields
await page.getByText('Copy Image', { exact: true }).waitFor(); await page.getByText('Copy Image', { exact: true }).waitFor();
@@ -9,6 +9,7 @@ import {
clickOnRowMenu, clickOnRowMenu,
loadTab, loadTab,
navigate, navigate,
openDetailAction,
openFilterDrawer, openFilterDrawer,
setTableChoiceFilter, setTableChoiceFilter,
showCalendarView, showCalendarView,
@@ -292,8 +293,7 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
await page.getByRole('button', { name: 'Issue Order' }).waitFor(); await page.getByRole('button', { name: 'Issue Order' }).waitFor();
// Display QR code // Display QR code
await page.getByLabel('action-menu-barcode-actions').click(); await openDetailAction(page, 'barcode', 'view');
await page.getByLabel('action-menu-barcode-actions-view').click();
await page.getByRole('img', { name: 'QR Code' }).waitFor(); await page.getByRole('img', { name: 'QR Code' }).waitFor();
await page.getByRole('banner').getByRole('button').click(); await page.getByRole('banner').getByRole('button').click();
@@ -314,8 +314,7 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
} }
// Link to barcode // Link to barcode
await page.getByLabel('action-menu-barcode-actions', { exact: true }).click(); await openDetailAction(page, 'barcode', 'link-barcode');
await page.getByLabel('action-menu-barcode-actions-link-barcode').click();
await page.getByLabel('barcode-input-scanner').click(); await page.getByLabel('barcode-input-scanner').click();
@@ -338,8 +337,7 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
await page.getByText('Purchase Order: PO0013', { exact: true }).waitFor(); await page.getByText('Purchase Order: PO0013', { exact: true }).waitFor();
// Unlink barcode // Unlink barcode
await page.getByLabel('action-menu-barcode-actions').click(); await openDetailAction(page, 'barcode', 'unlink-barcode');
await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click();
await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor(); await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor();
await page.getByText('This will remove the link to').waitFor(); await page.getByText('This will remove the link to').waitFor();
await page.getByRole('button', { name: 'Unlink Barcode' }).click(); await page.getByRole('button', { name: 'Unlink Barcode' }).click();
@@ -431,8 +429,8 @@ test('Purchase Orders - Order Parts', async ({ browser }) => {
await page.getByLabel(`Select record ${ii}`, { exact: true }).click(); await page.getByLabel(`Select record ${ii}`, { exact: true }).click();
} }
await page.getByLabel('action-menu-part-actions').click(); await openDetailAction(page, 'part', 'order-parts');
await page.getByLabel('action-menu-part-actions-order-parts').click();
await page await page
.getByRole('heading', { name: 'Order Parts' }) .getByRole('heading', { name: 'Order Parts' })
.locator('div') .locator('div')
@@ -456,8 +454,7 @@ test('Purchase Orders - Order Parts', async ({ browser }) => {
await navigate(page, 'part/69/'); await navigate(page, 'part/69/');
await page.waitForURL('**/part/69/**'); await page.waitForURL('**/part/69/**');
await page.getByLabel('action-menu-stock-actions').click(); await openDetailAction(page, 'stock', 'order');
await page.getByLabel('action-menu-stock-actions-order').click();
// Select supplier part // Select supplier part
await page.getByLabel('related-field-supplier_part').click(); await page.getByLabel('related-field-supplier_part').click();
@@ -630,8 +627,7 @@ test('Purchase Orders - Duplicate', async ({ browser }) => {
url: 'purchasing/purchase-order/13/detail' url: 'purchasing/purchase-order/13/detail'
}); });
await page.getByLabel('action-menu-order-actions').click(); await openDetailAction(page, 'order', 'duplicate');
await page.getByLabel('action-menu-order-actions-duplicate').click();
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect( await expect(
+3 -3
View File
@@ -625,9 +625,9 @@ test('Transfer Order - Reference', async ({ browser }) => {
.click(); .click();
// Ensure a new reference is suggested // Ensure a new reference is suggested
await expect( await page.waitForLoadState('networkidle');
page.getByLabel('text-field-reference', { exact: true }) await page.waitForTimeout(250);
).not.toBeEmpty();
// Grab the Transfer Order reference // Grab the Transfer Order reference
const reference: string = await page const reference: string = await page
.getByRole('textbox', { name: 'text-field-reference' }) .getByRole('textbox', { name: 'text-field-reference' })
+2 -3
View File
@@ -2,7 +2,7 @@ import { createApi } from './api';
/** Unit tests for form validation, rendering, etc */ /** Unit tests for form validation, rendering, etc */
import { expect, test } from './baseFixtures'; import { expect, test } from './baseFixtures';
import { stevenuser } from './defaults'; import { stevenuser } from './defaults';
import { navigate } from './helpers'; import { navigate, openDetailAction } from './helpers';
import { doCachedLogin } from './login'; import { doCachedLogin } from './login';
// Test hover form action in related fields // Test hover form action in related fields
@@ -84,8 +84,7 @@ test('Forms - Stock Item Validation', async ({ browser }) => {
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
// Edit the resulting stock item // Edit the resulting stock item
await page.getByLabel('action-menu-stock-item-actions').click(); await openDetailAction(page, 'stock-item', 'edit');
await page.getByLabel('action-menu-stock-item-actions-edit').click();
await page.getByLabel('number-field-purchase_price').fill('-1'); await page.getByLabel('number-field-purchase_price').fill('-1');
+3 -3
View File
@@ -1,6 +1,6 @@
import { expect, test } from './baseFixtures.js'; import { expect, test } from './baseFixtures.js';
import { logoutUrl, noaccessuser } from './defaults.js'; import { logoutUrl, noaccessuser } from './defaults.js';
import { navigate } from './helpers.js'; import { navigate, openDetailAction } from './helpers.js';
import { doLogin } from './login.js'; import { doLogin } from './login.js';
import { TOTP } from 'otpauth'; import { TOTP } from 'otpauth';
@@ -54,8 +54,8 @@ test('Login - Change Password', async ({ page }) => {
// Navigate to the 'change password' page // Navigate to the 'change password' page
await navigate(page, 'settings/user/account', { waitUntil: 'networkidle' }); await navigate(page, 'settings/user/account', { waitUntil: 'networkidle' });
await page.getByLabel('action-menu-account-actions').click();
await page.getByLabel('action-menu-account-actions-change-password').click(); await openDetailAction(page, 'account', 'change-password');
// First attempt with some errors // First attempt with some errors
await page.getByLabel('password', { exact: true }).fill('youshallnotpass'); await page.getByLabel('password', { exact: true }).fill('youshallnotpass');
+39 -1
View File
@@ -9,6 +9,41 @@ import {
} from './helpers.js'; } from './helpers.js';
import { doCachedLogin } from './login.js'; import { doCachedLogin } from './login.js';
// Test filtering by "quick filter" actions (against table columns)
test('Tables - Quick Filters', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'part/category/index/parts/'
});
await clearTableFilters(page);
await page
.getByRole('button', { name: 'Part Not sorted' })
.getByRole('button')
.first()
.click();
await page.getByRole('combobox', { name: 'choice-filter-active' }).click();
await page.getByRole('option', { name: 'Yes' }).click();
await page
.getByRole('button', { name: 'Part Not sorted' })
.getByRole('button')
.first()
.click();
await page.getByRole('combobox', { name: 'choice-filter-locked' }).click();
await page.getByRole('option', { name: 'No' }).click();
await page
.getByRole('button', { name: 'IPN Not sorted' })
.getByRole('button')
.first()
.click();
await page.getByRole('combobox', { name: 'choice-filter-has_ipn' }).click();
await page.getByRole('option', { name: 'Yes' }).click();
await page.getByRole('cell', { name: 'ENCAB' }).first().waitFor();
});
test('Tables - Filters', async ({ browser }) => { test('Tables - Filters', async ({ browser }) => {
// Head to the "build order list" page // Head to the "build order list" page
const page = await doCachedLogin(browser, { url: 'manufacturing/index/' }); const page = await doCachedLogin(browser, { url: 'manufacturing/index/' });
@@ -46,7 +81,10 @@ test('Tables - Filters', async ({ browser }) => {
await page.getByRole('button', { name: 'Add Filter' }).click(); await page.getByRole('button', { name: 'Add Filter' }).click();
await page.getByRole('combobox', { name: 'Filter' }).click(); await page.getByRole('combobox', { name: 'Filter' }).click();
await page.getByRole('option', { name: 'Outstanding' }).click(); await page.getByRole('option', { name: 'Outstanding' }).click();
await page.getByRole('combobox', { name: 'Value' }).click();
await page
.getByRole('combobox', { name: 'choice-filter-outstanding' })
.click();
await page.getByRole('option', { name: 'Yes' }).click(); await page.getByRole('option', { name: 'Yes' }).click();
// Save the filter group // Save the filter group