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
2 changes: 1 addition & 1 deletion docs/Edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ The default `onError` function is:

## `offline`

By default, `<EditBase>` renders nothing when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:
By default, `<Edit>` renders the `<Offline>` component when there is no connectivity and the record hasn't been cached yet. You can provide your own component via the `offline` prop:

```jsx
import { Edit } from 'react-admin';
Expand Down
46 changes: 44 additions & 2 deletions docs/ReferenceArrayField.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ You can change how the list of related records is rendered by passing a custom c
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `source` | Required | `string` | - | Name of the property to display |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'tags' |
| `children` | Optional&nbsp;* | `Element` | `<SingleFieldList>` | One or several elements that render a list of records based on a `ListContext` |
| `children` | Optional&nbsp;* | `ReactNode` | `<SingleFieldList>` | One or several elements that render a list of records based on a `ListContext` |
| `render` | Optional&nbsp;* | `(listContext) => Element` | `<SingleFieldList>` | A function that takes a list context and render a list of records |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `offline` | Optional | `ReactNode` | `<Offline variant="inline" />` | The component to render when there is no connectivity and the record isn't in the cache |
| `pagination` | Optional | `ReactNode` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
Expand Down Expand Up @@ -235,6 +236,47 @@ React-admin uses [the i18n system](./Translation.md) to translate the label, so
<ReferenceArrayField label="resource.posts.fields.tags" source="tag_ids" reference="tags" />
```

## `offline`

By default, `<ReferenceArrayField>` renders the `<Offline variant="inline">` when there is no connectivity and the records haven't been cached yet. You can provide your own component via the `offline` prop:

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

export const PostShow = () => (
<Show>
<ReferenceArrayField
source="tag_ids"
reference="tags"
offline={<Alert severity="warning">No network. Could not load the tags.</Alert>}
>
...
</ReferenceArrayField>
</Show>
);
```

**Tip**: If the records are 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 { IsOffline, ReferenceArrayField, Show } from 'react-admin';
import { Alert } from '@mui/material';

export const PostShow = () => (
<Show>
<ReferenceArrayField source="tag_ids" reference="tags">
<IsOffline>
<Alert severity="warning">
You are offline, tags may be outdated
</Alert>
</IsOffline>
...
</ReferenceArrayField>
</Show>
);
```

## `pagination`

`<ReferenceArrayField>` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, you can limit the number of displayed records with the [`perPage`](#perpage) prop. Then, let users display remaining records by rendering pagination controls. For that purpose, pass a pagination element to the `pagination` prop.
Expand Down
44 changes: 44 additions & 0 deletions docs/ReferenceArrayFieldBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ You can change how the list of related records is rendered by passing a custom c
| `children` | Optional\* | `Element` | | One or several elements that render a list of records based on a `ListContext` |
| `render` | Optional\* | `(ListContext) => Element` | | A function that takes a list context and renders a list of records |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache |
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
Expand Down Expand Up @@ -175,6 +176,49 @@ For instance, to render only tags that are 'published', you can use the followin
```
{% endraw %}

## `offline`

By default, `<ReferenceArrayFieldBase>` renders nothing when there is no connectivity and the records haven't been cached yet. You can provide your own component via the `offline` prop:

```jsx
import { ReferenceArrayFieldBase, ShowBase } from 'ra-core';

export const PostShow = () => (
<ShowBase>
<ReferenceArrayFieldBase
source="tag_ids"
reference="tags"
offline={<p>No network. Could not load the tags.</p>}
>
...
</ReferenceArrayFieldBase>
</ShowBase>
);
```

**Tip**: If the records are 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 { IsOffline, ReferenceArrayFieldBase, ShowBase } from 'ra-core';

export const PostShow = () => (
<ShowBase>
<ReferenceArrayFieldBase
source="tag_ids"
reference="tags"
offline={<p>No network. Could not load the tags.</p>}
>
<IsOffline>
<p>
You are offline, tags may be outdated
</p>
</IsOffline>
...
</ReferenceArrayFieldBase>
</ShowBase>
);
```

## `perPage`

`<ReferenceArrayFieldBase>` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, it may be a good idea to limit the number of displayed records. The `perPage` prop allows to create a client-side pagination for the related records.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
Basic,
Errored,
Loading,
Offline,
WithRenderProp,
} from './ReferenceArrayFieldBase.stories';

Expand Down Expand Up @@ -83,4 +84,18 @@ describe('ReferenceArrayFieldBase', () => {
expect(screen.queryByText('Charlie Watts')).not.toBeNull();
});
});

