Skip to content

Commit 29c4c07

Browse files
djhiMadeorsk
authored andcommitted
Add offline support to <ReferenceOneFieldBase> and <ReferenceOneField>
1 parent d416811 commit 29c4c07

File tree

8 files changed

+239
-63
lines changed

8 files changed

+239
-63
lines changed

docs/ReferenceOneField.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const BookShow = () => (
6464
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
6565
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
6666
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
67+
| `offline` | Optional | `ReactNode` | - | The text or element to display when there is no network connectivity |
6768
| `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
6869
| `sort` | Optional | `{ field: String, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'ASC' }` | Used to order referenced records |
6970

@@ -156,6 +157,30 @@ You can also set the `link` prop to a string, which will be used as the link typ
156157
</ReferenceOneField>
157158
```
158159

160+
161+
## `offline`
162+
163+
Use `offline` to customize the text displayed when the related record is empty because there is no network connectivity.
164+
165+
```jsx
166+
<ReferenceOneField label="Details" reference="book_details" target="book_id" offline="No network, could not fetch data">
167+
...
168+
</ReferenceOneField>
169+
```
170+
171+
`offline` also accepts a `ReactNode`.
172+
173+
```jsx
174+
<ReferenceOneField
175+
label="Details"
176+
reference="book_details"
177+
target="book_id"
178+
offline={<p>No network, could not fetch data</p>}
179+
>
180+
...
181+
</ReferenceOneField>
182+
```
183+
159184
## `queryOptions`
160185

161186
`<ReferenceOneField>` uses `react-query` to fetch the related record. You can set [any of `useQuery` options](https://tanstack.com/query/v5/docs/react/reference/useQuery) via the `queryOptions` prop.

docs/ReferenceOneFieldBase.md

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const BookDetails = () => {
7878
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
7979
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
8080
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
81+
| `offline` | Optional | `ReactNode` | - | The text or element to display when there is no network connectivity |
8182
| `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
8283
| `sort` | Optional | `{ field: String, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'ASC' }` | Used to order referenced records |
8384

@@ -122,39 +123,6 @@ const BookDetails = () => {
122123
}
123124
```
124125

125-
## `render`
126-
127-
Alternatively to children you can pass a `render` function prop to `<ReferenceOneFieldBase>`. The `render` function prop will receive the `ReferenceFieldContext` as its argument, allowing to inline the render logic.
128-
When receiving a `render` function prop the `<ReferenceOneFieldBase>` component will ignore the children property.
129-
130-
```jsx
131-
const BookShow = () => (
132-
<ReferenceOneFieldBase
133-
reference="book_details"
134-
target="book_id"
135-
render={({ isPending, error, referenceRecord }) => {
136-
if (isPending) {
137-
return <p>Loading...</p>;
138-
}
139-
140-
if (error) {
141-
return <p className="error" >{error.toString()}</p>;
142-
}
143-
144-
if (!referenceRecord) {
145-
return <p>No details found</p>;
146-
}
147-
return (
148-
<div>
149-
<p>{referenceRecord.genre}</p>
150-
<p>{referenceRecord.ISBN}</p>
151-
</div>
152-
);
153-
}}
154-
/>
155-
);
156-
```
157-
158126
## `empty`
159127

160128
Use `empty` to customize the text displayed when the related record is empty.
@@ -228,6 +196,29 @@ You can also set the `link` prop to a string, which will be used as the link typ
228196
```
229197
{% endraw %}
230198

199+
## `offline`
200+
201+
Use `offline` to customize the text displayed when the related record is empty because there is no network connectivity.
202+
203+
```jsx
204+
<ReferenceOneFieldBase label="Details" reference="book_details" target="book_id" offline="No network, could not fetch data">
205+
...
206+
</ReferenceOneFieldBase>
207+
```
208+
209+
`offline` also accepts a `ReactNode`.
210+
211+
```jsx
212+
<ReferenceOneFieldBase
213+
label="Details"
214+
reference="book_details"
215+
target="book_id"
216+
offline={<p>No network, could not fetch data</p>}
217+
>
218+
...
219+
</ReferenceOneFieldBase>
220+
```
221+
231222
## `queryOptions`
232223

233224
`<ReferenceOneFieldBase>` uses `react-query` to fetch the related record. You can set [any of `useQuery` options](https://tanstack.com/query/v5/docs/react/reference/useQuery) via the `queryOptions` prop.
@@ -247,6 +238,39 @@ For instance, if you want to disable the refetch on window focus for this query,
247238
```
248239
{% endraw %}
249240

241+
## `render`
242+
243+
Alternatively to children you can pass a `render` function prop to `<ReferenceOneFieldBase>`. The `render` function prop will receive the `ReferenceFieldContext` as its argument, allowing to inline the render logic.
244+
When receiving a `render` function prop the `<ReferenceOneFieldBase>` component will ignore the children property.
245+
246+
```jsx
247+
const BookShow = () => (
248+
<ReferenceOneFieldBase
249+
reference="book_details"
250+
target="book_id"
251+
render={({ isPending, error, referenceRecord }) => {
252+
if (isPending) {
253+
return <p>Loading...</p>;
254+
}
255+
256+
if (error) {
257+
return <p className="error" >{error.toString()}</p>;
258+
}
259+
260+
if (!referenceRecord) {
261+
return <p>No details found</p>;
262+
}
263+
return (
264+
<div>
265+
<p>{referenceRecord.genre}</p>
266+
<p>{referenceRecord.ISBN}</p>
267+
</div>
268+
);
269+
}}
270+
/>
271+
);
272+
```
273+
250274
## `reference`
251275

252276
The name of the resource to fetch for the related records.

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
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
Loading,
6+
Offline,
67
WithRenderProp,
78
} from './ReferenceOneFieldBase.stories';
89

@@ -61,4 +62,13 @@ describe('ReferenceOneFieldBase', () => {
6162
});
6263
});
6364
});
65+
66+
it('should render the offline prop node when offline', async () => {
67+
render(<Offline />);
68+
fireEvent.click(await screen.findByText('Simulate offline'));
69+
fireEvent.click(await screen.findByText('Toggle Child'));
70+
await screen.findByText('Offline');
71+
fireEvent.click(await screen.findByText('Simulate online'));
72+
await screen.findByText('9780393966473');
73+
});
6474
});

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
ReferenceOneFieldBase,
77
ResourceContextProvider,
88
TestMemoryRouter,
9+
useIsOffline,
910
useReferenceFieldContext,
1011
} from '../..';
12+
import { onlineManager } from '@tanstack/react-query';
1113

