Skip to content

Commit 9789289

Browse files
authored
Merge pull request #10852 from marmelab/offline-support-show-view
Add offline support to `<ShowBase>` and `<Show>`
2 parents 4772050 + e82301b commit 9789289

File tree

24 files changed

+656
-60
lines changed

24 files changed

+656
-60
lines changed

docs/Show.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ That's enough to display the post show view above.
7070
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
7171
| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the show is loading
7272
| `id` | Optional | `string | number` | | The record id. If not provided, it will be deduced from the URL
73+
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
7374
| `queryOptions` | Optional | `object` | | The options to pass to the `useQuery` hook
7475
| `resource` | Optional | `string` | | The resource name, e.g. `posts`
7576
| `sx` | Optional | `object` | | Override or extend the styles applied to the component
@@ -83,7 +84,7 @@ By default, `<Show>` includes an action toolbar with an `<EditButton>` if the `<
8384

8485
```jsx
8586
import Button from '@mui/material/Button';
86-
import { EditButton, TopToolbar } from 'react-admin';
87+
import { EditButton, Show, TopToolbar } from 'react-admin';
8788

8889
const PostShowActions = () => (
8990
<TopToolbar>
@@ -158,6 +159,8 @@ React-admin provides 2 built-in show layout components:
158159
To use an alternative layout, switch the `<Show>` child component:
159160

160161
```diff
162+
import { Show } from 'react-admin';
163+
161164
export const PostShow = () => (
162165
<Show>
163166
- <SimpleShowLayout>
@@ -188,6 +191,7 @@ You can override the main area container by passing a `component` prop:
188191

189192
{% raw %}
190193
```jsx
194+
import { Show } from 'react-admin';
191195
import { Box } from '@mui/material';
192196

193197
const ShowWrapper = ({ children }) => (
@@ -210,6 +214,8 @@ const PostShow = () => (
210214
By default, the `<Show>` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
211215

212216
```jsx
217+
import { Show } from 'react-admin';
218+
213219
const PostShow = () => (
214220
<Show disableAuthentication>
215221
...
@@ -273,6 +279,8 @@ const BookShow = () => (
273279
By default, `<Show>` deduces the identifier of the record to show from the URL path. So under the `/posts/123/show` path, the `id` prop will be `123`. You may want to force a different identifier. In this case, pass a custom `id` prop.
274280

275281
```jsx
282+
import { Show } from 'react-admin';
283+
276284
export const PostShow = () => (
277285
<Show id="123">
278286
...
@@ -282,6 +290,38 @@ export const PostShow = () => (
282290

283291
**Tip**: Pass both a custom `id` and a custom `resource` prop to use `<Show>` independently of the current URL. This even allows you to use more than one `<Show>` component in the same page.
284292

293+
## `offline`
294+
295+
By default, `<Show>` 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:
296+
297+
```jsx
298+
import { Show } from 'react-admin';
299+
300+
export const PostShow = () => (
301+
<Show offline={<p>No network. Could not load the post.</p>}>
302+
...
303+
</Show>
304+
);
305+
```
306+
307+
**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:
308+
309+
```jsx
310+
import { Show, IsOffline } from 'react-admin';
311+
import { Alert } from '@mui/material';
312+
313+
export const PostShow = () => (
314+
<Show offline={<p>No network. Could not load the post.</p>}>
315+
<IsOffline>
316+
<Alert severity="warning">
317+
You are offline, the data may be outdated
318+
</Alert>
319+
</IsOffline>
320+
...
321+
</Show>
322+
);
323+
```
324+
285325
## `queryOptions`
286326

287327
`<Show>` accepts a `queryOptions` prop to pass options to the react-query client.
@@ -372,6 +412,8 @@ export const PostShow = () => (
372412
By default, `<Show>` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
373413

374414
```jsx
415+
import { Show } from 'react-admin';
416+
375417
export const UsersShow = () => (
376418
<Show resource="users">
377419
...

docs/ShowBase.md

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ const App = () => (
6363
| Prop | Required | Type | Default | Description
6464
|------------------|----------|-------------------|---------|--------------------------------------------------------
6565
| `children` | Optional | `ReactNode` | | The components rendering the record fields
66-
| `render` | Optional | `(props: ShowControllerResult<RecordType>) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
66+
| `render` | Optional | `(props: ShowControllerResult<RecordType>) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
6767
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
68-
| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading
6968
| `id` | Optional | `string` | | The record identifier. If not provided, it will be deduced from the URL
69+
| `loading` | Optional | `ReactNode` | | The component to render while checking for authentication and permissions
70+
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
7071
| `queryOptions` | Optional | `object` | | The options to pass to the `useQuery` hook
7172
| `resource` | Optional | `string` | | The resource name, e.g. `posts`
7273

@@ -107,38 +108,13 @@ const BookShow = () => (
107108
```
108109
{% endraw %}
109110

110-
## `render`
111-
112-
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ShowContext` as argument.
113-
114-
{% raw %}
115-
```jsx
116-
import { ShowBase, TextField, DateField, ReferenceField, WithRecord } from 'react-admin';
117-
118-
const BookShow = () => (
119-
<ShowBase render={({ isPending, error, record }) => {
120-
if (isPending) {
121-
return <p>Loading...</p>;
122-
}
123-
124-
if (error) {
125-
return (
126-
<p className="error">
127-
{error.message}
128-
</p>
129-
);
130-
}
131-
return <p>{record.title}</p>;
132-
}}/>
133-
);
134-
```
135-
{% endraw %}
136-
137111
## `disableAuthentication`
138112

139113
By default, the `<ShowBase>` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
140114

141115
```jsx
116+
import { ShowBase } from 'react-admin';
117+
142118
const PostShow = () => (
143119
<ShowBase disableAuthentication>
144120
...
@@ -151,6 +127,8 @@ const PostShow = () => (
151127
By default, `<ShowBase>` deduces the identifier of the record to show from the URL path. So under the `/posts/123/show` path, the `id` prop will be `123`. You may want to force a different identifier. In this case, pass a custom `id` prop.
152128

153129
```jsx
130+
import { ShowBase } from 'react-admin';
131+
154132
export const PostShow = () => (
155133
<ShowBase id="123">
156134
...
@@ -160,6 +138,49 @@ export const PostShow = () => (
160138

161139
**Tip**: Pass both a custom `id` and a custom `resource` prop to use `<ShowBase>` independently of the current URL. This even allows you to use more than one `<ShowBase>` component in the same page.
162140

141+
## `loading`
142+
143+
By default, `<ShowBase>` renders nothing while checking for authentication and permissions. You can provide your own component via the `loading` prop:
144+
145+
```jsx
146+
import { ShowBase } from 'react-admin';
147+
148+
export const PostShow = () => (
149+
<ShowBase loading={<p>Checking for permissions...</p>}>
150+
...
151+
</ShowBase>
152+
);
153+
```
154+
155+
## `offline`
156+
157+
By default, `<ShowBase>` 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:
158+
159+
```jsx
160+
import { ShowBase } from 'react-admin';
161+
162+
export const PostShow = () => (
163+
<ShowBase offline={<p>No network. Could not load the post.</p>}>
164+
...
165+
</ShowBase>
166+
);
167+
```
168+
169+
**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:
170+
171+
```jsx
172+
import { ShowBase, IsOffline } from 'react-admin';
173+
174+
export const PostShow = () => (
175+
<ShowBase offline={<p>No network. Could not load the post.</p>}>
176+
<IsOffline>
177+
No network. The post data may be outdated.
178+
</IsOffline>
179+
...
180+
</ShowBase>
181+
);
182+
```
183+
163184
## `queryOptions`
164185

165186
`<ShowBase>` accepts a `queryOptions` prop to pass options to the react-query client.
@@ -205,11 +226,40 @@ The default `onError` function is:
205226
}
206227
```
207228

229+
## `render`
230+
231+
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ShowContext` as argument.
232+
233+
{% raw %}
234+
```jsx
235+
import { ShowBase, TextField, DateField, ReferenceField, WithRecord } from 'react-admin';
236+
237+
const BookShow = () => (
238+
<ShowBase render={({ isPending, error, record }) => {
239+
if (isPending) {
240+
return <p>Loading...</p>;
241+
}
242+
243+
if (error) {
244+
return (
245+
<p className="error">
246+
{error.message}
247+
</p>
248+
);
249+
}
250+
return <p>{record.title}</p>;
251+
}}/>
252+
);
253+
```
254+
{% endraw %}
255+
208256
## `resource`
209257

210258
By default, `<ShowBase>` operates on the current `ResourceContext` (defined at the routing level), so under the `/posts/1/show` path, the `resource` prop will be `posts`. You may want to force a different resource. In this case, pass a custom `resource` prop, and it will override the `ResourceContext` value.
211259

212260
```jsx
261+
import { ShowBase } from 'react-admin';
262+
213263
export const UsersShow = () => (
214264
<ShowBase resource="users">
215265
...

packages/ra-core/src/controller/show/ShowBase.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
AccessControl,
88
DefaultTitle,
99
NoAuthProvider,
10+
Offline,
1011
WithAuthProviderNoAccessControl,
1112
WithRenderProp,
1213
} from './ShowBase.stories';
@@ -118,4 +119,16 @@ describe('ShowBase', () => {
118119
expect(dataProvider.getOne).toHaveBeenCalled();
119120
await screen.findByText('Hello');
120121
});
122+
123+
it('should render the offline prop node when offline', async () => {
124+
const { rerender } = render(<Offline isOnline={false} />);
125+
await screen.findByText('You are offline, cannot load data');
126+
rerender(<Offline isOnline={true} />);
127+
await screen.findByText('Hello');
128+
expect(
129+
screen.queryByText('You are offline, cannot load data')
130+
).toBeNull();
131+
rerender(<Offline isOnline={false} />);
132+
await screen.findByText('You are offline, the data may be outdated');
133+
});
121134
});

packages/ra-core/src/controller/show/ShowBase.stories.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ import * as React from 'react';
22
import englishMessages from 'ra-language-english';
33
import frenchMessages from 'ra-language-french';
44
import polyglotI18nProvider from 'ra-i18n-polyglot';
5+
import fakeRestDataProvider from 'ra-data-fakerest';
56
import {
67
AuthProvider,
78
CoreAdminContext,
89
ShowBase,
910
ShowBaseProps,
1011
DataProvider,
11-
testDataProvider,
12-
useRecordContext,
1312
mergeTranslations,
1413
I18nProvider,
1514
useShowContext,
1615
useLocaleState,
16+
IsOffline,
17+
WithRecord,
1718
} from '../..';
19+
import { onlineManager } from '@tanstack/react-query';
1820

1921
export default {
2022
title: 'ra-core/controller/ShowBase',
@@ -162,21 +164,72 @@ export const WithRenderProp = ({
162164
</CoreAdminContext>
163165
);
164166

165-
const defaultDataProvider = testDataProvider({
166-
getOne: () =>
167-
// @ts-ignore
168-
Promise.resolve({ data: { id: 12, test: 'Hello', title: 'Hello' } }),
169-
});
167+
export const Offline = ({
168+
dataProvider = defaultDataProvider,
169+
isOnline = true,
170+
...props
171+
}: {
172+
dataProvider?: DataProvider;
173+
isOnline?: boolean;
174+
} & Partial<ShowBaseProps>) => {
175+
React.useEffect(() => {
176+
onlineManager.setOnline(isOnline);
177+
}, [isOnline]);
178+
return (
179+
<CoreAdminContext dataProvider={dataProvider}>
180+
<ShowBase
181+
{...defaultProps}
182+
{...props}
183+
offline={<p>You are offline, cannot load data</p>}
184+
>
185+
<OfflineChild />
186+
</ShowBase>
187+
</CoreAdminContext>
188+
);
189+
};
190+
191+
Offline.args = {
192+
isOnline: true,
193+
};
194+
195+
Offline.argTypes = {
196+
isOnline: {
197+
control: { type: 'boolean' },
198+
},
199+
};
200+
201+
const defaultDataProvider = fakeRestDataProvider(
202+
{
203+
posts: [
204+
{ id: 12, test: 'Hello', title: 'Hello' },
205+
{ id: 13, test: 'World', title: 'World' },
206+
],
207+
},
208+
process.env.NODE_ENV !== 'test',
209+
process.env.NODE_ENV !== 'test' ? 300 : 0
210+
);
170211

171212
const defaultProps = {
172213
id: 12,
173214
resource: 'posts',
174215
};
175216

176217
const Child = () => {
177-
const record = useRecordContext();
218+
return <WithRecord render={record => <p>{record?.test}</p>} />;
219+
};
178220

179-
return <p>{record?.test}</p>;
221+
const OfflineChild = () => {
222+
return (
223+
<>
224+
<p>Use the story controls to simulate offline mode:</p>
225+
<IsOffline>
226+
<p style={{ color: 'orange' }}>
227+
You are offline, the data may be outdated
228+
</p>
229+
</IsOffline>
230+
<WithRecord render={record => <p>{record?.test}</p>} />
231+
</>
232+
);
180233
};
181234

182235
const Title = () => {

0 commit comments

Comments
 (0)