it('should render the offline prop node when offline', async () => {
render(<Offline />);
await screen.findByText('The Beatles');
fireEvent.click(await screen.findByText('Simulate offline'));
fireEvent.click(await screen.findByText('Toggle Child'));
await screen.findByText('You are offline, cannot load data');
fireEvent.click(await screen.findByText('Simulate online'));
await screen.findByText('John Lennon');
// Ensure the data is still displayed when going offline after it was loaded
fireEvent.click(await screen.findByText('Simulate offline'));
await screen.findByText('You are offline, the data may be outdated');
await screen.findByText('John Lennon');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { ReferenceArrayFieldBase } from './ReferenceArrayFieldBase';
import {
CoreAdmin,
DataProvider,
IsOffline,
Resource,
ShowBase,
TestMemoryRouter,
useIsOffline,
useListContext,
WithRecord,
} from '../..';
import { QueryClient } from '@tanstack/react-query';
import { onlineManager, QueryClient } from '@tanstack/react-query';

export default { title: 'ra-core/controller/field/ReferenceArrayFieldBase' };

Expand Down Expand Up @@ -154,3 +157,104 @@ export const WithRenderProp = ({
</CoreAdmin>
</TestMemoryRouter>
);

export const Offline = () => (
<TestMemoryRouter initialEntries={['/bands/1/show']}>
<CoreAdmin
dataProvider={defaultDataProvider}
queryClient={
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
}
>
<Resource
name="bands"
show={
<ShowBase>
<div>
<WithRecord render={band => <p>{band.name}</p>} />
<RenderChildOnDemand>
<ReferenceArrayFieldBase
source="members"
reference="artists"
offline={
<p style={{ color: 'orange' }}>
You are offline, cannot load data
</p>
}
render={({ data, isPending, error }) => {
if (isPending) {
return <p>Loading...</p>;
}

if (error) {
return (
<p style={{ color: 'red' }}>
{error.toString()}
</p>
);
}

return (
<>
<IsOffline>
<p
style={{
color: 'orange',
}}
>
You are offline, the
data may be outdated
</p>
</IsOffline>
<p>
{data?.map(
(datum, index) => (
<li key={index}>
{datum.name}
</li>
)
)}
</p>
</>
);
}}
/>
</RenderChildOnDemand>
</div>
<SimulateOfflineButton />
</ShowBase>
}
/>
</CoreAdmin>
</TestMemoryRouter>
);

const SimulateOfflineButton = () => {
const isOffline = useIsOffline();
return (
<button
type="button"
onClick={() => onlineManager.setOnline(isOffline)}
>
{isOffline ? 'Simulate online' : 'Simulate offline'}
</button>
);
};

const RenderChildOnDemand = ({ children }) => {
const [showChild, setShowChild] = React.useState(false);
return (
<>
<button onClick={() => setShowChild(!showChild)}>
Toggle Child
</button>
{showChild && <div>{children}</div>}
</>
);
};
61 changes: 34 additions & 27 deletions packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const ReferenceArrayFieldBase = <
loading,
empty,
filter,
offline,
page = 1,
perPage,
reference,
Expand Down Expand Up @@ -107,25 +108,27 @@ export const ReferenceArrayFieldBase = <
"<ReferenceArrayFieldBase> requires either a 'render' prop or 'children' prop"
);
}
const {
error: controllerError,
isPending,
isPaused,
isPlaceholderData,
} = controllerProps;

if (controllerProps.isPending && loading) {
return (
<ResourceContextProvider value={reference}>
{loading}
</ResourceContextProvider>
);
}
if (controllerProps.error && error) {
return (
<ResourceContextProvider value={reference}>
<ListContextProvider value={controllerProps}>
{error}
</ListContextProvider>
</ResourceContextProvider>
);
}
if (
// there is an empty page component
const shouldRenderLoading =
isPending && !isPaused && loading !== undefined && loading !== false;
const shouldRenderOffline =
isPaused &&
(isPending || isPlaceholderData) &&
offline !== undefined &&
offline !== false;
const shouldRenderError =
!isPending &&
!isPaused &&
controllerError &&
error !== undefined &&
error !== false;
const shouldRenderEmpty = // there is an empty page component
empty &&
// there is no error
!controllerProps.error &&
Expand All @@ -141,19 +144,22 @@ export const ReferenceArrayFieldBase = <
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
controllerProps.data.length === 0)) &&
// the user didn't set any filters
!Object.keys(controllerProps.filterValues).length
) {
return (
<ResourceContextProvider value={reference}>
{empty}
</ResourceContextProvider>
);
}
!Object.keys(controllerProps.filterValues).length;

return (
<ResourceContextProvider value={reference}>
<ListContextProvider value={controllerProps}>
{render ? render(controllerProps) : children}
{shouldRenderLoading
? loading
: shouldRenderOffline
? offline
: shouldRenderError
? error
: shouldRenderEmpty
? empty
: render
? render(controllerProps)
: children}
</ListContextProvider>
</ResourceContextProvider>
);
Expand All @@ -169,6 +175,7 @@ export interface ReferenceArrayFieldBaseProps<
loading?: ReactNode;
empty?: ReactNode;
filter?: FilterPayload;
offline?: ReactNode;
page?: number;
perPage?: number;
reference: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,5 @@ export const Basic = ({ children = defaultRenderProp }) => (
);

export default {
title: 'ra-core/controller/useReferenceArrayFieldController',
title: 'ra-core/controller/field/useReferenceArrayFieldController',
};
Loading
Loading