Skip to content

Commit f801621

Browse files
committed
Add offline support to <ReferenceFieldBase> and <ReferenceField>
1 parent f23f8ef commit f801621

File tree

11 files changed

+355
-118
lines changed

11 files changed

+355
-118
lines changed

docs/ReferenceField.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
7979
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
8080
| `label` | Optional | `string | Function` | `resources. [resource]. fields.[source]` | Label to use for the field when rendered in layout components |
8181
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
82+
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
8283
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
8384
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
8485

@@ -98,6 +99,7 @@ By default, `<ReferenceField>` renders the `recordRepresentation` of the referen
9899
```
99100

100101
Alternatively, you can use [the `render` prop](#render) to render the referenced record in a custom way.
102+
101103
## `empty`
102104

103105
`<ReferenceField>` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
@@ -172,6 +174,32 @@ You can also use a custom `link` function to get a custom path for the children.
172174
/>
173175
```
174176

177+
## `offline`
178+
179+
`<ReferenceField>` can display a custom message when the referenced record is missing because there is no network connectivity, thanks to the `offline` prop.
180+
181+
```jsx
182+
<ReferenceField source="user_id" reference="users" offline="No network, could not fetch data" >
183+
...
184+
</ReferenceField>
185+
```
186+
187+
`<ReferenceField>` renders the `empty` element when:
188+
189+
- the referenced record is missing (no record in the `users` table with the right `user_id`), and
190+
- there is no network connectivity
191+
192+
You can pass either a React element or a string to the `offline` prop:
193+
194+
```jsx
195+
<ReferenceField source="user_id" reference="users" empty={<span>No network, could not fetch data</span>} >
196+
...
197+
</ReferenceField>
198+
<ReferenceField source="user_id" reference="users" empty="No network, could not fetch data" >
199+
...
200+
</ReferenceField>
201+
```
202+
175203
## `queryOptions`
176204

177205
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.

docs/ReferenceFieldBase.md

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
6868
| `children` | Optional | `ReactNode` | - | React component to render the referenced record. |
6969
| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and renders the referenced record. |
7070
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
71+
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
7172
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
7273
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
7374

@@ -100,35 +101,6 @@ export const MyReferenceField = () => (
100101
);
101102
```
102103

103-
## `render`
104-
105-
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
106-
107-
```jsx
108-
export const MyReferenceField = () => (
109-
<ReferenceFieldBase
110-
source="user_id"
111-
reference="users"
112-
render={({ error, isPending, referenceRecord }) => {
113-
if (isPending) {
114-
return <p>Loading...</p>;
115-
}
116-
117-
if (error) {
118-
return (
119-
<p className="error">
120-
{error.message}
121-
</p>
122-
);
123-
}
124-
return <p>{referenceRecord.name}</p>;
125-
}}
126-
/>
127-
);
128-
```
129-
130-
The `render` function prop will take priority on `children` props if both are set.
131-
132104
## `empty`
133105

134106
`<ReferenceFieldBase>` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
@@ -155,6 +127,32 @@ You can pass either a React element or a string to the `empty` prop:
155127
</ReferenceFieldBase>
156128
```
157129

130+
## `offline`
131+
132+
`<ReferenceFieldBase>` can display a custom message when the referenced record is missing because there is no network connectivity, thanks to the `offline` prop.
133+
134+
```jsx
135+
<ReferenceFieldBase source="user_id" reference="users" offline="No network, could not fetch data" >
136+
...
137+
</ReferenceFieldBase>
138+
```
139+
140+
`<ReferenceFieldBase>` renders the `empty` element when:
141+
142+
- the referenced record is missing (no record in the `users` table with the right `user_id`), and
143+
- there is no network connectivity
144+
145+
You can pass either a React element or a string to the `offline` prop:
146+
147+
```jsx
148+
<ReferenceFieldBase source="user_id" reference="users" empty={<span>No network, could not fetch data</span>} >
149+
...
150+
</ReferenceFieldBase>
151+
<ReferenceFieldBase source="user_id" reference="users" empty="No network, could not fetch data" >
152+
...
153+
</ReferenceFieldBase>
154+
```
155+
158156
## `queryOptions`
159157

