Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/InfiniteList.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ The props are the same as [the `<List>` component](./List.md):
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
| `filter` | Optional | `object` | - | The permanent filter values. |
| `filter DefaultValues` | Optional | `object` | - | The default filter values. |
| `offline` | Optional | `ReactNode` | `<Offline>` | The component to render when there is no connectivity and there is no data in the cache |
| `pagination` | Optional | `ReactElement` | `<Infinite Pagination>` | The pagination component to use. |
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
Expand All @@ -85,6 +86,49 @@ Check the [`<List>` component](./List.md) for details about each prop.

Additional props are passed down to the root component (a MUI `<Card>` by default).

## `offline`

By default, `<InfiniteList>` renders the `<Offline>` component when there is no connectivity and there are no records in the cache yet for the current parameters (page, sort, etc.). You can provide your own component via the `offline` prop:

```jsx
import { InfiniteList, InfinitePagination } from 'react-admin';
import { Alert } from '@mui/material';

const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;
// The offline component may be displayed at the bottom of the page if the network connectivity is lost
// when loading new pages. Make sure you pass your custom offline component here too
const pagination = <InfinitePagination offline={offline} />;

export const PostList = () => (
<InfiniteList offline={offline} pagination={pagination}>
...
</InfiniteList>
);
```

**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `<IsOffline>` component:

```jsx
import { InfiniteList, InfinitePagination, IsOffline } from 'react-admin';
import { Alert } from '@mui/material';

const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;
// The offline component may be displayed at the bottom of the page if the network connectivity is lost
// when loading new pages. Make sure you pass your custom offline component here too
const pagination = <InfinitePagination offline={offline} />;

export const PostList = () => (
<InfiniteList offline={offline} pagination={pagination}>
<IsOffline>
<Alert severity="warning">
You are offline, the data may be outdated
</Alert>
</IsOffline>
...
</InfiniteList>
);
```

## `pagination`

You can replace the default "load on scroll" pagination (triggered by a component named `<InfinitePagination>`) by a custom pagination component. To get the pagination state and callbacks, you'll need to read the `InfinitePaginationContext`.
Expand Down
38 changes: 38 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ You can find more advanced examples of `<List>` usage in the [demos](./Demos.md)
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
| `filter` | Optional | `object` | - | The permanent filter values. |
| `filter DefaultValues` | Optional | `object` | - | The default filter values. |
| `offline` | Optional | `ReactNode` | `<Offline>` | The component to render when there is no connectivity and there is no data in the cache |
| `pagination` | Optional | `ReactElement` | `<Pagination>` | The pagination component to use. |
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
Expand Down Expand Up @@ -774,6 +775,43 @@ export const PostList = () => (
const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filter };
```

## `offline`

By default, `<List>` renders the `<Offline>` component when there is no connectivity and there are no records in the cache yet for the current parameters (page, sort, etc.). You can provide your own component via the `offline` prop:

```jsx
import { List } from 'react-admin';
import { Alert } from '@mui/material';

const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;

export const PostList = () => (
<List offline={offline}>
...
</List>
);
```

**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `<IsOffline>` component:

```jsx
import { List, IsOffline } from 'react-admin';
import { Alert } from '@mui/material';

const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;

