Skip to content

Commit e587878

Browse files
committed
fix: make filter reset behavior consistent
This fixes some of the incorrect behavior with filter clear or reset buttons. They should now only be shown when the state differs from default and use correct terms for restoring and clearing (depends on default filters).
1 parent 4aa5a00 commit e587878

23 files changed

Lines changed: 776 additions & 83 deletions

File tree

playwright/test-utils/helpers/filters.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ export const removeFilter = async (page: Page, filter: FilterConfig) => {
332332
export const resetFilters = async (page: Page) => {
333333
// Close any open dropdowns first
334334
await page.keyboard.press('Escape');
335-
await page.getByRole('button', { name: /Reset filters/i }).click();
335+
try {
336+
await page.getByRole('button', { name: /(Reset|Clear) filters/i }).waitFor({ timeout: 5000 });
337+
} catch {
338+
return;
339+
}
340+
await page.getByRole('button', { name: /(Reset|Clear) filters/i }).click();
336341
await waitForTableLoad(page);
337342
};

src/Messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export default defineMessages({
166166
labelsFiltersClear: {
167167
id: 'labelsFiltersClear',
168168
description: 'label for remove filter chips',
169-
defaultMessage: 'Reset filters',
169+
defaultMessage: 'Clear filters',
170170
},
171171
labelsFiltersCvesSearchPlaceHolder: {
172172
id: 'labelsFiltersCvesSearch',

src/PresentationalComponents/TableView/TableView.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import { PrimaryToolbar } from '@redhat-cloud-services/frontend-components/Prima
44
import { SkeletonTable } from '@redhat-cloud-services/frontend-components/SkeletonTable';
55
import PropTypes from 'prop-types';
66
import React from 'react';
7-
import messages from '../../Messages';
87
import AsyncRemediationButton from '../../SmartComponents/Remediation/AsyncRemediationButton';
9-
import { arrayFromObj, buildFilterChips, convertLimitOffset } from '../../Utilities/Helpers';
8+
import { arrayFromObj, buildActiveFilterConfig, convertLimitOffset } from '../../Utilities/Helpers';
109
import { useRemoveFilter, useBulkSelectConfig } from '../../Utilities/hooks';
11-
import { intl } from '../../Utilities/IntlProvider';
1210
import TableFooter from './TableFooter';
1311
import ErrorHandler from '../../PresentationalComponents/Snippets/ErrorHandler';
1412
import { Skeleton, ToolbarItem } from '@patternfly/react-core';
@@ -50,6 +48,10 @@ const TableView = ({
5048
const selectedCount = selectedRows && arrayFromObj(selectedRows).length;
5149
const { code, hasError, isLoading } = status;
5250
const bulkSelectConfig = useBulkSelectConfig(selectedCount, onSelect, metadata, rows, onCollapse);
51+
const activeFiltersConfig = React.useMemo(
52+
() => buildActiveFilterConfig(filter, search, deleteFilters, searchChipLabel, defaultFilters),
53+
[defaultFilters, deleteFilters, filter, search, searchChipLabel],
54+
);
5355

5456
const [isColumnMgmtModalOpen, setColumnMgmtModalOpen] = React.useState(false);
5557
const [appliedColumns, setAppliedColumns] = React.useState(columns);
@@ -102,13 +104,7 @@ const TableView = ({
102104
)
103105
}
104106
filterConfig={filterConfig}
105-
activeFiltersConfig={{
106-
filters: buildFilterChips(filter, search, searchChipLabel),
107-
onDelete: deleteFilters,
108-
deleteTitle: intl.formatMessage(
109-
(defaultFilters && messages.labelsFiltersReset) || messages.labelsFiltersClear,
110-
),
111-
}}
107+
activeFiltersConfig={activeFiltersConfig}
112108
actionsConfig={{
113109
actions: [
114110
remediationProvider && (

src/PresentationalComponents/TableView/TableView.test.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import TableView from './TableView';
22
import { render, screen, waitFor } from '@testing-library/react';
33
import { Provider, useSelector } from 'react-redux';
44
import configureStore from 'redux-mock-store';
5-
import { storeListDefaults } from '../../Utilities/constants';
5+
import { pageDefaultFilters, storeListDefaults } from '../../Utilities/constants';
66
import { systemPackages } from '../../Utilities/RawDataForTesting';
77
import userEvent from '@testing-library/user-event';
8+
import '@testing-library/jest-dom';
89

910
const testObj = {
1011
columns: [],
@@ -134,6 +135,67 @@ describe('TableView', () => {
134135
expect(asFragment()).toMatchSnapshot();
135136
});
136137

138+
it('should keep default filter chips visible while hiding reset at the default state', () => {
139+
render(
140+
<Provider store={store}>
141+
<TableView
142+
{...testObj}
143+
defaultFilters={pageDefaultFilters.packages}
144+
searchChipLabel='Package'
145+
store={{
146+
rows: [],
147+
metadata: { total_items: 10, limit: 20, offset: 0 },
148+
status: { isLoading: false, code: 200, hasError: false },
149+
queryParams: { filter: { systems_applicable: ['gt:0'] } },
150+
}}
151+
/>
152+
</Provider>,
153+
);
154+
155+
expect(screen.getByText('Systems with patches available')).toBeInTheDocument();
156+
expect(screen.queryByRole('button', { name: /reset filters/i })).not.toBeInTheDocument();
157+
});
158+
159+
it('should show a reset button when active filters differ from defaults', () => {
160+
render(
161+
<Provider store={store}>
162+
<TableView
163+
{...testObj}
164+
defaultFilters={pageDefaultFilters.packages}
165+
searchChipLabel='Package'
166+
store={{
167+
rows: [],
168+
metadata: { total_items: 10, limit: 20, offset: 0 },
169+
status: { isLoading: false, code: 200, hasError: false },
170+
queryParams: { filter: { systems_applicable: ['eq:0'] } },
171+
}}
172+
/>
173+
</Provider>,
174+
);
175+
176+
expect(screen.getByRole('button', { name: /reset filters/i })).toBeInTheDocument();
177+
});
178+
179+
it('should show a clear button for pages without defaults', () => {
180+
render(
181+
<Provider store={store}>
182+
<TableView
183+
{...testObj}
184+
defaultFilters={pageDefaultFilters.advisories}
185+
searchChipLabel='Advisory'
186+
store={{
187+
rows: [],
188+
metadata: { total_items: 10, limit: 20, offset: 0 },
189+
status: { isLoading: false, code: 200, hasError: false },
190+
queryParams: { filter: { advisory_type_name: 'bugfix' } },
191+
}}
192+
/>
193+
</Provider>,
194+
);
195+
196+
expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
197+
});
198+
137199
it('Should unselect', async () => {
138200
await render(
139201
<Provider store={store}>

src/SmartComponents/Advisories/Advisories.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
selectAdvisoryRow,
1717
} from '../../store/Actions/Actions';
1818
import { exportAdvisoriesCSV, exportAdvisoriesJSON } from '../../Utilities/api/api';
19+
import { pageDefaultFilters } from '../../Utilities/constants';
1920
import { createAdvisoriesRows } from '../../Utilities/DataMappers';
2021
import {
2122
createSortBy,
@@ -200,6 +201,7 @@ const Advisories = () => {
200201
rebootFilter(apply, queryParams?.filter),
201202
],
202203
}}
204+
defaultFilters={pageDefaultFilters.advisories}
203205
searchChipLabel={intl.formatMessage(messages.labelsFiltersSearchAdvisoriesTitle)}
204206
isRemediationLoading={isRemediationLoading}
205207
hasColumnManagement

src/SmartComponents/AdvisoryDetail/CvesModal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import TableView from '../../PresentationalComponents/TableView/TableView';
66
import searchFilter from '../../PresentationalComponents/Filters/SearchFilter';
77
import { cvesTableColumns } from '../../PresentationalComponents/TableView/TableViewAssets';
88
import { fetchCves } from '../../store/Actions/VulnerabilityActions';
9+
import { pageDefaultFilters } from '../../Utilities/constants';
910
import { createCvesRows } from '../../Utilities/DataMappers';
1011
import { sortCves } from '../../Utilities/Helpers';
1112
import { SortByDirection } from '@patternfly/react-table';
@@ -108,6 +109,7 @@ const CvesModal = ({ cveIds }: CvesModalProps) => {
108109
status,
109110
queryParams: { filter: {}, search },
110111
}}
112+
defaultFilters={pageDefaultFilters.cves}
111113
filterConfig={{
112114
items: [
113115
searchFilter(

src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
exportAdvisorySystemsJSON,
1818
fetchAdvisorySystems,
1919
} from '../../Utilities/api/api';
20-
import { remediationIdentifiers } from '../../Utilities/constants';
20+
import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
2121
import {
2222
arrayFromObj,
2323
persistantParams,
@@ -64,7 +64,11 @@ const AdvisorySystemsTable = ({
6464
(newColumns) => setAppliedColumns(newColumns),
6565
);
6666

67-
const [deleteFilters] = useRemoveFilter({ search, ...filter }, apply);
67+
const [deleteFilters] = useRemoveFilter(
68+
{ search, ...filter },
69+
apply,
70+
pageDefaultFilters.advisorySystems,
71+
);
6872

6973
const filterConfig = {
7074
items: [
@@ -78,7 +82,12 @@ const AdvisorySystemsTable = ({
7882
],
7983
};
8084

81-
const activeFiltersConfig = buildActiveFiltersConfig(filter, search, deleteFilters);
85+
const activeFiltersConfig = buildActiveFiltersConfig(
86+
filter,
87+
search,
88+
deleteFilters,
89+
pageDefaultFilters.advisorySystems,
90+
);
8291

8392
const onSelect = useOnSelect(systems, selectedRows, {
8493
endpoint: ID_API_ENDPOINTS.advisorySystems(advisoryName),

src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.test.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const initStore = (state) => {
3131
return mockStore(state);
3232
};
3333

34+
beforeEach(() => {
35+
InventoryTable.mockClear();
36+
});
37+
3438
const renderComponent = async (mockedStore) => {
3539
render(
3640
<ComponentWithContext renderOptions={{ store: initStore(mockedStore) }}>
@@ -179,6 +183,21 @@ describe('AdvisorySystemsTable.js', () => {
179183
);
180184
});
181185

186+
it('should keep active filters empty when the page is at its default state', async () => {
187+
await renderComponent(mockState);
188+
189+
expect(InventoryTable).toHaveBeenCalledWith(
190+
expect.objectContaining({
191+
activeFiltersConfig: {
192+
deleteTitle: 'Clear filters',
193+
filters: [],
194+
onDelete: expect.any(Function),
195+
},
196+
}),
197+
{},
198+
);
199+
});
200+
182201
it('should provide activeFilters config', async () => {
183202
const filteredState = {
184203
...mockState,
@@ -194,7 +213,7 @@ describe('AdvisorySystemsTable.js', () => {
194213
expect(InventoryTable).toHaveBeenCalledWith(
195214
expect.objectContaining({
196215
activeFiltersConfig: {
197-
deleteTitle: 'Reset filters',
216+
deleteTitle: 'Clear filters',
198217
filters: [
199218
{
200219
category: 'Status',

src/SmartComponents/PackageSystems/PackageSystems.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import {
2626
fetchPackageSystems,
2727
fetchPackageVersions,
2828
} from '../../Utilities/api/api';
29-
import { remediationIdentifiers } from '../../Utilities/constants';
29+
import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
3030
import {
3131
arrayFromObj,
32-
buildFilterChips,
32+
buildActiveFilterConfig,
3333
decodeQueryparams,
3434
filterRemediatablePackageSystems,
3535
persistantParams,
@@ -91,7 +91,11 @@ const PackageSystems = ({ packageName }) => {
9191
(newColumns) => setAppliedColumns(newColumns),
9292
);
9393

94-
const [deleteFilters] = useRemoveFilter({ ...filter, search }, apply);
94+
const [deleteFilters] = useRemoveFilter(
95+
{ ...filter, search },
96+
apply,
97+
pageDefaultFilters.packageSystems,
98+
);
9599

96100
const filterConfig = {
97101
items: [
@@ -107,15 +111,15 @@ const PackageSystems = ({ packageName }) => {
107111
};
108112

109113
const activeFiltersConfig = useMemo(
110-
() => ({
111-
filters: buildFilterChips(
114+
() =>
115+
buildActiveFilterConfig(
112116
filter,
113117
search,
118+
deleteFilters,
114119
intl.formatMessage(messages.labelsFiltersSystemsSearchTitle),
120+
pageDefaultFilters.packageSystems,
115121
),
116-
onDelete: deleteFilters,
117-
}),
118-
[filter, search],
122+
[deleteFilters, filter, search],
119123
);
120124

121125
const constructFilename = (system) => `${system.available_evra}`;

src/SmartComponents/PackageSystems/PackageSystems.test.js

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { systemRows } from '../../Utilities/RawDataForTesting';
33
import { initMocks } from '../../Utilities/unitTestingUtilities.js';
44
import PackageSystems from './PackageSystems';
55
import { ComponentWithContext } from '../../Utilities/TestingUtilities.js';
6-
import { render, screen } from '@testing-library/react';
6+
import { render, screen, waitFor } from '@testing-library/react';
7+
import { InventoryTable } from '@redhat-cloud-services/frontend-components/Inventory';
78

89
initMocks();
910

@@ -61,26 +62,82 @@ const mockState = {
6162
},
6263
};
6364

64-
const initStore = () => {
65+
const initStore = (state = mockState) => {
6566
const mockStore = configureStore([]);
66-
return mockStore(mockState);
67+
return mockStore(state);
6768
};
6869

69-
const store = initStore(mockState);
70-
71-
beforeEach(() => {
70+
const renderComponent = async (state = mockState) => {
7271
render(
73-
<ComponentWithContext renderOptions={{ store }}>
72+
<ComponentWithContext renderOptions={{ store: initStore(state) }}>
7473
<PackageSystems packageName='testName' />
7574
</ComponentWithContext>,
7675
);
76+
77+
await waitFor(() => {
78+
expect(screen.getByTestId('inventory-mock-component')).toBeVisible();
79+
});
80+
};
81+
82+
beforeEach(() => {
83+
InventoryTable.mockClear();
7784
});
7885

7986
// TODO: find a meaningful way of testing InventoryTable fed module
8087
describe('PackageSystems.js', () => {
81-
it('Should render inventory table', () => {
88+
it('Should render inventory table', async () => {
89+
await renderComponent();
8290
expect(screen.getByTestId('inventory-mock-component')).toBeVisible();
8391
});
92+
93+
it('should keep active filters empty when there are no non-default filters', async () => {
94+
await renderComponent();
95+
96+
expect(InventoryTable).toHaveBeenCalledWith(
97+
expect.objectContaining({
98+
activeFiltersConfig: {
99+
deleteTitle: 'Clear filters',
100+
filters: [],
101+
onDelete: expect.any(Function),
102+
},
103+
}),
104+
{},
105+
);
106+
});
107+
108+
it('should provide a clear filters action when filters are active', async () => {
109+
await renderComponent({
110+
...mockState,
111+
PackageSystemsStore: {
112+
queryParams: {
113+
filter: { status: ['Applicable'] },
114+
},
115+
},
116+
});
117+
118+
expect(InventoryTable).toHaveBeenCalledWith(
119+
expect.objectContaining({
120+
activeFiltersConfig: {
121+
deleteTitle: 'Clear filters',
122+
filters: [
123+
{
124+
category: 'Status',
125+
chips: [
126+
{
127+
id: 'status',
128+
name: 'Applicable',
129+
value: 'Applicable',
130+
},
131+
],
132+
id: 'status',
133+
},
134+
],
135+
onDelete: expect.any(Function),
136+
},
137+
}),
138+
{},
139+
);
140+
});
84141
// it('Should dispatch change package systems params action once only', () => {
85142
// const dispatchedActions = store.getActions();
86143
// expect(dispatchedActions.filter(item => item.type === 'CHANGE_PACKAGE_SYSTEMS_PARAMS')).toHaveLength(1);

0 commit comments

Comments
 (0)