1214
export default { title: 'ra-core/controller/field/ReferenceOneFieldBase' };
1315

@@ -100,3 +102,46 @@ export const WithRenderProp = ({
100102
</Wrapper>
101103
);
102104
};
105+
106+
export const Offline = () => {
107+
return (
108+
<Wrapper>
109+
<div>
110+
<RenderChildOnDemand>
111+
<ReferenceOneFieldBase
112+
reference="book_details"
113+
target="book_id"
114+
offline={<span>Offline</span>}
115+
>
116+
<BookDetails />
117+
</ReferenceOneFieldBase>
118+
</RenderChildOnDemand>
119+
</div>
120+
<SimulateOfflineButton />
121+
</Wrapper>
122+
);
123+
};
124+
125+
const SimulateOfflineButton = () => {
126+
const isOffline = useIsOffline();
127+
return (
128+
<button
129+
type="button"
130+
onClick={() => onlineManager.setOnline(isOffline)}
131+
>
132+
{isOffline ? 'Simulate online' : 'Simulate offline'}
133+
</button>
134+
);
135+
};
136+
137+
const RenderChildOnDemand = ({ children }) => {
138+
const [showChild, setShowChild] = React.useState(false);
139+
return (
140+
<>
141+
<button onClick={() => setShowChild(!showChild)}>
142+
Toggle Child
143+
</button>
144+
{showChild && <div>{children}</div>}
145+
</>
146+
);
147+
};

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const ReferenceOneFieldBase = <
4141
sort,
4242
filter,
4343
link,
44+
offline,
4445
queryOptions,
4546
} = props;
4647

