Skip to content

Commit 92e7108

Browse files
authored
Merge pull request #10901 from marmelab/offline-support-reference-array-field
Add offline support to `<ReferenceArrayFieldBase>` and `<ReferenceArrayField>`
2 parents bad689e + 9c75e3e commit 92e7108

13 files changed

+423
-121
lines changed

docs/Edit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ The default `onError` function is:
542542

543543
## `offline`
544544

545-
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:
545+
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:
546546

547547
```jsx
548548
import { Edit } from 'react-admin';

docs/ReferenceArrayField.md

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

239+
## `offline`
240+
241+
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:
242+
243+
```jsx
244+
import { ReferenceArrayField, Show } from 'react-admin';
245+
import { Alert } from '@mui/material';
246+
247+
export const PostShow = () => (
248+
<Show>
249+
<ReferenceArrayField
250+
source="tag_ids"
251+
reference="tags"
252+
offline={<Alert severity="warning">No network. Could not load the tags.</Alert>}
253+
>
254+
...
255+
</ReferenceArrayField>
256+
</Show>
257+
);
258+
```
259+
260+
**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:
261+
262+
```jsx
263+
import { IsOffline, ReferenceArrayField, Show } from 'react-admin';
264+
import { Alert } from '@mui/material';
265+
266+
export const PostShow = () => (
267+
<Show>
268+
<ReferenceArrayField source="tag_ids" reference="tags">
269+
<IsOffline>
270+
<Alert severity="warning">
271+
You are offline, tags may be outdated
272+
</Alert>
273+
</IsOffline>
274+
...
275+
</ReferenceArrayField>
276+
</Show>
277+
);
278+
```
279+
238280
## `pagination`
239281

240282
`<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.

docs/ReferenceArrayFieldBase.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ You can change how the list of related records is rendered by passing a custom c
9292
| `children` | Optional\* | `Element` | | One or several elements that render a list of records based on a `ListContext` |
9393
| `render` | Optional\* | `(ListContext) => Element` | | A function that takes a list context and renders a list of records |
9494
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
95+
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache |
9596
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
9697
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
9798
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
@@ -175,6 +176,49 @@ For instance, to render only tags that are 'published', you can use the followin
175176
```
176177
{% endraw %}
177178
179+
## `offline`
180+
181+
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:
182+
183+
```jsx
184+
import { ReferenceArrayFieldBase, ShowBase } from 'ra-core';
185+
186+
export const PostShow = () => (
187+
<ShowBase>
188+
<ReferenceArrayFieldBase
189+
source="tag_ids"
190+
reference="tags"
191+
offline={<p>No network. Could not load the tags.</p>}
192+
>
193+
...
194+
</ReferenceArrayFieldBase>
195+
</ShowBase>
196+
);
197+
```
198+
199+
**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:
200+
201+
```jsx
202+
import { IsOffline, ReferenceArrayFieldBase, ShowBase } from 'ra-core';
203+
204+
export const PostShow = () => (
205+
<ShowBase>
206+
<ReferenceArrayFieldBase
207+
source="tag_ids"
208+
reference="tags"
209+
offline={<p>No network. Could not load the tags.</p>}
210+
>
211+
<IsOffline>
212+
<p>
213+
You are offline, tags may be outdated
214+
</p>
215+
</IsOffline>
216+
...
217+
</ReferenceArrayFieldBase>
218+
</ShowBase>
219+
);
220+
```
221+
178222
## `perPage`
179223
180224
`<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.

packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
2-
import { render, screen, waitFor } from '@testing-library/react';
2+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33
import {
44
Basic,
55
Errored,
66
Loading,
7+
Offline,
78
WithRenderProp,
89
} from './ReferenceArrayFieldBase.stories';
910

@@ -83,4 +84,18 @@ describe('ReferenceArrayFieldBase', () => {
8384
expect(screen.queryByText('Charlie Watts')).not.toBeNull();
8485
});
8586
});
87+
88+
it('should render the offline prop node when offline', async () => {
89+
render(<Offline />);
90+
await screen.findByText('The Beatles');
91+
fireEvent.click(await screen.findByText('Simulate offline'));
92+
fireEvent.click(await screen.findByText('Toggle Child'));
93+
await screen.findByText('You are offline, cannot load data');
94+
fireEvent.click(await screen.findByText('Simulate online'));
95+
await screen.findByText('John Lennon');
96+
// Ensure the data is still displayed when going offline after it was loaded
97+
fireEvent.click(await screen.findByText('Simulate offline'));
98+
await screen.findByText('You are offline, the data may be outdated');
99+
await screen.findByText('John Lennon');
100+
});
86101
});

packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import { ReferenceArrayFieldBase } from './ReferenceArrayFieldBase';
55
import {
66
CoreAdmin,
77
DataProvider,
8+
IsOffline,
89
Resource,
910
ShowBase,
1011
TestMemoryRouter,
12+
useIsOffline,
1113
useListContext,
14+
WithRecord,
1215
} from '../..';
13-
import { QueryClient } from '@tanstack/react-query';
16+
import { onlineManager, QueryClient } from '@tanstack/react-query';
1417

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

