Skip to content

Commit 60b0fd2

Browse files
committed
Add empty property to ListBase and InfiniteListBase.
1 parent 4763556 commit 60b0fd2

File tree

9 files changed

+150
-10
lines changed

9 files changed

+150
-10
lines changed

docs/ListBase.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ The `<ListBase>` component accepts the following props:
8484
* [`debounce`](./List.md#debounce)
8585
* [`disableAuthentication`](./List.md#disableauthentication)
8686
* [`disableSyncWithLocation`](./List.md#disablesyncwithlocation)
87+
* [`empty`](./List.md#empty)
8788
* [`emptyWhileLoading`](./List.md#emptywhileloading)
8889
* [`exporter`](./List.md#exporter)
8990
* [`filter`](./List.md#filter-permanent-filter)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from './InfiniteListBase.stories';
1212
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
1313
import { testDataProvider } from '../../dataProvider';
14+
import { Empty } from './ListBase.stories';
1415

1516
describe('InfiniteListBase', () => {
1617
it('should fetch a list of records on mount, put it in a ListContext, and render its children', async () => {
@@ -160,6 +161,14 @@ describe('InfiniteListBase', () => {
160161
expect(screen.queryByText('Loading...')).toBeNull();
161162
});
162163

164+
it('should render a custom empty component when data is empty', async () => {
165+
render(<Empty />);
166+
expect(screen.queryByText('Loading...')).not.toBeNull();
167+
expect(screen.queryByText('War and Peace')).toBeNull();
168+
fireEvent.click(screen.getByText('Resolve books loading'));
169+
await screen.findByText('No books');
170+
});
171+
163172
it('should render loading component while loading', async () => {
164173
render(<Loading />);
165174
expect(screen.queryByText('Loading books...')).not.toBeNull();

packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,39 @@ export const FetchError = () => {
252252
);
253253
};
254254

255+
export const Empty = () => {
256+
let resolveGetList: (() => void) | null = null;
257+
const dataProvider = {
258+
...defaultDataProvider,
259+
getList: () => {
260+
return new Promise<GetListResult>(resolve => {
261+
resolveGetList = () => resolve({ data: [], total: 0 });
262+
});
263+
},
264+
};
265+
266+
return (
267+
<CoreAdminContext dataProvider={dataProvider}>
268+
<button
269+
onClick={() => {
270+
resolveGetList && resolveGetList();
271+
}}
272+
>
273+
Resolve books loading
274+
</button>
275+
<InfiniteListBase
276+
resource="books"
277+
perPage={5}
278+
loading={<p>Loading...</p>}
279+
empty={<p>No books</p>}
280+
>
281+
<BookListView />
282+
<InfinitePagination />
283+
</InfiniteListBase>
284+
</CoreAdminContext>
285+
);
286+
};
287+
255288
const defaultI18nProvider = polyglotI18nProvider(
256289
locale =>
257290
locale === 'fr'

packages/ra-core/src/controller/list/InfiniteListBase.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
5050
loading,
5151
offline,
5252
error,
53+
empty,
5354
children,
5455
render,
5556
...props
@@ -71,6 +72,11 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
7172
isPending,
7273
isPlaceholderData,
7374
error: errorState,
75+
data,
76+
total,
77+
hasPreviousPage,
78+
hasNextPage,
79+
filterValues,
7480
} = controllerProps;
7581

7682
const showAuthLoading =
@@ -95,6 +101,23 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
95101

96102
const showError = errorState && error !== false && error !== undefined;
97103

104+
const showEmpty =
105+
!errorState &&
106+
// the list is not loading data for the first time
107+
!isPending &&
108+
// the API returned no data (using either normal or partial pagination)
109+
(total === 0 ||
110+
(total == null &&
111+
hasPreviousPage === false &&
112+
hasNextPage === false &&
113+
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
114+
data.length === 0)) &&
115+
// the user didn't set any filters
116+
!Object.keys(filterValues).length &&
117+
// there is an empty page component
118+
empty !== undefined &&
119+
empty !== false;
120+
98121
return (
99122
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
100123
<OptionalResourceContextProvider value={props.resource}>
@@ -118,9 +141,11 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
118141
? offline
119142
: showError
120143
? error
121-
: render
122-
? render(controllerProps)
123-
: children}
144+
: showEmpty
145+
? empty
146+
: render
147+
? render(controllerProps)
148+
: children}
124149
</InfinitePaginationContext.Provider>
125150
</ListContextProvider>
126151
</OptionalResourceContextProvider>
@@ -133,6 +158,7 @@ export interface InfiniteListBaseProps<RecordType extends RaRecord = any>
133158
loading?: ReactNode;
134159
offline?: ReactNode;
135160
error?: ReactNode;
161+
empty?: ReactNode;
136162
children?: ReactNode;
137163
render?: (props: InfiniteListControllerResult<RecordType>) => ReactNode;
138164
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33
import {
44
AccessControl,
55
DefaultTitle,
6+
Empty,
67
EmptyWhileLoading,
78
EmptyWhileLoadingRender,
89
FetchError,
@@ -168,6 +169,13 @@ describe('ListBase', () => {
168169
await screen.findByText('War and Peace');
169170
await screen.findByText('You are offline, the data may be outdated');
170171
});
172+
it('should render a custom empty component when data is empty', async () => {
173+
render(<Empty />);
174+
expect(screen.queryByText('Loading...')).not.toBeNull();
175+
expect(screen.queryByText('War and Peace')).toBeNull();
176+
fireEvent.click(screen.getByText('Resolve books loading'));
177+
await screen.findByText('No books');
178+
});
171179
it('should render nothing while loading if emptyWhileLoading is set to true', async () => {
172180
render(<EmptyWhileLoading />);
173181
expect(screen.queryByText('Loading...')).toBeNull();

packages/ra-core/src/controller/list/ListBase.stories.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,39 @@ export const FetchError = () => {
503503
);
504504
};
505505

506+
export const Empty = () => {
507+
let resolveGetList: (() => void) | null = null;
508+
const baseProvider = defaultDataProvider(0);
509+
const dataProvider = {
510+
...baseProvider,
511+
getList: () => {
512+
return new Promise<GetListResult>(resolve => {
513+
resolveGetList = () => resolve({ data: [], total: 0 });
514+
});
515+
},
516+
};
517+
518+
return (
519+
<CoreAdminContext dataProvider={dataProvider}>
520+
<button
521+
onClick={() => {
522+
resolveGetList && resolveGetList();
523+
}}
524+
>
525+
Resolve books loading
526+
</button>
527+
<ListBase
528+
resource="books"
529+
perPage={5}
530+
loading={<p>Loading...</p>}
531+
empty={<p>No books</p>}
532+
>
533+
<BookListView />
534+
</ListBase>
535+
</CoreAdminContext>
536+
);
537+
};
538+
506539
export const EmptyWhileLoading = () => {
507540
let resolveGetList: (() => void) | null = null;
508541
const baseProvider = defaultDataProvider(0);

packages/ra-core/src/controller/list/ListBase.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const ListBase = <RecordType extends RaRecord = any>({
5151
loading,
5252
offline,
5353
error,
54+
empty,
5455
render,
5556
...props
5657
}: ListBaseProps<RecordType>) => {
@@ -71,6 +72,11 @@ export const ListBase = <RecordType extends RaRecord = any>({
7172
isPending,
7273
isPlaceholderData,
7374
error: errorState,
75+
data,
76+
total,
77+
hasPreviousPage,
78+
hasNextPage,
79+
filterValues,
7480
} = controllerProps;
7581

7682
const showAuthLoading =
@@ -95,7 +101,25 @@ export const ListBase = <RecordType extends RaRecord = any>({
95101

96102
const showError = errorState && error !== false && error !== undefined;
97103

98-
const showEmpty = isPending && !showOffline && emptyWhileLoading === true;
104+
const showEmptyWhileLoading =
105+
isPending && !showOffline && emptyWhileLoading === true;
106+
107+
const showEmpty =
108+
!errorState &&
109+
// the list is not loading data for the first time
110+
!isPending &&
111+
// the API returned no data (using either normal or partial pagination)
112+
(total === 0 ||
113+
(total == null &&
114+
hasPreviousPage === false &&
115+
hasNextPage === false &&
116+
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
117+
data.length === 0)) &&
118+
// the user didn't set any filters
119+
!Object.keys(filterValues).length &&
120+
// there is an empty page component
121+
empty !== undefined &&
122+
empty !== false;
99123

100124
return (
101125
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
@@ -109,11 +133,13 @@ export const ListBase = <RecordType extends RaRecord = any>({
109133
? offline
110134
: showError
111135
? error
112-
: showEmpty
136+
: showEmptyWhileLoading
113137
? null
114-
: render
115-
? render(controllerProps)
116-
: children}
138+
: showEmpty
139+
? empty
140+
: render
141+
? render(controllerProps)
142+
: children}
117143
</ListContextProvider>
118144
</OptionalResourceContextProvider>
119145
);
@@ -126,6 +152,7 @@ export interface ListBaseProps<RecordType extends RaRecord = any>
126152
loading?: ReactNode;
127153
offline?: ReactNode;
128154
error?: ReactNode;
155+
empty?: ReactNode;
129156
children?: ReactNode;
130157
render?: (props: ListControllerResult<RecordType, Error>) => ReactNode;
131158
}

packages/ra-ui-materialui/src/list/InfiniteList.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ const defaultFilter = {};
118118
const defaultAuthLoading = <Loading />;
119119

120120
export interface InfiniteListProps<RecordType extends RaRecord = any>
121-
extends Omit<InfiniteListBaseProps<RecordType>, 'children' | 'render'>,
121+
extends Omit<
122+
InfiniteListBaseProps<RecordType>,
123+
'children' | 'render' | 'empty'
124+
>,
122125
ListViewProps {}
123126

124127
const PREFIX = 'RaInfiniteList';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const List = <RecordType extends RaRecord = any>(
111111
};
112112

113113
export interface ListProps<RecordType extends RaRecord = any>
114-
extends Omit<ListBaseProps<RecordType>, 'children' | 'render'>,
114+
extends Omit<ListBaseProps<RecordType>, 'children' | 'render' | 'empty'>,
115115
ListViewProps {}
116116

117117
const defaultFilter = {};

0 commit comments

Comments
 (0)