export const PostList = () => (
<List offline={offline}>
<IsOffline>
<Alert severity="warning">
You are offline, the data may be outdated
</Alert>
</IsOffline>
...
</List>
);
```

## `pagination`

By default, the `<List>` view displays a set of pagination controls at the bottom of the list.
Expand Down
1 change: 1 addition & 0 deletions docs/ListBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The `<ListBase>` component accepts the following props:
* [`exporter`](./List.md#exporter)
* [`filter`](./List.md#filter-permanent-filter)
* [`filterDefaultValues`](./List.md#filterdefaultvalues)
* [`offline`](./List.md#offline)
* [`perPage`](./List.md#perpage)
* [`queryOptions`](./List.md#queryoptions)
* `render`
Expand Down
23 changes: 23 additions & 0 deletions packages/ra-core/src/controller/list/InfiniteListBase.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ describe('InfiniteListBase', () => {
resolveAuth!();
await screen.findByText('Hello');
});
it('should not wait for the authentication resolution before loading data when disableAuthentication is true', async () => {
const authProvider = {
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
checkAuth: jest.fn(),
};
const dataProvider = testDataProvider({
// @ts-ignore
getList: jest.fn(() =>
Promise.resolve({ data: [{ id: 1, title: 'Hello' }], total: 1 })
),
});
render(
<WithAuthProviderNoAccessControl
authProvider={authProvider}
dataProvider={dataProvider}
InfiniteListProps={{ disableAuthentication: true }}
/>
);
await screen.findByText('Hello');
expect(authProvider.checkAuth).not.toHaveBeenCalled();
});
it('should wait for both the authentication and authorization resolution before loading data', async () => {
let resolveAuth: () => void;
let resolveCanAccess: (value: boolean) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import fakeRestProvider from 'ra-data-fakerest';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import { InfiniteListBase } from './InfiniteListBase';
import { InfiniteListBase, InfiniteListBaseProps } from './InfiniteListBase';
import { CoreAdminContext } from '../../core';
import { useListContext } from './useListContext';
import { useInfinitePaginationContext } from './useInfinitePaginationContext';
import {
AuthProvider,
DataProvider,
I18nProvider,
IsOffline,
mergeTranslations,
TestMemoryRouter,
useLocaleState,
} from '../..';
import { onlineManager } from '@tanstack/react-query';

export default {
title: 'ra-core/controller/list/InfiniteListBase',
Expand Down Expand Up @@ -140,15 +143,18 @@ export const WithAuthProviderNoAccessControl = ({
checkError: () => Promise.resolve(),
},
dataProvider = defaultDataProvider,
InfiniteListProps,
}: {
authProvider?: AuthProvider;
dataProvider?: DataProvider;
InfiniteListProps?: Partial<InfiniteListBaseProps>;
}) => (
<CoreAdminContext authProvider={authProvider} dataProvider={dataProvider}>
<InfiniteListBase
resource="books"
perPage={5}
loading={<div>Authentication loading...</div>}
{...InfiniteListProps}
>
<BookListView />
</InfiniteListBase>
Expand Down Expand Up @@ -314,6 +320,93 @@ export const WithRenderProps = () => (
</CoreAdminContext>
);

export const Offline = ({
dataProvider = defaultDataProvider,
isOnline = true,
...props
}: {
dataProvider?: DataProvider;
isOnline?: boolean;
} & Partial<InfiniteListBaseProps>) => {
React.useEffect(() => {
onlineManager.setOnline(isOnline);
}, [isOnline]);
return (
<TestMemoryRouter>
<CoreAdminContext dataProvider={dataProvider}>
<InfiniteListBase
resource="books"
perPage={5}
{...props}
offline={<p>You are offline, cannot load data</p>}
render={controllerProps => {
const {
data,
error,
isPending,
page,
perPage,
setPage,
total,
} = controllerProps;
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error...</div>;
}

return (
<div>
<p>
Use the story controls to simulate offline
mode:
</p>
<IsOffline>
<p style={{ color: 'orange' }}>
You are offline, the data may be
outdated
</p>
</IsOffline>
<button
disabled={page <= 1}
onClick={() => setPage(page - 1)}
>
previous
</button>
<span>
Page {page} of {Math.ceil(total / perPage)}
</span>
<button
disabled={page >= total / perPage}
onClick={() => setPage(page + 1)}
>
next
</button>
<ul>
{data.map((record: any) => (
<li key={record.id}>{record.title}</li>
))}
</ul>
</div>
);
}}
/>
</CoreAdminContext>
</TestMemoryRouter>
);
};

Offline.args = {
isOnline: true,
};

Offline.argTypes = {
isOnline: {
control: { type: 'boolean' },
},
};

const Title = () => {
const { defaultTitle } = useListContext();
const [locale, setLocale] = useLocaleState();
Expand Down
31 changes: 25 additions & 6 deletions packages/ra-core/src/controller/list/InfiniteListBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ import { useIsAuthPending } from '../../auth';
export const InfiniteListBase = <RecordType extends RaRecord = any>({
children,
render,
loading = null,
loading,
offline,
...props
}: InfiniteListBaseProps<RecordType>) => {
const controllerProps = useInfiniteListController<RecordType>(props);
Expand All @@ -57,16 +58,27 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
action: 'list',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

if (!render && !children) {
throw new Error(
"<InfiniteListBase> requires either a 'render' prop or 'children' prop"
);
}

const showLoading =
isAuthPending &&
!props.disableAuthentication &&
loading !== undefined &&
loading !== false;

const { isPaused, isPending, isPlaceholderData } = controllerProps;
const showOffline =
isPaused &&
// If isPending and isPaused are true, we are offline and couldn't even load the initial data
// If isPaused and isPlaceholderData are true, we are offline and couldn't even load data with different parameters on the same useQuery observer
(isPending || isPlaceholderData) &&
offline !== undefined &&
offline !== false;

return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
<OptionalResourceContextProvider value={props.resource}>
Expand All @@ -82,7 +94,13 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
controllerProps.isFetchingPreviousPage,
}}
>
{render ? render(controllerProps) : children}
{showLoading
? loading
: showOffline
? offline
: render
? render(controllerProps)
: children}
</InfinitePaginationContext.Provider>
</ListContextProvider>
</OptionalResourceContextProvider>
Expand All @@ -93,5 +111,6 @@ export interface InfiniteListBaseProps<RecordType extends RaRecord = any>
extends InfiniteListControllerProps<RecordType> {
loading?: ReactNode;
children?: ReactNode;
offline?: ReactNode;
render?: (props: InfiniteListControllerResult<RecordType>) => ReactNode;
}
Loading
Loading