@@ -154,3 +157,104 @@ export const WithRenderProp = ({
154157
</CoreAdmin>
155158
</TestMemoryRouter>
156159
);
160+
161+
export const Offline = () => (
162+
<TestMemoryRouter initialEntries={['/bands/1/show']}>
163+
<CoreAdmin
164+
dataProvider={defaultDataProvider}
165+
queryClient={
166+
new QueryClient({
167+
defaultOptions: {
168+
queries: {
169+
retry: false,
170+
},
171+
},
172+
})
173+
}
174+
>
175+
<Resource
176+
name="bands"
177+
show={
178+
<ShowBase>
179+
<div>
180+
<WithRecord render={band => <p>{band.name}</p>} />
181+
<RenderChildOnDemand>
182+
<ReferenceArrayFieldBase
183+
source="members"
184+
reference="artists"
185+
offline={
186+
<p style={{ color: 'orange' }}>
187+
You are offline, cannot load data
188+
</p>
189+
}
190+
render={({ data, isPending, error }) => {
191+
if (isPending) {
192+
return <p>Loading...</p>;
193+
}
194+
195+
if (error) {
196+
return (
197+
<p style={{ color: 'red' }}>
198+
{error.toString()}
199+
</p>
200+
);
201+
}
202+
203+
return (
204+
<>
205+
<IsOffline>
206+
<p
207+
style={{
208+
color: 'orange',
209+
}}
210+
>
211+
You are offline, the
212+
data may be outdated
213+
</p>
214+
</IsOffline>
215+
<p>
216+
{data?.map(
217+
(datum, index) => (
218+
<li key={index}>
219+
{datum.name}
220+
</li>
221+
)
222+
)}
223+
</p>
224+
</>
225+
);
226+
}}
227+
/>
228+
</RenderChildOnDemand>
229+
</div>
230+
<SimulateOfflineButton />
231+
</ShowBase>
232+
}
233+
/>
234+
</CoreAdmin>
235+
</TestMemoryRouter>
236+
);
237+
238+
const SimulateOfflineButton = () => {
239+
const isOffline = useIsOffline();
240+
return (
241+
<button
242+
type="button"
243+
onClick={() => onlineManager.setOnline(isOffline)}
244+
>
245+
{isOffline ? 'Simulate online' : 'Simulate offline'}
246+
</button>
247+
);
248+
};
249+
250+
const RenderChildOnDemand = ({ children }) => {
251+
const [showChild, setShowChild] = React.useState(false);
252+
return (
253+
<>
254+
<button onClick={() => setShowChild(!showChild)}>
255+
Toggle Child
256+
</button>
257+
{showChild && <div>{children}</div>}
258+
</>
259+
);
260+
};

packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const ReferenceArrayFieldBase = <
7878
loading,
7979
empty,
8080
filter,
81+
offline,
8182
page = 1,
8283
perPage,
8384
reference,
@@ -107,25 +108,27 @@ export const ReferenceArrayFieldBase = <
107108
"<ReferenceArrayFieldBase> requires either a 'render' prop or 'children' prop"
108109
);
109110
}
111+
const {
112+
error: controllerError,
113+
isPending,
114+
isPaused,
115+
isPlaceholderData,
116+
} = controllerProps;
110117

111-
if (controllerProps.isPending && loading) {
112-
return (
113-
<ResourceContextProvider value={reference}>
114-
{loading}
115-
</ResourceContextProvider>
116-
);
117-
}
118-
if (controllerProps.error && error) {
119-
return (
120-
<ResourceContextProvider value={reference}>
121-
<ListContextProvider value={controllerProps}>
122-
{error}
123-
</ListContextProvider>
124-
</ResourceContextProvider>
125-
);
126-
}
127-
if (
128-
// there is an empty page component
118+
const shouldRenderLoading =
119+
isPending && !isPaused && loading !== undefined && loading !== false;
120+
const shouldRenderOffline =
121+
isPaused &&
122+
(isPending || isPlaceholderData) &&
123+
offline !== undefined &&
124+
offline !== false;
125+
const shouldRenderError =
126+
!isPending &&
127+
!isPaused &&
128+
controllerError &&
129+
error !== undefined &&
130+
error !== false;
131+
const shouldRenderEmpty = // there is an empty page component
129132
empty &&
130133
// there is no error
131134
!controllerProps.error &&
@@ -141,19 +144,22 @@ export const ReferenceArrayFieldBase = <
141144
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
142145
controllerProps.data.length === 0)) &&
143146
// the user didn't set any filters
144-
!Object.keys(controllerProps.filterValues).length
145-
) {
146-
return (
147-
<ResourceContextProvider value={reference}>
148-
{empty}
149-
</ResourceContextProvider>
150-
);
151-
}
147+
!Object.keys(controllerProps.filterValues).length;
152148

153149
return (
154150
<ResourceContextProvider value={reference}>
155151
<ListContextProvider value={controllerProps}>
156-
{render ? render(controllerProps) : children}
152+
{shouldRenderLoading
153+
? loading
154+
: shouldRenderOffline
155+
? offline
156+
: shouldRenderError
157+
? error
158+
: shouldRenderEmpty
159+
? empty
160+
: render
161+
? render(controllerProps)
162+
: children}
157163
</ListContextProvider>
158164
</ResourceContextProvider>
159165
);
@@ -169,6 +175,7 @@ export interface ReferenceArrayFieldBaseProps<
169175
loading?: ReactNode;
170176
empty?: ReactNode;
171177
filter?: FilterPayload;
178+
offline?: ReactNode;
172179
page?: number;
173180
perPage?: number;
174181
reference: string;

packages/ra-core/src/controller/field/useReferenceArrayFieldController.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,5 @@ export const Basic = ({ children = defaultRenderProp }) => (
9595
);
9696

9797
export default {
98-
title: 'ra-core/controller/useReferenceArrayFieldController',
98+
title: 'ra-core/controller/field/useReferenceArrayFieldController',
9999
};

0 commit comments

Comments
 (0)