2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-06 07:00:39 +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
+20
View File
@@ -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();
};
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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
+7 -5
View File
@@ -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(
+3 -3
View File
@@ -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 -3
View File
@@ -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');
+3 -3
View File
@@ -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');
+39 -1
View File
@@ -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