@@ -78,38 +79,46 @@ export const ReferenceOneFieldBase = <
7879
}
7980

8081
const recordFromContext = useRecordContext<RecordType>(props);
81-
if (controllerProps.isPending && loading) {
82-
return (
83-
<ResourceContextProvider value={reference}>
84-
{loading}
85-
</ResourceContextProvider>
86-
);
87-
}
88-
if (controllerProps.error && error) {
89-
return (
90-
<ResourceContextProvider value={reference}>
91-
<ReferenceFieldContextProvider value={context}>
92-
{error}
93-
</ReferenceFieldContextProvider>
94-
</ResourceContextProvider>
95-
);
96-
}
97-
if (
98-
!recordFromContext ||
99-
(!controllerProps.isPending && controllerProps.referenceRecord == null)
100-
) {
101-
return (
102-
<ResourceContextProvider value={reference}>
103-
{empty}
104-
</ResourceContextProvider>
105-
);
106-
}
82+
const {
83+
error: controllerError,
84+
isPending,
85+
isPaused,
86+
referenceRecord,
87+
} = controllerProps;
88+
89+
const shouldRenderLoading =
90+
!isPaused && isPending && loading !== false && loading !== undefined;
91+
const shouldRenderOffline =
92+
isPaused &&
93+
!referenceRecord &&
94+
offline !== false &&
95+
offline !== undefined;
96+
const shouldRenderError =
97+
!!controllerError && error !== false && error !== undefined;
98+
const shouldRenderEmpty =
99+
(!recordFromContext ||
100+
(!isPaused &&
101+
referenceRecord == null &&
102+
!controllerError &&
103+
!isPending)) &&
104+
empty !== false &&
105+
empty !== undefined;
107106

108107
return (
109108
<ResourceContextProvider value={reference}>
110109
<ReferenceFieldContextProvider value={context}>
111-
<RecordContextProvider value={context.referenceRecord}>
112-
{render ? render(controllerProps) : children}
110+
<RecordContextProvider value={referenceRecord}>
111+
{shouldRenderLoading
112+
? loading
113+
: shouldRenderOffline
114+
? offline
115+
: shouldRenderError
116+
? error
117+
: shouldRenderEmpty
118+
? empty
119+
: render
120+
? render(controllerProps)
121+
: children}
113122
</RecordContextProvider>
114123
</ReferenceFieldContextProvider>
115124
</ResourceContextProvider>
@@ -127,6 +136,7 @@ export interface ReferenceOneFieldBaseProps<
127136
loading?: ReactNode;
128137
error?: ReactNode;
129138
empty?: ReactNode;
139+
offline?: ReactNode;
130140
render?: (props: UseReferenceResult<ReferenceRecordType>) => ReactNode;
131141
link?: LinkToType<ReferenceRecordType>;
132142
resource?: string;

packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Empty,
1111
Themed,
1212
WithRenderProp,
13+
Offline,
1314
} from './ReferenceOneField.stories';
1415

1516
describe('ReferenceOneField', () => {
@@ -86,4 +87,13 @@ describe('ReferenceOneField', () => {
8687
render(<Themed />);
8788
expect(await screen.findByTestId('themed')).toBeDefined();
8889
});
90+
91+
it('should render the offline prop node when offline', async () => {
92+
render(<Offline />);
93+
fireEvent.click(await screen.findByText('Simulate offline'));
94+
fireEvent.click(await screen.findByText('Toggle Child'));
95+
await screen.findByText('No connectivity. Could not fetch data.');
96+
fireEvent.click(await screen.findByText('Simulate online'));
97+
await screen.findByText('9780393966473');
98+
});
8999
});

0 commit comments

Comments
 (0)