Skip to content

Commit 61fe351

Browse files
authored
Merge pull request #8017 from marmelab/7831-clear-all-filters
Add a "Clear all filters" button
2 parents efad0fb + 97af862 commit 61fe351

File tree

8 files changed

+129
-53
lines changed

8 files changed

+129
-53
lines changed

packages/ra-core/src/i18n/TranslationMessages.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface TranslationMessages extends StringMap {
2323
list: string;
2424
refresh: string;
2525
remove_filter: string;
26+
remove_all_filters: string;
2627
remove: string;
2728
save: string;
2829
search: string;

packages/ra-language-english/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const englishMessages: TranslationMessages = {
1919
list: 'List',
2020
refresh: 'Refresh',
2121
remove_filter: 'Remove this filter',
22+
remove_all_filters: 'Remove all filters',
2223
remove: 'Remove',
2324
save: 'Save',
2425
search: 'Search',

packages/ra-language-french/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const frenchMessages: TranslationMessages = {
2020
list: 'Liste',
2121
refresh: 'Actualiser',
2222
remove_filter: 'Supprimer ce filtre',
23+
remove_all_filters: 'Supprimer tous les filtres',
2324
remove: 'Supprimer',
2425
save: 'Enregistrer',
2526
select_all: 'Tout sélectionner',

packages/ra-ui-materialui/src/list/List.stories.tsx

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { Admin } from 'react-admin';
2+
import { Admin, AutocompleteInput } from 'react-admin';
33
import { Resource, useListContext } from 'ra-core';
44
import fakeRestDataProvider from 'ra-data-fakerest';
55
import { createMemoryHistory } from 'history';
@@ -12,7 +12,7 @@ import { SearchInput, TextInput } from '../input';
1212

1313
export default { title: 'ra-ui-materialui/list/List' };
1414

15-
const dataProvider = fakeRestDataProvider({
15+
const data = {
1616
books: [
1717
{
1818
id: 1,
@@ -80,9 +80,22 @@ const dataProvider = fakeRestDataProvider({
8080
author: 'James Joyce',
8181
year: 1922,
8282
},
83+
{
84+
id: 12,
85+
title: 'One Hundred Years of Solitude',
86+
author: 'Gabriel García Márquez',
87+
year: 1967,
88+
},
89+
{
90+
id: 13,
91+
title: 'Snow Country',
92+
author: 'Yasunari Kawabata',
93+
year: 1956,
94+
},
8395
],
8496
authors: [],
85-
});
97+
};
98+
const dataProvider = fakeRestDataProvider(data);
8699

87100
const history = createMemoryHistory({ initialEntries: ['/books'] });
88101

@@ -130,7 +143,19 @@ const BookListWithFilters = () => (
130143
<List
131144
filters={[
132145
<SearchInput source="q" alwaysOn />,
133-
<TextInput source="title" />,
146+
<AutocompleteInput
147+
source="title"
148+
optionValue="title"
149+
optionText="title"
150+
choices={data.books}
151+
/>,
152+
<AutocompleteInput
153+
source="author"
154+
optionValue="author"
155+
optionText="author"
156+
choices={data.books}
157+
/>,
158+
<TextInput source="year" />,
134159
]}
135160
>
136161
<BookList />

packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx

+38-23
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, fireEvent, screen } from '@testing-library/react';
3+
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
44
import { createTheme } from '@mui/material/styles';
55

66
import { AdminContext } from '../../AdminContext';
77
import { FilterButton } from './FilterButton';
88
import { TextInput } from '../../input';
9+
import { Basic } from './FilterButton.stories';
910

1011
const theme = createTheme();
1112

@@ -44,25 +45,6 @@ describe('<FilterButton />', () => {
4445
expect(queryByText('Name')).toBeNull();
4546
});
4647

47-
it('should not display the filter button if all filters are shown and there is no filter value', () => {
48-
render(
49-
<AdminContext theme={theme}>
50-
<FilterButton
51-
{...defaultProps}
52-
filters={[
53-
<TextInput source="title" label="Title" />,
54-
<TextInput source="customer.name" label="Name" />,
55-
]}
56-
displayedFilters={{
57-
title: true,
58-
'customer.name': true,
59-
}}
60-
/>
61-
</AdminContext>
62-
);
63-
expect(screen.queryByLabelText('ra.action.add_filter')).toBeNull();
64-
});
65-
6648
it('should display the filter button if all filters are shown and there is a filter value', () => {
6749
render(
6850
<AdminContext theme={theme}>
@@ -91,7 +73,7 @@ describe('<FilterButton />', () => {
9173
const hiddenFilter = (
9274
<TextInput source="Returned" label="Returned" disabled={true} />
9375
);
94-
const { getByRole, getByLabelText } = render(
76+
const { getByLabelText, queryByText } = render(
9577
<AdminContext theme={theme}>
9678
<FilterButton
9779
{...defaultProps}
@@ -102,12 +84,45 @@ describe('<FilterButton />', () => {
10284

10385
fireEvent.click(getByLabelText('ra.action.add_filter'));
10486

105-
const disabledFilter = getByRole('menuitem');
87+
const disabledFilter = queryByText('Returned')?.closest('li');
10688

10789
expect(disabledFilter).not.toBeNull();
108-
expect(disabledFilter.getAttribute('aria-disabled')).toEqual(
90+
expect(disabledFilter?.getAttribute('aria-disabled')).toEqual(
10991
'true'
11092
);
11193
});
94+
95+
it('should remove all filters when the "Clear all filters" button is clicked', async () => {
96+
render(<Basic />);
97+
98+
// First, check we don't have a clear filters option yet
99+
await screen.findByText('Add filter');
100+
fireEvent.click(screen.getByText('Add filter'));
101+
102+
await screen.findByText('Title', { selector: 'li > span' });
103+
expect(screen.queryByDisplayValue('Remove all filters')).toBeNull();
104+
105+
// Then we apply a filter
106+
fireEvent.click(
107+
screen.getByText('Title', { selector: 'li > span' })
108+
);
109+
await screen.findByDisplayValue(
110+
'Accusantium qui nihil voluptatum quia voluptas maxime ab similique'
111+
);
112+
113+
// Then we clear all filters
114+
fireEvent.click(screen.getByText('Add filter'));
115+
await screen.findByText('Remove all filters');
116+
fireEvent.click(screen.getByText('Remove all filters'));
117+
118+
// We check that the previously applied filter has been removed
119+
await waitFor(() => {
120+
expect(
121+
screen.queryByDisplayValue(
122+
'Accusantium qui nihil voluptatum quia voluptas maxime ab similique'
123+
)
124+
).toBeNull();
125+
});
126+
});
112127
});
113128
});

packages/ra-ui-materialui/src/list/filter/FilterButton.stories.tsx

+46-22
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
Pagination,
1111
TextField,
1212
TextInput,
13+
TopToolbar,
1314
} from 'react-admin';
14-
import { Stack } from '@mui/material';
1515
import fakerestDataProvider from 'ra-data-fakerest';
1616

1717
export default { title: 'ra-ui-materialui/list/filter/FilterButton' };
@@ -100,24 +100,21 @@ const data = {
100100
},
101101
],
102102
};
103-
const postFilters = [
104-
<TextInput label="Search" source="q" alwaysOn />,
105-
<TextInput label="Title" source="title" defaultValue="Hello, World!" />,
106-
];
107103

108-
const ListToolbar = () => (
109-
<Stack direction="row" justifyContent="space-between">
110-
<FilterForm filters={postFilters} />
111-
<div>
112-
<FilterButton filters={postFilters} />
113-
<CreateButton />
114-
</div>
115-
</Stack>
116-
);
117-
118-
const PostList = () => (
104+
const ListToolbar = (props: { postFilters: React.ReactElement[] }) => {
105+
return (
106+
<TopToolbar>
107+
<FilterForm filters={props.postFilters} />
108+
<div>
109+
<FilterButton filters={props.postFilters} />
110+
<CreateButton />
111+
</div>
112+
</TopToolbar>
113+
);
114+
};
115+
const PostList = (props: { postFilters: React.ReactElement[] }) => (
119116
<ListBase>
120-
<ListToolbar />
117+
<ListToolbar postFilters={props.postFilters} />
121118
<Datagrid>
122119
<TextField source="id" />
123120
<TextField source="title" />
@@ -127,8 +124,35 @@ const PostList = () => (
127124
</ListBase>
128125
);
129126

130-
export const Basic = () => (
131-
<Admin dataProvider={fakerestDataProvider(data)}>
132-
<Resource name="posts" list={PostList} />
133-
</Admin>
134-
);
127+
export const Basic = () => {
128+
const postFilters: React.ReactElement[] = [
129+
<TextInput label="Search" source="q" alwaysOn />,
130+
<TextInput
131+
label="Title"
132+
source="title"
133+
defaultValue="Accusantium qui nihil voluptatum quia voluptas maxime ab similique"
134+
/>,
135+
];
136+
return (
137+
<Admin dataProvider={fakerestDataProvider(data)}>
138+
<Resource
139+
name="posts"
140+
list={<PostList postFilters={postFilters} />}
141+
/>
142+
</Admin>
143+
);
144+
};
145+
146+
export const DisabledFilters = () => {
147+
const postFilters: React.ReactElement[] = [
148+
<TextInput label="Title" source="title" disabled={true} />,
149+
];
150+
return (
151+
<Admin dataProvider={fakerestDataProvider(data)}>
152+
<Resource
153+
name="posts"
154+
list={<PostList postFilters={postFilters} />}
155+
/>
156+
</Admin>
157+
);
158+
};

packages/ra-ui-materialui/src/list/filter/FilterButton.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ export const FilterButton = (props: FilterButtonProps): JSX.Element => {
3434
displayedFilters = {},
3535
filterValues,
3636
perPage,
37+
setFilters,
3738
showFilter,
3839
sort,
3940
} = useListContext(props);
4041
const hasFilterValues = !isEqual(filterValues, {});
42+
const hasDisplayedFilters = !isEqual(displayedFilters, {});
4143
const validSavedQueries = extractValidSavedQueries(savedQueries);
4244
const hasSavedCurrentQuery = validSavedQueries.some(savedQuery =>
4345
isEqual(savedQuery.value, {
@@ -184,13 +186,20 @@ export const FilterButton = (props: FilterButtonProps): JSX.Element => {
184186
</MenuItem>
185187
)
186188
)}
187-
{hasFilterValues && !hasSavedCurrentQuery ? (
189+
{hasFilterValues && !hasSavedCurrentQuery && (
188190
<MenuItem onClick={showAddSavedQueryDialog}>
189191
{translate('ra.saved_queries.new_label', {
190192
_: 'Save current query...',
191193
})}
192194
</MenuItem>
193-
) : null}
195+
)}
196+
{hasDisplayedFilters && (
197+
<MenuItem onClick={() => setFilters({}, {}, false)}>
198+
{translate('ra.action.remove_all_filters', {
199+
_: 'Remove all filters',
200+
})}
201+
</MenuItem>
202+
)}
194203
</Menu>
195204
<AddSavedQueryDialog
196205
open={addSavedQueryDialogOpen}

packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('<FilterForm />', () => {
6868
/>
6969
</AdminContext>
7070
);
71-
fireEvent.change(screen.queryByLabelText('Title'), {
71+
fireEvent.change(screen.queryByLabelText('Title') as Element, {
7272
target: { value: 'foo' },
7373
});
7474
await waitFor(() => {
@@ -164,7 +164,7 @@ describe('<FilterForm />', () => {
164164
/>
165165
</AdminContext>
166166
);
167-
fireEvent.change(screen.queryByLabelText('Title'), {
167+
fireEvent.change(screen.queryByLabelText('Title') as Element, {
168168
target: { value: 'foo' },
169169
});
170170
await waitFor(() => {

0 commit comments

Comments
 (0)