Skip to content

Commit 507109b

Browse files
authored
Merge pull request #9149 from marmelab/fix-filterlivesearch-reset
Fix FilterLiveSearch reset button does not reset the value
2 parents 60a4332 + dfdfc8a commit 507109b

File tree

6 files changed

+327
-138
lines changed

6 files changed

+327
-138
lines changed

docs/useList.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,19 @@ const { data, total } = useList({
103103
// data will be [{ id: 1, name: 'Arnold' }] and total will be 1
104104
```
105105

106-
The filtering capabilities are very limited. For instance, there is no "greater than" or "less than" operator. You can only filter on the equality of a field.
106+
The filtering capabilities are very limited. A filter on a field is a simple string comparison. There is no "greater than" or "less than" operator. You can do a full-text filter by using the `q` filter.
107+
108+
```jsx
109+
const { data, total } = useList({
110+
data: [
111+
{ id: 1, name: 'Arnold' },
112+
{ id: 2, name: 'Sylvester' },
113+
{ id: 3, name: 'Jean-Claude' },
114+
],
115+
filter: { q: 'arno' },
116+
});
117+
// data will be [{ id: 1, name: 'Arnold' }] and total will be 1
118+
```
107119

108120
## `filterCallback`
109121

packages/ra-core/src/controller/list/useList.spec.tsx

+135-111
Original file line numberDiff line numberDiff line change
@@ -21,70 +21,6 @@ const UseList = ({
2121
};
2222

2323
describe('<useList />', () => {
24-
it('should filter string data based on the filter props', () => {
25-
const callback = jest.fn();
26-
const data = [
27-
{ id: 1, title: 'hello' },
28-
{ id: 2, title: 'world' },
29-
];
30-
31-
render(
32-
<UseList
33-
data={data}
34-
filter={{ title: 'world' }}
35-
sort={{ field: 'id', order: 'ASC' }}
36-
callback={callback}
37-
/>
38-
);
39-
40-
expect(callback).toHaveBeenCalledWith(
41-
expect.objectContaining({
42-
sort: { field: 'id', order: 'ASC' },
43-
isFetching: false,
44-
isLoading: false,
45-
data: [{ id: 2, title: 'world' }],
46-
error: undefined,
47-
total: 1,
48-
})
49-
);
50-
});
51-
52-
it('should filter array data based on the filter props', async () => {
53-
const callback = jest.fn();
54-
const data = [
55-
{ id: 1, items: ['one', 'two'] },
56-
{ id: 2, items: ['three'] },
57-
{ id: 3, items: 'four' },
58-
{ id: 4, items: ['five'] },
59-
];
60-
61-
render(
62-
<UseList
63-
data={data}
64-
filter={{ items: ['two', 'four', 'five'] }}
65-
sort={{ field: 'id', order: 'ASC' }}
66-
callback={callback}
67-
/>
68-
);
69-
70-
await waitFor(() => {
71-
expect(callback).toHaveBeenCalledWith(
72-
expect.objectContaining({
73-
sort: { field: 'id', order: 'ASC' },
74-
isFetching: false,
75-
isLoading: false,
76-
data: [
77-
{ id: 1, items: ['one', 'two'] },
78-
{ id: 3, items: 'four' },
79-
{ id: 4, items: ['five'] },
80-
],
81-
error: undefined,
82-
total: 3,
83-
})
84-
);
85-
});
86-
});
87-
8824
it('should apply sorting correctly', async () => {
8925
const callback = jest.fn();
9026
const data = [
@@ -229,66 +165,154 @@ describe('<useList />', () => {
229165
);
230166
});
231167

232-
it('should filter array data based on the custom filter', async () => {
233-
const callback = jest.fn();
234-
const data = [
235-
{ id: 1, items: ['one', 'two'] },
236-
{ id: 2, items: ['three'] },
237-
{ id: 3, items: 'four' },
238-
{ id: 4, items: ['five'] },
239-
];
168+
describe('filter', () => {
169+
it('should filter string data based on the filter props', () => {
170+
const callback = jest.fn();
171+
const data = [
172+
{ id: 1, title: 'hello' },
173+
{ id: 2, title: 'world' },
174+
];
240175

241-
render(
242-
<UseList
243-
data={data}
244-
sort={{ field: 'id', order: 'ASC' }}
245-
filterCallback={record => record.id > 2}
246-
callback={callback}
247-
/>
248-
);
176+
render(
177+
<UseList
178+
data={data}
179+
filter={{ title: 'world' }}
180+
sort={{ field: 'id', order: 'ASC' }}
181+
callback={callback}
182+
/>
183+
);
249184

250-
await waitFor(() => {
251185
expect(callback).toHaveBeenCalledWith(
252186
expect.objectContaining({
253187
sort: { field: 'id', order: 'ASC' },
254188
isFetching: false,
255189
isLoading: false,
256-
data: [
257-
{ id: 3, items: 'four' },
258-
{ id: 4, items: ['five'] },
259-
],
190+
data: [{ id: 2, title: 'world' }],
260191
error: undefined,
261-
total: 2,
192+
total: 1,
262193
})
263194
);
264195
});
265-
});
266196

267-
it('should filter data based on a custom filter with nested objects', () => {
268-
const callback = jest.fn();
269-
const data = [
270-
{ id: 1, title: { name: 'hello' } },
271-
{ id: 2, title: { name: 'world' } },
272-
];
197+
it('should filter array data based on the filter props', async () => {
198+
const callback = jest.fn();
199+
const data = [
200+
{ id: 1, items: ['one', 'two'] },
201+
{ id: 2, items: ['three'] },
202+
{ id: 3, items: 'four' },
203+
{ id: 4, items: ['five'] },
204+
];
273205

274-
render(
275-
<UseList
276-
data={data}
277-
filter={{ title: { name: 'world' } }}
278-
sort={{ field: 'id', order: 'ASC' }}
279-
callback={callback}
280-
/>
281-
);
206+
render(
207+
<UseList
208+
data={data}
209+
filter={{ items: ['two', 'four', 'five'] }}
210+
sort={{ field: 'id', order: 'ASC' }}
211+
callback={callback}
212+
/>
213+
);
282214

283-
expect(callback).toHaveBeenCalledWith(
284-
expect.objectContaining({
285-
sort: { field: 'id', order: 'ASC' },
286-
isFetching: false,
287-
isLoading: false,
288-
data: [{ id: 2, title: { name: 'world' } }],
289-
error: undefined,
290-
total: 1,
291-
})
292-
);
215+
await waitFor(() => {
216+
expect(callback).toHaveBeenCalledWith(
217+
expect.objectContaining({
218+
sort: { field: 'id', order: 'ASC' },
219+
isFetching: false,
220+
isLoading: false,
221+
data: [
222+
{ id: 1, items: ['one', 'two'] },
223+
{ id: 3, items: 'four' },
224+
{ id: 4, items: ['five'] },
225+
],
226+
error: undefined,
227+
total: 3,
228+
})
229+
);
230+
});
231+
});
232+
233+
it('should filter array data based on the custom filter', async () => {
234+
const callback = jest.fn();
235+
const data = [
236+
{ id: 1, items: ['one', 'two'] },
237+
{ id: 2, items: ['three'] },
238+
{ id: 3, items: 'four' },
239+
{ id: 4, items: ['five'] },
240+
];
241+
242+
render(
243+
<UseList
244+
data={data}
245+
sort={{ field: 'id', order: 'ASC' }}
246+
filterCallback={record => record.id > 2}
247+
callback={callback}
248+
/>
249+
);
250+
251+
await waitFor(() => {
252+
expect(callback).toHaveBeenCalledWith(
253+
expect.objectContaining({
254+
sort: { field: 'id', order: 'ASC' },
255+
isFetching: false,
256+
isLoading: false,
257+
data: [
258+
{ id: 3, items: 'four' },
259+
{ id: 4, items: ['five'] },
260+
],
261+
error: undefined,
262+
total: 2,
263+
})
264+
);
265+
});
266+
});
267+
268+
it('should filter data based on a custom filter with nested objects', () => {
269+
const callback = jest.fn();
270+
const data = [
271+
{ id: 1, title: { name: 'hello' } },
272+
{ id: 2, title: { name: 'world' } },
273+
];
274+
275+
render(
276+
<UseList
277+
data={data}
278+
filter={{ title: { name: 'world' } }}
279+
sort={{ field: 'id', order: 'ASC' }}
280+
callback={callback}
281+
/>
282+
);
283+
284+
expect(callback).toHaveBeenCalledWith(
285+
expect.objectContaining({
286+
sort: { field: 'id', order: 'ASC' },
287+
isFetching: false,
288+
isLoading: false,
289+
data: [{ id: 2, title: { name: 'world' } }],
290+
error: undefined,
291+
total: 1,
292+
})
293+
);
294+
});
295+
296+
it('should apply the q filter as a full-text filter', () => {
297+
const callback = jest.fn();
298+
const data = [
299+
{ id: 1, title: 'Abc', author: 'Def' }, // matches 'ab'
300+
{ id: 2, title: 'Ghi', author: 'Jkl' }, // does not match 'ab'
301+
{ id: 3, title: 'Mno', author: 'Abc' }, // matches 'ab'
302+
];
303+
304+
render(
305+
<UseList data={data} filter={{ q: 'ab' }} callback={callback} />
306+
);
307+
308+
expect(callback).toHaveBeenCalledWith(
309+
expect.objectContaining({
310+
data: [
311+
{ id: 1, title: 'Abc', author: 'Def' },
312+
{ id: 3, title: 'Mno', author: 'Abc' },
313+
],
314+
})
315+
);
316+
});
293317
});
294318
});

packages/ra-core/src/controller/list/useList.ts

+10
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,16 @@ export const useList = <RecordType extends RaRecord = any>(
178178
: recordValue.includes(filterValue)
179179
: Array.isArray(filterValue)
180180
? filterValue.includes(recordValue)
181+
: filterName === 'q' // special full-text filter
182+
? Object.keys(record).some(
183+
key =>
184+
typeof record[key] === 'string' &&
185+
record[key]
186+
.toLowerCase()
187+
.includes(
188+
(filterValue as string).toLowerCase()
189+
)
190+
)
181191
: filterValue == recordValue; // eslint-disable-line eqeqeq
182192
return result;
183193
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
4+
import { Basic } from './FilterLiveSearch.stories';
5+
6+
describe('FilterLiveSearch', () => {
7+
it('renders an empty text input', () => {
8+
render(<Basic />);
9+
expect(
10+
screen.getByPlaceholderText('ra.action.search').getAttribute('type')
11+
).toBe('text');
12+
expect(
13+
screen
14+
.getByPlaceholderText('ra.action.search')
15+
.getAttribute('value')
16+
).toBe('');
17+
});
18+
it('filters the list when typing', () => {
19+
render(<Basic />);
20+
expect(screen.queryAllByRole('listitem')).toHaveLength(27);
21+
fireEvent.change(screen.getByPlaceholderText('ra.action.search'), {
22+
target: { value: 'st' },
23+
});
24+
expect(screen.queryAllByRole('listitem')).toHaveLength(2); // Austria and Estonia
25+
});
26+
it('clears the filter when user click on the reset button', () => {
27+
render(<Basic />);
28+
fireEvent.change(screen.getByPlaceholderText('ra.action.search'), {
29+
target: { value: 'st' },
30+
});
31+
expect(screen.queryAllByRole('listitem')).toHaveLength(2);
32+
fireEvent.click(screen.getByLabelText('ra.action.clear_input_value'));
33+
expect(screen.queryAllByRole('listitem')).toHaveLength(27);
34+
});
35+
});

0 commit comments

Comments
 (0)