mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-12 03:28:37 +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:
@@ -23,7 +23,7 @@ export function TableColumnSelect({
|
||||
<Menu.Label>{t`Select Columns`}</Menu.Label>
|
||||
<Divider />
|
||||
{columns
|
||||
.filter((col) => col.switchable ?? true)
|
||||
.filter((col) => (col.switchable ?? true) && !col.propHidden)
|
||||
.map((col) => (
|
||||
<Menu.Item key={col.accessor}>
|
||||
<Checkbox
|
||||
|
||||
@@ -84,7 +84,7 @@ export type TableState = {
|
||||
* @param editable - Whether the value of this column can be edited
|
||||
* @param definition - Optional field definition for the column
|
||||
* @param render - A custom render function
|
||||
* @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 width - The width of the column
|
||||
* @param minWidth - The minimum width of the column
|
||||
@@ -109,7 +109,10 @@ export type TableColumnProps<T = any> = {
|
||||
editable?: boolean;
|
||||
definition?: ApiFormFieldType;
|
||||
render?: (record: T, index?: number) => any;
|
||||
filter?: any;
|
||||
filter?:
|
||||
| string
|
||||
| string[]
|
||||
| (({ close }: { close: () => void }) => ReactNode);
|
||||
filtering?: boolean;
|
||||
width?: number;
|
||||
minWidth?: string | number;
|
||||
|
||||
@@ -117,6 +117,7 @@ export function IPNColumn(props: TableColumnProps): TableColumn {
|
||||
switchable: true,
|
||||
title: t`IPN`,
|
||||
copyable: true,
|
||||
filter: 'has_ipn',
|
||||
...props
|
||||
};
|
||||
}
|
||||
@@ -433,6 +434,7 @@ export function BooleanColumn(props: TableColumn): TableColumn {
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
minWidth: '75px',
|
||||
filter: props.filter ?? props.accessor,
|
||||
render: (record: any) => (
|
||||
<Center>
|
||||
<YesNoButton value={resolveItem(record, props.accessor ?? '')} />
|
||||
@@ -564,6 +566,7 @@ export function ProjectCodeColumn(props: TableColumnProps): TableColumn {
|
||||
sortable: true,
|
||||
title: t`Project Code`,
|
||||
hidden: !enabled,
|
||||
filter: 'project_code',
|
||||
render: (record: any) => {
|
||||
const project_code = resolveItem(
|
||||
record,
|
||||
@@ -584,6 +587,7 @@ export function StatusColumn(props: StatusColumnProps): TableColumn {
|
||||
|
||||
return {
|
||||
accessor: 'status',
|
||||
filter: 'status',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
minWidth: '50px',
|
||||
@@ -636,6 +640,7 @@ export function CreatedByColumn(props: TableColumnProps): TableColumn {
|
||||
accessor: 'created_by',
|
||||
ordering: 'created_by',
|
||||
title: t`Created By`,
|
||||
filter: 'created_by',
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -665,6 +670,7 @@ export function ResponsibleColumn(props: TableColumnProps): TableColumn {
|
||||
accessor: 'responsible_detail',
|
||||
ordering: 'responsible',
|
||||
title: t`Responsible`,
|
||||
filter: 'assigned_to',
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -688,6 +694,7 @@ export function StartDateColumn(props: TableColumnProps): TableColumn {
|
||||
return DateColumn({
|
||||
accessor: 'start_date',
|
||||
title: t`Start Date`,
|
||||
filter: ['has_start_date', 'start_date_before', 'start_date_after'],
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -696,6 +703,7 @@ export function TargetDateColumn(props: TableColumnProps): TableColumn {
|
||||
return DateColumn({
|
||||
accessor: 'target_date',
|
||||
title: t`Target Date`,
|
||||
filter: ['has_target_date', 'target_date_before', 'target_date_after'],
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -704,6 +712,7 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn {
|
||||
return DateColumn({
|
||||
accessor: 'creation_date',
|
||||
title: t`Creation Date`,
|
||||
filter: ['created_before', 'created_after'],
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -712,6 +721,7 @@ export function CompletionDateColumn(props: TableColumnProps): TableColumn {
|
||||
return DateColumn({
|
||||
accessor: 'completion_date',
|
||||
title: t`Completion Date`,
|
||||
filter: ['completed_before', 'completed_after'],
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -720,6 +730,7 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn {
|
||||
return DateColumn({
|
||||
accessor: 'shipment_date',
|
||||
title: t`Shipment Date`,
|
||||
filter: ['shipment_date_before', 'shipment_date_after'],
|
||||
...props
|
||||
});
|
||||
}
|
||||
@@ -729,6 +740,7 @@ export function UpdatedAtColumn(props: TableColumnProps): TableColumn {
|
||||
accessor: 'updated_at',
|
||||
title: t`Updated`,
|
||||
defaultVisible: false,
|
||||
filter: ['updated_before', 'updated_after'],
|
||||
extra: { showTime: true },
|
||||
...props
|
||||
});
|
||||
|
||||
@@ -182,15 +182,17 @@ function MultiApiFilterElement({
|
||||
);
|
||||
}
|
||||
|
||||
function FilterElement({
|
||||
export function FilterElement({
|
||||
filterName,
|
||||
filterProps,
|
||||
valueOptions,
|
||||
brief = false,
|
||||
onValueChange
|
||||
}: {
|
||||
filterName: string;
|
||||
filterProps: TableFilter;
|
||||
valueOptions: TableFilterChoice[];
|
||||
brief?: boolean;
|
||||
onValueChange: (value: string | null, displayValue?: any) => void;
|
||||
}) {
|
||||
const setDateValue = useCallback(
|
||||
@@ -226,7 +228,7 @@ function FilterElement({
|
||||
filters: filterProps.apiFilter,
|
||||
placeholder: t`Select filter value`,
|
||||
model: filterProps.model,
|
||||
label: t`Select filter value`,
|
||||
label: brief ? undefined : t`Select filter value`,
|
||||
onValueChange: (value: any, instance: any) => {
|
||||
if (filterProps.transform) {
|
||||
const choice = filterProps.transform(instance);
|
||||
@@ -244,9 +246,10 @@ function FilterElement({
|
||||
case 'text':
|
||||
return (
|
||||
<TextInput
|
||||
label={t`Value`}
|
||||
label={brief ? undefined : t`Value`}
|
||||
value={textValue}
|
||||
placeholder={t`Enter filter value`}
|
||||
aria-label={`text-filter-${filterName}`}
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
aria-label='apply-text-filter'
|
||||
@@ -256,8 +259,10 @@ function FilterElement({
|
||||
<IconCheck />
|
||||
</ActionIcon>
|
||||
}
|
||||
onChange={(e) => setTextValue(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setTextValue(e.currentTarget.value)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onValueChange(textValue);
|
||||
}
|
||||
@@ -267,9 +272,11 @@ function FilterElement({
|
||||
case 'date':
|
||||
return (
|
||||
<DateInput
|
||||
label={t`Value`}
|
||||
aria-label={`date-filter-${filterName}`}
|
||||
label={brief ? undefined : t`Value`}
|
||||
placeholder={t`Select date value`}
|
||||
onChange={setDateValue}
|
||||
popoverProps={{ withinPortal: false }}
|
||||
/>
|
||||
);
|
||||
case 'choice':
|
||||
@@ -278,8 +285,9 @@ function FilterElement({
|
||||
return (
|
||||
<Select
|
||||
data={valueOptions}
|
||||
aria-label={`choice-filter-${filterName}`}
|
||||
searchable={filterProps.type == 'choice'}
|
||||
label={t`Value`}
|
||||
label={brief ? undefined : t`Value`}
|
||||
withScrollArea={false}
|
||||
placeholder={t`Select filter 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({
|
||||
filterSet,
|
||||
availableFilters
|
||||
@@ -331,19 +346,6 @@ function FilterAddGroup({
|
||||
return getTableFilterOptions(filter);
|
||||
}, [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
|
||||
const filterProps: TableFilter | undefined = useMemo(() => {
|
||||
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({
|
||||
title,
|
||||
availableFilters,
|
||||
@@ -584,8 +707,10 @@ export function FilterSelectDrawer({
|
||||
aria-label='filter-group-name'
|
||||
placeholder={t`Group name`}
|
||||
value={saveName}
|
||||
onChange={(e) => setSaveName(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setSaveName(e.currentTarget.value)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') confirmSave();
|
||||
if (e.key === 'Escape') setSaving(false);
|
||||
}}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { extractAvailableFields } from '../functions/forms';
|
||||
import { showApiErrorMessage } from '../functions/notifications';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { useUserSettingsState } from '../states/SettingsStates';
|
||||
import { ColumnFilterPopover } from './FilterSelectDrawer';
|
||||
import InvenTreeTableHeader from './InvenTreeTableHeader';
|
||||
|
||||
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).
|
||||
// For switchable columns, visibility is driven by tableState.hiddenColumns.
|
||||
// Non-switchable columns are always visible (unless hidden by props).
|
||||
const hidden: boolean =
|
||||
col.hidden === true
|
||||
? true
|
||||
: col.switchable == false
|
||||
? false
|
||||
: (tableState.hiddenColumns?.includes(col.accessor) ?? false);
|
||||
const propHidden: boolean = col.hidden === true;
|
||||
const hidden: boolean = propHidden
|
||||
? true
|
||||
: col.switchable == false
|
||||
? false
|
||||
: (tableState.hiddenColumns?.includes(col.accessor) ?? false);
|
||||
|
||||
// Wrap the render function with CopyableCell if copyable is enabled
|
||||
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 {
|
||||
...col,
|
||||
hidden: hidden,
|
||||
resizable: col.resizable ?? true,
|
||||
title: col.title ?? fieldNames[col.accessor] ?? `${col.accessor}`,
|
||||
render: wrappedRender,
|
||||
filter: resolvedFilter,
|
||||
filtering: namedFilters.length > 0 ? namedFiltersActive : col.filtering,
|
||||
propHidden: propHidden,
|
||||
cellsStyle: (record: any, index: number) => {
|
||||
const width = (col as any).minWidth ?? 100;
|
||||
return {
|
||||
@@ -347,10 +391,12 @@ export function InvenTreeTableInternal<T extends Record<string, any>>({
|
||||
return cols;
|
||||
}, [
|
||||
columns,
|
||||
filters,
|
||||
fieldNames,
|
||||
tableProps.rowActions,
|
||||
tableState.hiddenColumns,
|
||||
tableState.selectedRecords
|
||||
tableState.selectedRecords,
|
||||
tableState.filterSet.activeFilters
|
||||
]);
|
||||
|
||||
// Callback when column visibility is toggled
|
||||
|
||||
@@ -171,11 +171,6 @@ export function BomTable({
|
||||
DescriptionColumn({
|
||||
accessor: 'sub_part_detail.description'
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'sub_part_detail.virtual',
|
||||
defaultVisible: false,
|
||||
title: t`Virtual Part`
|
||||
}),
|
||||
ReferenceColumn({
|
||||
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({
|
||||
accessor: 'optional',
|
||||
defaultVisible: false
|
||||
|
||||
@@ -44,7 +44,8 @@ export function UsedInTable({
|
||||
return [
|
||||
PartColumn({
|
||||
title: t`Assembly`,
|
||||
part: 'part_detail'
|
||||
part: 'part_detail',
|
||||
filter: ['part_active', 'part_locked']
|
||||
}),
|
||||
IPNColumn({
|
||||
sortable: true
|
||||
|
||||
@@ -322,6 +322,7 @@ export default function BuildLineTable({
|
||||
ordering: 'part',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
filter: ['assembly', 'testable', 'tracked'],
|
||||
render: (record: any) => {
|
||||
const hasAllocatedItems = record.allocatedQuantity > 0;
|
||||
|
||||
@@ -356,12 +357,14 @@ export default function BuildLineTable({
|
||||
BooleanColumn({
|
||||
accessor: 'bom_item_detail.optional',
|
||||
ordering: 'optional',
|
||||
filter: 'optional',
|
||||
hidden: hasOutput,
|
||||
defaultVisible: false
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'bom_item_detail.consumable',
|
||||
ordering: 'consumable',
|
||||
filter: 'consumable',
|
||||
hidden: hasOutput,
|
||||
defaultVisible: false
|
||||
}),
|
||||
@@ -381,6 +384,7 @@ export default function BuildLineTable({
|
||||
BooleanColumn({
|
||||
accessor: 'part_detail.trackable',
|
||||
ordering: 'trackable',
|
||||
filter: 'tracked',
|
||||
hidden: hasOutput,
|
||||
defaultVisible: false
|
||||
}),
|
||||
@@ -457,6 +461,7 @@ export default function BuildLineTable({
|
||||
{
|
||||
accessor: 'available_stock',
|
||||
sortable: true,
|
||||
filter: 'available',
|
||||
switchable: false,
|
||||
render: renderAvailableColumn
|
||||
},
|
||||
@@ -481,12 +486,14 @@ export default function BuildLineTable({
|
||||
DecimalColumn({
|
||||
accessor: 'on_order',
|
||||
defaultVisible: false,
|
||||
filter: 'on_order',
|
||||
sortable: true
|
||||
}),
|
||||
{
|
||||
accessor: 'allocated',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
filter: 'allocated',
|
||||
hidden: !isActive,
|
||||
minWidth: 125,
|
||||
render: (record: any) => {
|
||||
@@ -532,6 +539,7 @@ export default function BuildLineTable({
|
||||
{
|
||||
accessor: 'consumed',
|
||||
sortable: true,
|
||||
filter: 'consumed',
|
||||
hidden: !!output?.pk,
|
||||
minWidth: 125,
|
||||
render: (record: any) => {
|
||||
|
||||
@@ -15,8 +15,8 @@ import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
BooleanColumn,
|
||||
CompletionDateColumn,
|
||||
CreationDateColumn,
|
||||
DateColumn,
|
||||
DescriptionColumn,
|
||||
IPNColumn,
|
||||
LinkColumn,
|
||||
@@ -104,6 +104,7 @@ export function BuildOrderTable({
|
||||
BooleanColumn({
|
||||
accessor: 'external',
|
||||
title: t`External`,
|
||||
filter: 'external',
|
||||
sortable: true,
|
||||
switchable: true,
|
||||
hidden: !globalSettings.isSet('BUILDORDER_EXTERNAL_BUILDS')
|
||||
@@ -115,11 +116,7 @@ export function BuildOrderTable({
|
||||
defaultVisible: false
|
||||
}),
|
||||
TargetDateColumn({}),
|
||||
DateColumn({
|
||||
accessor: 'completion_date',
|
||||
title: t`Completion Date`,
|
||||
sortable: true
|
||||
}),
|
||||
CompletionDateColumn({}),
|
||||
UserColumn({
|
||||
accessor: 'issued_by_detail',
|
||||
ordering: 'issued_by',
|
||||
|
||||
@@ -64,6 +64,7 @@ export function CompanyTable({
|
||||
DescriptionColumn({}),
|
||||
BooleanColumn({
|
||||
accessor: 'active',
|
||||
filter: 'active',
|
||||
title: t`Active`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
|
||||
@@ -54,7 +54,8 @@ export function ParameterTable({
|
||||
accessor: 'template_detail.name',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
ordering: 'name'
|
||||
ordering: 'name',
|
||||
filter: 'enabled'
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'template_detail.description'
|
||||
@@ -105,6 +106,7 @@ export function ParameterTable({
|
||||
UserColumn({
|
||||
accessor: 'updated_by_detail',
|
||||
ordering: 'updated_by',
|
||||
filter: 'updated_by',
|
||||
title: t`Updated By`
|
||||
})
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import { type RowAction, RowEditAction } from '@lib/components/RowActions';
|
||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { DescriptionColumn } from '../ColumnRenderers';
|
||||
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
/**
|
||||
@@ -64,14 +63,11 @@ export function PartCategoryTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
copyable: true,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
BooleanColumn({
|
||||
accessor: 'structural',
|
||||
sortable: true,
|
||||
defaultVisible: false,
|
||||
render: (record: any) => {
|
||||
return <YesNoButton value={record.structural} />;
|
||||
}
|
||||
},
|
||||
defaultVisible: false
|
||||
}),
|
||||
{
|
||||
accessor: 'part_count',
|
||||
sortable: true
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function PartPurchaseOrdersTable({
|
||||
ordering: 'order',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
filter: ['pending', 'received'],
|
||||
title: t`Purchase Order`
|
||||
}),
|
||||
StatusColumn({
|
||||
@@ -36,6 +37,7 @@ export default function PartPurchaseOrdersTable({
|
||||
sortable: true,
|
||||
ordering: 'status',
|
||||
title: t`Status`,
|
||||
filter: 'order_status',
|
||||
model: ModelType.purchaseorder
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useImporterState } from '../../states/ImporterState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
BooleanColumn,
|
||||
CategoryColumn,
|
||||
DefaultLocationColumn,
|
||||
DescriptionColumn,
|
||||
@@ -55,7 +56,8 @@ function partTableColumns(): TableColumn[] {
|
||||
return [
|
||||
PartColumn({
|
||||
part: '',
|
||||
accessor: 'name'
|
||||
accessor: 'name',
|
||||
filter: ['active', 'locked', 'starred']
|
||||
}),
|
||||
IPNColumn({
|
||||
accessor: 'IPN'
|
||||
@@ -67,7 +69,8 @@ function partTableColumns(): TableColumn[] {
|
||||
{
|
||||
accessor: 'units',
|
||||
sortable: true,
|
||||
copyable: true
|
||||
copyable: true,
|
||||
filter: 'has_units'
|
||||
},
|
||||
DescriptionColumn({}),
|
||||
CategoryColumn({
|
||||
@@ -79,7 +82,7 @@ function partTableColumns(): TableColumn[] {
|
||||
{
|
||||
accessor: 'total_in_stock',
|
||||
sortable: true,
|
||||
|
||||
filter: ['has_stock', 'low_stock', 'high_stock'],
|
||||
render: (record) => {
|
||||
if (record.virtual) {
|
||||
return (
|
||||
@@ -197,10 +200,19 @@ function partTableColumns(): TableColumn[] {
|
||||
title: t`Price Range`,
|
||||
sortable: true,
|
||||
ordering: 'pricing_max',
|
||||
filter: 'has_pricing',
|
||||
defaultVisible: false,
|
||||
render: (record: any) =>
|
||||
formatPriceRange(record.pricing_min, record.pricing_max)
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'assembly',
|
||||
defaultVisible: false
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'virtual',
|
||||
defaultVisible: false
|
||||
}),
|
||||
LinkColumn({})
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { useTestResultFields } from '../../forms/StockForms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { LocationColumn, PartColumn } from '../ColumnRenderers';
|
||||
import { LocationColumn, PartColumn, StatusColumn } from '../ColumnRenderers';
|
||||
import {
|
||||
BatchFilter,
|
||||
HasBatchCodeFilter,
|
||||
@@ -256,6 +256,7 @@ export default function PartTestResultTable({
|
||||
title: t`Stock Item`,
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
filter: ['in_stock', 'is_building'],
|
||||
render: (record: any) => {
|
||||
if (record.serial) {
|
||||
return `# ${record.serial}`;
|
||||
@@ -284,10 +285,15 @@ export default function PartTestResultTable({
|
||||
}
|
||||
}
|
||||
},
|
||||
StatusColumn({
|
||||
accessor: 'status',
|
||||
model: ModelType.stockitem
|
||||
}),
|
||||
{
|
||||
accessor: 'batch',
|
||||
title: t`Batch Code`,
|
||||
sortable: true,
|
||||
filter: ['batch', 'has_batch_code'],
|
||||
switchable: true,
|
||||
copyable: true
|
||||
},
|
||||
|
||||
@@ -86,12 +86,14 @@ export function ManufacturerPartTable({
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
switchable: !!partId
|
||||
switchable: !!partId,
|
||||
filter: 'part_active'
|
||||
}),
|
||||
IPNColumn({}),
|
||||
{
|
||||
accessor: 'manufacturer',
|
||||
sortable: true,
|
||||
filter: 'manufacturer_active',
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record?.manufacturer_detail} />
|
||||
)
|
||||
|
||||
@@ -93,11 +93,13 @@ export function SupplierPartTable({
|
||||
return [
|
||||
PartColumn({
|
||||
switchable: !!partId,
|
||||
part: 'part_detail'
|
||||
part: 'part_detail',
|
||||
filter: ['part_active']
|
||||
}),
|
||||
IPNColumn({}),
|
||||
{
|
||||
accessor: 'supplier',
|
||||
filter: 'supplier_active',
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record?.supplier_detail} />
|
||||
@@ -179,6 +181,7 @@ export function SupplierPartTable({
|
||||
accessor: 'available',
|
||||
sortable: true,
|
||||
defaultVisible: false,
|
||||
filter: 'has_stock',
|
||||
render: (record: any) => {
|
||||
const extra = [];
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ export default function SalesOrderShipmentTable({
|
||||
{
|
||||
accessor: 'checked',
|
||||
title: t`Checked`,
|
||||
filter: 'checked',
|
||||
switchable: true,
|
||||
sortable: false,
|
||||
render: (record: any) => <YesNoButton value={!!record.checked_by} />
|
||||
@@ -168,12 +169,14 @@ export default function SalesOrderShipmentTable({
|
||||
title: t`Shipped`,
|
||||
switchable: true,
|
||||
sortable: false,
|
||||
filter: 'shipped',
|
||||
render: (record: any) => <YesNoButton value={!!record.shipment_date} />
|
||||
},
|
||||
{
|
||||
accessor: 'delivered',
|
||||
title: t`Delivered`,
|
||||
switchable: true,
|
||||
filter: 'delivered',
|
||||
sortable: false,
|
||||
render: (record: any) => <YesNoButton value={!!record.delivery_date} />
|
||||
},
|
||||
|
||||
@@ -63,7 +63,8 @@ function stockItemTableColumns({
|
||||
return [
|
||||
PartColumn({
|
||||
accessor: 'part',
|
||||
part: 'part_detail'
|
||||
part: 'part_detail',
|
||||
filter: ['active']
|
||||
}),
|
||||
IPNColumn({}),
|
||||
{
|
||||
@@ -79,13 +80,21 @@ function stockItemTableColumns({
|
||||
accessor: '',
|
||||
title: t`Stock`,
|
||||
sortable: true,
|
||||
ordering: 'stock'
|
||||
ordering: 'stock',
|
||||
filter: [
|
||||
'available',
|
||||
'allocated',
|
||||
'consumed',
|
||||
'installed',
|
||||
'sent_to_customer'
|
||||
]
|
||||
}),
|
||||
StatusColumn({ model: ModelType.stockitem }),
|
||||
{
|
||||
accessor: 'batch',
|
||||
sortable: true,
|
||||
copyable: true
|
||||
copyable: true,
|
||||
filter: ['has_batch_code', 'batch']
|
||||
},
|
||||
LocationColumn({
|
||||
hidden: !showLocation,
|
||||
@@ -151,22 +160,26 @@ function stockItemTableColumns({
|
||||
DateColumn({
|
||||
title: t`Created`,
|
||||
accessor: 'creation_date',
|
||||
sortable: true
|
||||
sortable: true,
|
||||
filter: ['created_before', 'created_after']
|
||||
}),
|
||||
DateColumn({
|
||||
title: t`Last Updated`,
|
||||
accessor: 'updated'
|
||||
accessor: 'updated',
|
||||
filter: ['updated_before', 'updated_after']
|
||||
}),
|
||||
DateColumn({
|
||||
title: t`Expiry Date`,
|
||||
accessor: 'expiry_date',
|
||||
hidden: !useGlobalSettingsState.getState().isSet('STOCK_ENABLE_EXPIRY'),
|
||||
defaultVisible: false
|
||||
defaultVisible: false,
|
||||
filter: ['stale', 'expiry_before', 'expiry_after']
|
||||
}),
|
||||
DateColumn({
|
||||
accessor: '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`,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
filter: ['enabled', 'required'],
|
||||
render: (record: any) => {
|
||||
const enabled = record.enabled ?? record.template_detail?.enabled;
|
||||
const installed =
|
||||
|
||||
@@ -97,6 +97,7 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
{
|
||||
accessor: 'location_type',
|
||||
sortable: false,
|
||||
filter: ['has_location_type', 'location_type'],
|
||||
render: (record: any) => record.location_type_detail?.name
|
||||
}
|
||||
];
|
||||
|
||||
@@ -210,3 +210,23 @@ export const expectTableColumnCount = async (page: Page, count: number) => {
|
||||
const columns = page.locator('table thead tr th');
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getRowFromCell,
|
||||
loadTab,
|
||||
navigate,
|
||||
openDetailAction,
|
||||
setTableChoiceFilter,
|
||||
showCalendarView,
|
||||
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
|
||||
await page.getByPlaceholder('Search').fill('7');
|
||||
await page.getByText('On Hold').first().waitFor();
|
||||
await page.getByText('Pending').first().waitFor();
|
||||
|
||||
// Load a particular build order
|
||||
await page.getByRole('cell', { name: 'BO0017' }).click();
|
||||
@@ -756,8 +756,8 @@ test('Build Order - Duplicate', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
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
|
||||
await expect(
|
||||
|
||||
@@ -5,12 +5,13 @@ import {
|
||||
clickOnParamFilter,
|
||||
loadTab,
|
||||
navigate,
|
||||
openDetailAction,
|
||||
setTableChoiceFilter,
|
||||
showParametricView
|
||||
} from '../helpers.js';
|
||||
import { doCachedLogin } from '../login.js';
|
||||
|
||||
test('Company', async ({ browser }) => {
|
||||
test('Company - Basic Tests', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await navigate(page, 'company/1/details');
|
||||
@@ -42,8 +43,7 @@ test('Company', async ({ browser }) => {
|
||||
await loadTab(page, 'Notes');
|
||||
|
||||
// Let's edit the company details
|
||||
await page.getByLabel('action-menu-company-actions').click();
|
||||
await page.getByLabel('action-menu-company-actions-edit').click();
|
||||
await openDetailAction(page, 'company', 'edit');
|
||||
|
||||
await page.getByLabel('text-field-name', { exact: true }).fill('');
|
||||
await page
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getRowFromCell,
|
||||
loadTab,
|
||||
navigate,
|
||||
openDetailAction,
|
||||
setTableChoiceFilter,
|
||||
showParametricView,
|
||||
showTableView
|
||||
@@ -69,6 +70,9 @@ test('Parts - Tabs', async ({ browser }) => {
|
||||
test('Parts - Image Selection', async ({ browser }) => {
|
||||
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
|
||||
await page
|
||||
.getByRole('tabpanel', { name: 'Part Details' })
|
||||
@@ -1015,8 +1019,8 @@ test('Parts - Bulk Edit', async ({ browser }) => {
|
||||
// Edit the category for multiple parts
|
||||
await page.getByLabel('Select record 1', { 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.waitForTimeout(250);
|
||||
@@ -1032,9 +1036,7 @@ test('Parts - Duplicate', async ({ browser }) => {
|
||||
});
|
||||
|
||||
// Open "duplicate part" dialog
|
||||
await page.getByLabel('action-menu-part-actions').click();
|
||||
|
||||
await page.getByLabel('action-menu-part-actions-duplicate').click();
|
||||
await openDetailAction(page, 'part', 'duplicate');
|
||||
|
||||
// Check for expected fields
|
||||
await page.getByText('Copy Image', { exact: true }).waitFor();
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
clickOnRowMenu,
|
||||
loadTab,
|
||||
navigate,
|
||||
openDetailAction,
|
||||
openFilterDrawer,
|
||||
setTableChoiceFilter,
|
||||
showCalendarView,
|
||||
@@ -292,8 +293,7 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||
|
||||
// Display QR code
|
||||
await page.getByLabel('action-menu-barcode-actions').click();
|
||||
await page.getByLabel('action-menu-barcode-actions-view').click();
|
||||
await openDetailAction(page, 'barcode', 'view');
|
||||
await page.getByRole('img', { name: 'QR Code' }).waitFor();
|
||||
await page.getByRole('banner').getByRole('button').click();
|
||||
|
||||
@@ -314,8 +314,7 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
|
||||
}
|
||||
|
||||
// Link to barcode
|
||||
await page.getByLabel('action-menu-barcode-actions', { exact: true }).click();
|
||||
await page.getByLabel('action-menu-barcode-actions-link-barcode').click();
|
||||
await openDetailAction(page, 'barcode', 'link-barcode');
|
||||
|
||||
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();
|
||||
|
||||
// Unlink barcode
|
||||
await page.getByLabel('action-menu-barcode-actions').click();
|
||||
await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click();
|
||||
await openDetailAction(page, 'barcode', 'unlink-barcode');
|
||||
await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor();
|
||||
await page.getByText('This will remove the link to').waitFor();
|
||||
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('action-menu-part-actions').click();
|
||||
await page.getByLabel('action-menu-part-actions-order-parts').click();
|
||||
await openDetailAction(page, 'part', 'order-parts');
|
||||
|
||||
await page
|
||||
.getByRole('heading', { name: 'Order Parts' })
|
||||
.locator('div')
|
||||
@@ -456,8 +454,7 @@ test('Purchase Orders - Order Parts', async ({ browser }) => {
|
||||
await navigate(page, 'part/69/');
|
||||
await page.waitForURL('**/part/69/**');
|
||||
|
||||
await page.getByLabel('action-menu-stock-actions').click();
|
||||
await page.getByLabel('action-menu-stock-actions-order').click();
|
||||
await openDetailAction(page, 'stock', 'order');
|
||||
|
||||
// Select supplier part
|
||||
await page.getByLabel('related-field-supplier_part').click();
|
||||
@@ -630,8 +627,7 @@ test('Purchase Orders - Duplicate', async ({ browser }) => {
|
||||
url: 'purchasing/purchase-order/13/detail'
|
||||
});
|
||||
|
||||
await page.getByLabel('action-menu-order-actions').click();
|
||||
await page.getByLabel('action-menu-order-actions-duplicate').click();
|
||||
await openDetailAction(page, 'order', 'duplicate');
|
||||
|
||||
// Ensure a new reference is suggested
|
||||
await expect(
|
||||
|
||||
@@ -625,9 +625,9 @@ test('Transfer Order - Reference', async ({ browser }) => {
|
||||
.click();
|
||||
|
||||
// Ensure a new reference is suggested
|
||||
await expect(
|
||||
page.getByLabel('text-field-reference', { exact: true })
|
||||
).not.toBeEmpty();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(250);
|
||||
|
||||
// Grab the Transfer Order reference
|
||||
const reference: string = await page
|
||||
.getByRole('textbox', { name: 'text-field-reference' })
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createApi } from './api';
|
||||
/** Unit tests for form validation, rendering, etc */
|
||||
import { expect, test } from './baseFixtures';
|
||||
import { stevenuser } from './defaults';
|
||||
import { navigate } from './helpers';
|
||||
import { navigate, openDetailAction } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
// 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();
|
||||
|
||||
// Edit the resulting stock item
|
||||
await page.getByLabel('action-menu-stock-item-actions').click();
|
||||
await page.getByLabel('action-menu-stock-item-actions-edit').click();
|
||||
await openDetailAction(page, 'stock-item', 'edit');
|
||||
|
||||
await page.getByLabel('number-field-purchase_price').fill('-1');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { logoutUrl, noaccessuser } from './defaults.js';
|
||||
import { navigate } from './helpers.js';
|
||||
import { navigate, openDetailAction } from './helpers.js';
|
||||
import { doLogin } from './login.js';
|
||||
|
||||
import { TOTP } from 'otpauth';
|
||||
@@ -54,8 +54,8 @@ test('Login - Change Password', async ({ page }) => {
|
||||
|
||||
// Navigate to the 'change password' page
|
||||
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
|
||||
await page.getByLabel('password', { exact: true }).fill('youshallnotpass');
|
||||
|
||||
@@ -9,6 +9,41 @@ import {
|
||||
} from './helpers.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 }) => {
|
||||
// Head to the "build order list" page
|
||||
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('combobox', { name: 'Filter' }).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();
|
||||
|
||||
// Save the filter group
|
||||
|
||||
Reference in New Issue
Block a user