160158
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
@@ -186,6 +184,36 @@ For instance, if the `posts` resource has a `user_id` field, set the `reference`
186184
</ReferenceFieldBase>
187185
```
188186

187+
188+
## `render`
189+
190+
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
191+
192+
```jsx
193+
export const MyReferenceField = () => (
194+
<ReferenceFieldBase
195+
source="user_id"
196+
reference="users"
197+
render={({ error, isPending, referenceRecord }) => {
198+
if (isPending) {
199+
return <p>Loading...</p>;
200+
}
201+
202+
if (error) {
203+
return (
204+
<p className="error">
205+
{error.message}
206+
</p>
207+
);
208+
}
209+
return <p>{referenceRecord.name}</p>;
210+
}}
211+
/>
212+
);
213+
```
214+
215+
The `render` function prop will take priority on `children` props if both are set.
216+
189217
## `sortBy`
190218

191219
By default, when used in a `<Datagrid>`, and when the user clicks on the column header of a `<ReferenceFieldBase>`, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, screen, waitFor } from '@testing-library/react';
3+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
44
import { CoreAdminContext } from '../../core/CoreAdminContext';
55
import { useResourceContext } from '../../core/useResourceContext';
66
import { testDataProvider } from '../../dataProvider';
@@ -10,8 +10,10 @@ import {
1010
Errored,
1111
Loading,
1212
Meta,
13+
Offline,
1314
WithRenderProp,
1415
} from './ReferenceFieldBase.stories';
16+
import { RecordContextProvider } from '../record';
1517

1618
describe('<ReferenceFieldBase />', () => {
1719
beforeAll(() => {
@@ -40,26 +42,26 @@ describe('<ReferenceFieldBase />', () => {
4042
});
4143
});
4244

43-
it('should pass the correct resource down to child component', async () => {
45+
it.only('should pass the correct resource down to child component', async () => {
4446
const MyComponent = () => {
4547
const resource = useResourceContext();
4648
return <div>{resource}</div>;
4749
};
4850
const dataProvider = testDataProvider({
49-
// @ts-ignore
50-
getList: () =>
51+
getMany: () =>
52+
// @ts-ignore
5153
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
5254
});
5355
render(
5456
<CoreAdminContext dataProvider={dataProvider}>
55-
<ReferenceFieldBase reference="posts" source="post_id">
56-
<MyComponent />
57-
</ReferenceFieldBase>
57+
<RecordContextProvider value={{ post_id: 1 }}>
58+
<ReferenceFieldBase reference="posts" source="post_id">
59+
<MyComponent />
60+
</ReferenceFieldBase>
61+
</RecordContextProvider>
5862
</CoreAdminContext>
5963
);
60-
await waitFor(() => {
61-
expect(screen.queryByText('posts')).not.toBeNull();
62-
});
64+
await screen.findByText('posts');
6365
});
6466

6567
it('should accept meta in queryOptions', async () => {
@@ -70,8 +72,8 @@ describe('<ReferenceFieldBase />', () => {
7072
);
7173
const dataProvider = testDataProvider({
7274
getMany,
73-
// @ts-ignore
7475
getOne: () =>
76+
// @ts-ignore
7577
Promise.resolve({
7678
data: {
7779
id: 1,
@@ -164,5 +166,14 @@ describe('<ReferenceFieldBase />', () => {
164166
expect(screen.queryByText('Leo')).not.toBeNull();
165167
});
166168
});
169+
170+
it('should render the offline prop node when offline', async () => {
171+
render(<Offline />);
172+
fireEvent.click(await screen.findByText('Simulate offline'));
173+
fireEvent.click(await screen.findByText('Toggle Child'));
174+
await screen.findByText('You are offline, cannot load data');
175+
fireEvent.click(await screen.findByText('Simulate online'));
176+
await screen.findByText('Leo');
177+
});
167178
});
168179
});

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

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { QueryClient } from '@tanstack/react-query';
2+
import { onlineManager, QueryClient } from '@tanstack/react-query';
33
import fakeRestDataProvider from 'ra-data-fakerest';
44
import { CoreAdmin } from '../../core/CoreAdmin';
55
import { Resource } from '../../core/Resource';
@@ -9,6 +9,7 @@ import { ReferenceFieldBase } from './ReferenceFieldBase';
99
import { useFieldValue } from '../../util/useFieldValue';
1010
import { useReferenceFieldContext } from './ReferenceFieldContext';
1111
import { DataProvider } from '../../types';
12+
import { useIsOffline } from '../../core/useIsOffline';
1213

1314
export default {
1415
title: 'ra-core/controller/field/ReferenceFieldBase',
@@ -395,6 +396,77 @@ export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
395396
</TestMemoryRouter>
396397
);
397398

399+
export const Offline = () => {
400+
return (
401+
<TestMemoryRouter initialEntries={['/books/1/show']}>
402+
<CoreAdmin
403+
dataProvider={dataProviderWithAuthors}
404+
queryClient={
405+
new QueryClient({
406+
defaultOptions: {
407+
queries: {
408+
retry: false,
409+
},
410+
},
411+
})
412+
}
413+
>
414+
<Resource name="authors" />
415+
<Resource
416+
name="books"
417+
show={
418+
<ShowBase>
419+
<div>
420+
<RenderChildOnDemand>
421+
<ReferenceFieldBase
422+
source="author"
423+
reference="authors"
424+
offline={
425+
<p>
426+
You are offline, cannot load
427+
data
428+
</p>
429+
}
430+
>
431+
<MyReferenceField>
432+
<TextField source="first_name" />
433+
</MyReferenceField>
434+
</ReferenceFieldBase>
435+
</RenderChildOnDemand>
436+
</div>
437+
<SimulateOfflineButton />
438+
</ShowBase>
439+
}
440+
/>
441+
</CoreAdmin>
442+
</TestMemoryRouter>
443+
);
444+
};
445+
446+
const SimulateOfflineButton = () => {
447+
const isOffline = useIsOffline();
448+
return (
449+
<button
450+
type="button"
451+
onClick={() => onlineManager.setOnline(isOffline)}
452+
>
453+
{isOffline ? 'Simulate online' : 'Simulate offline'}
454+
</button>
455+
);
456+
};
457+
458+
const RenderChildOnDemand = ({ children }) => {
459+
const [showChild, setShowChild] = React.useState(false);
460+
return (
461+
<>
462+
<button onClick={() => setShowChild(!showChild)}>
463+
Toggle Child
464+
</button>
465+
{showChild && <div>{children}</div>}
466+
</>
467+
);
468+
};
469+
398470
const MyReferenceField = (props: { children: React.ReactNode }) => {
399471
const context = useReferenceFieldContext();
400472

0 commit comments

Comments
 (0)