Skip to content

Commit

Permalink
Merge pull request #10248 from marmelab/fix-list-empty
Browse files Browse the repository at this point in the history
Fix List empty component wrongly appears when using partial pagination
  • Loading branch information
fzaninotto authored Sep 30, 2024
2 parents 5d25010 + fcf5529 commit 88f9f2a
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 88 deletions.
196 changes: 109 additions & 87 deletions packages/ra-ui-materialui/src/list/List.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { List } from './List';
import { Filter } from './filter';
import { TextInput } from '../input';
import { Notification } from '../layout';
import { Basic, Title, TitleFalse, TitleElement } from './List.stories';
import {
Basic,
Title,
TitleFalse,
TitleElement,
PartialPagination,
} from './List.stories';

const theme = createTheme(defaultTheme);

Expand Down Expand Up @@ -104,99 +110,115 @@ describe('<List />', () => {
expect(screen.queryAllByText('Hello')).toHaveLength(1);
});

it('should render an invite when the list is empty', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts">
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
screen.getByText('resources.posts.empty');
expect(screen.queryByText('dummy')).toBeNull();
describe('empty', () => {
it('should render an invite when the list is empty', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts">
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
screen.getByText('resources.posts.empty');
expect(screen.queryByText('dummy')).toBeNull();
});
});
});

it('should not render an invite when the list is empty with an empty prop set to false', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts" empty={false}>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('dummy');
it('should not render an invite when the list is empty with an empty prop set to false', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts" empty={false}>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('dummy');
});
});
});

it('should render custom empty component when data is empty', async () => {
const Dummy = () => null;
const CustomEmpty = () => <div>Custom Empty</div>;
it('should not render an empty component when using partial pagination and the list is not empty', async () => {
render(<PartialPagination />);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('John Doe');
});
});

const dataProvider = {
getList: jest.fn(() =>
Promise.resolve({
data: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
})
),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts" empty={<CustomEmpty />}>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('Custom Empty');
it('should render custom empty component when data is empty', async () => {
const Dummy = () => null;
const CustomEmpty = () => <div>Custom Empty</div>;

const dataProvider = {
getList: jest.fn(() =>
Promise.resolve({
data: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
})
),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts" empty={<CustomEmpty />}>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('Custom Empty');
});
});
});

it('should not render an invite when a filter is active', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List resource="posts" filterDefaultValues={{ foo: 'bar' }}>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('dummy');
it('should not render an invite when a filter is active', async () => {
const Dummy = () => {
const { isPending } = useListContext();
return <div>{isPending ? 'loading' : 'dummy'}</div>;
};
const dataProvider = {
getList: jest.fn(() => Promise.resolve({ data: [], total: 0 })),
} as any;
render(
<CoreAdminContext dataProvider={dataProvider}>
<ThemeProvider theme={theme}>
<List
resource="posts"
filterDefaultValues={{ foo: 'bar' }}
>
<Dummy />
</List>
</ThemeProvider>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('resources.posts.empty')).toBeNull();
screen.getByText('dummy');
});
});
});

Expand Down
62 changes: 62 additions & 0 deletions packages/ra-ui-materialui/src/list/List.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Resource,
useListContext,
TestMemoryRouter,
DataProvider,
} from 'ra-core';
import fakeRestDataProvider from 'ra-data-fakerest';
import { Box, Card, Typography, Button, Link as MuiLink } from '@mui/material';
Expand Down Expand Up @@ -336,6 +337,39 @@ export const Component = () => (
</TestMemoryRouter>
);

export const PartialPagination = () => (
<TestMemoryRouter initialEntries={['/authors']}>
<Admin
dataProvider={
{
getList: async (_resource, _params) => ({
data: [
{
id: 1,
name: 'John Doe',
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
}),
} as DataProvider
}
>
<Resource
name="authors"
list={() => (
<List pagination={false}>
<SimpleList primaryText="%{name}" />
</List>
)}
create={() => <span />}
/>
</Admin>
</TestMemoryRouter>
);

export const Empty = () => (
<TestMemoryRouter initialEntries={['/authors']}>
<Admin dataProvider={dataProvider}>
Expand All @@ -352,6 +386,34 @@ export const Empty = () => (
</TestMemoryRouter>
);

export const EmptyPartialPagination = () => (
<TestMemoryRouter initialEntries={['/authors']}>
<Admin
dataProvider={
{
getList: async (_resource, _params) => ({
data: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
}),
} as unknown as DataProvider
}
>
<Resource
name="authors"
list={() => (
<List pagination={false}>
<SimpleList primaryText="%{name}" />
</List>
)}
create={() => <span />}
/>
</Admin>
</TestMemoryRouter>
);

export const SX = () => (
<TestMemoryRouter initialEntries={['/books']}>
<Admin dataProvider={dataProvider}>
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-ui-materialui/src/list/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,16 @@ export const ListView = <RecordType extends RaRecord = any>(
empty !== false && <div className={ListClasses.noResults}>{empty}</div>;

const shouldRenderEmptyPage =
!error &&
// the list is not loading data for the first time
!isPending &&
// the API returned no data (using either normal or partial pagination)
(total === 0 ||
(total == null &&
hasPreviousPage === false &&
hasNextPage === false)) &&
hasNextPage === false &&
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
data.length === 0)) &&
// the user didn't set any filters
!Object.keys(filterValues).length &&
// there is an empty page component
Expand Down

0 comments on commit 88f9f2a

Please sign in to comment.