Skip to content

Commit 34804e1

Browse files
authored
Merge pull request #10902 from marmelab/offline-support-reference-many-field
Add offline support to `<ReferenceManyFieldBase>` and `<ReferenceManyField>`
2 parents daca68b + aed19c8 commit 34804e1

File tree

5 files changed

+207
-16
lines changed

5 files changed

+207
-16
lines changed

docs/ReferenceManyField.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ This example leverages [`<SingleFieldList>`](./SingleFieldList.md) to display an
9696
| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
9797
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
9898
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
99+
| `offline` | Optional | `ReactNode` | - | Element to display when there are no related records because of lack of network connectivity. |
99100
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
100101
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
101102
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v3/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
@@ -307,6 +308,49 @@ React-admin uses [the i18n system](./Translation.md) to translate the label, so
307308
</ReferenceManyField>
308309
```
309310

311+
## `offline`
312+
313+
By default, `<ReferenceManyField>` 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:
314+
315+
```jsx
316+
<ReferenceManyField
317+
reference="books"
318+
target="author_id"
319+
offline="Offline, could not load data"
320+
>
321+
...
322+
</ReferenceManyField>
323+
```
324+
325+
`offline` also accepts a `ReactNode`.
326+
327+
```jsx
328+
<ReferenceManyField
329+
reference="books"
330+
target="author_id"
331+
empty={<Alert severity="warning">Offline, could not load data</Alert>}
332+
>
333+
...
334+
</ReferenceManyField>
335+
```
336+
337+
**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:
338+
339+
```jsx
340+
<ReferenceManyField
341+
reference="books"
342+
target="author_id"
343+
empty={<Alert severity="warning">Offline, could not load data</Alert>}
344+
>
345+
<IsOffline>
346+
<Alert severity="warning">
347+
You are offline, the data may be outdated
348+
</Alert>
349+
</IsOffline>
350+
...
351+
</ReferenceManyField>
352+
```
353+
310354
## `pagination`
311355

312356
If you want to allow users to paginate the list, pass a `<Pagination>` element as the `pagination` prop:

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,20 @@ export const ReferenceManyFieldBase = <
113113
hasPreviousPage,
114114
isPaused,
115115
isPending,
116+
isPlaceholderData,
116117
total,
117118
} = controllerProps;
118119

119-
const showLoading =
120+
const shouldRenderLoading =
120121
isPending && !isPaused && loading !== false && loading !== undefined;
121-
const showOffline = isPaused && offline !== false && offline !== undefined;
122-
const showError = controllerError && error !== false && error !== undefined;
123-
const showEmpty =
122+
const shouldRenderOffline =
123+
isPaused &&
124+
(isPending || isPlaceholderData) &&
125+
offline !== false &&
126+
offline !== undefined;
127+
const shouldRenderError =
128+
controllerError && error !== false && error !== undefined;
129+
const shouldRenderEmpty =
124130
empty !== false &&
125131
empty !== undefined &&
126132
// there is no error
@@ -142,13 +148,13 @@ export const ReferenceManyFieldBase = <
142148
return (
143149
<ResourceContextProvider value={reference}>
144150
<ListContextProvider value={controllerProps}>
145-
{showLoading
151+
{shouldRenderLoading
146152
? loading
147-
: showOffline
153+
: shouldRenderOffline
148154
? offline
149-
: showError
155+
: shouldRenderError
150156
? error
151-
: showEmpty
157+
: shouldRenderEmpty
152158
? empty
153159
: render
154160
? render(controllerProps)

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { Pagination } from '../list/pagination/Pagination';
1212
import {
1313
Basic,
1414
Empty,
15+
Offline,
1516
WithPagination,
1617
WithPaginationAndSelectAllLimit,
1718
WithRenderProp,
1819
} from './ReferenceManyField.stories';
20+
import { Alert } from '@mui/material';
1921

2022
const theme = createTheme();
2123

@@ -207,7 +209,7 @@ describe('<ReferenceManyField />', () => {
207209
});
208210
});
209211

210-
it('should use render prop when provides', async () => {
212+
it('should use render prop when provided', async () => {
211213
render(<WithRenderProp />);
212214
await waitFor(() => {
213215
expect(screen.queryAllByRole('progressbar')).toHaveLength(0);
@@ -437,4 +439,41 @@ describe('<ReferenceManyField />', () => {
437439
);
438440
});
439441
});
442+
it('should render the default offline component node when offline', async () => {
443+
render(<Offline />);
444+
fireEvent.click(await screen.findByText('Simulate offline'));
445+
fireEvent.click(await screen.findByText('Toggle Child'));
446+
await screen.findByText('No connectivity. Could not fetch data.');
447+
fireEvent.click(await screen.findByText('Simulate online'));
448+
await screen.findByText("Harry Potter and the Philosopher's Stone");
449+
expect(
450+
screen.queryByText('No connectivity. Could not fetch data.')
451+
).toBeNull();
452+
fireEvent.click(await screen.findByText('Simulate offline'));
453+
await screen.findByText('You are offline, the data may be outdated');
454+
fireEvent.click(screen.getByLabelText('Go to page 2'));
455+
await screen.findByText('No connectivity. Could not fetch data.');
456+
fireEvent.click(screen.getByLabelText('Go to page 1'));
457+
await screen.findByText("Harry Potter and the Philosopher's Stone");
458+
fireEvent.click(await screen.findByText('Simulate online'));
459+
});
460+
it('should render the custom offline component node when offline', async () => {
461+
const CustomOffline = () => {
462+
return <Alert severity="warning">You are offline!</Alert>;
463+
};
464+
render(<Offline offline={<CustomOffline />} />);
465+
fireEvent.click(await screen.findByText('Simulate offline'));
466+
fireEvent.click(await screen.findByText('Toggle Child'));
467+
await screen.findByText('You are offline!');
468+
fireEvent.click(await screen.findByText('Simulate online'));
469+
await screen.findByText("Harry Potter and the Philosopher's Stone");
470+
expect(screen.queryByText('You are offline!')).toBeNull();
471+
fireEvent.click(await screen.findByText('Simulate offline'));
472+
await screen.findByText('You are offline, the data may be outdated');
473+
fireEvent.click(screen.getByLabelText('Go to page 2'));
474+
await screen.findByText('You are offline!');
475+
fireEvent.click(screen.getByLabelText('Go to page 1'));
476+
await screen.findByText("Harry Potter and the Philosopher's Stone");
477+
fireEvent.click(await screen.findByText('Simulate online'));
478+
});
440479
});

packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {
55
RecordContextProvider,
66
ResourceContextProvider,
77
TestMemoryRouter,
8+
useIsOffline,
9+
IsOffline,
810
} from 'ra-core';
911
import { Admin, ListGuesser, Resource } from 'react-admin';
1012
import type { AdminProps } from 'react-admin';
11-
import { ThemeProvider, Box, Stack } from '@mui/material';
13+
import { Alert, ThemeProvider, Box, Stack } from '@mui/material';
1214
import { createTheme } from '@mui/material/styles';
1315
import fakeDataProvider from 'ra-data-fakerest';
1416
import polyglotI18nProvider from 'ra-i18n-polyglot';
@@ -29,6 +31,7 @@ import { TextInput } from '../input';
2931
import { Edit } from '../detail';
3032
import { SimpleForm } from '../form';
3133
import { SelectAllButton, BulkDeleteButton } from '../button';
34+
import { onlineManager } from '@tanstack/react-query';
3235

3336
export default { title: 'ra-ui-materialui/fields/ReferenceManyField' };
3437

@@ -313,3 +316,75 @@ export const WithRenderProp = () => (
313316
/>
314317
</Wrapper>
315318
);
319+
320+
export const Offline = ({ offline }: { offline?: React.ReactNode }) => (
321+
<Wrapper
322+
i18nProvider={polyglotI18nProvider(() => englishMessages)}
323+
dataProvider={defaultDataProvider}
324+
record={authors[3]}
325+
>
326+
<RenderChildOnDemand>
327+
<ReferenceManyField
328+
reference="books"
329+
target="author_id"
330+
pagination={<Pagination />}
331+
perPage={5}
332+
offline={offline}
333+
>
334+
<IsOffline>
335+
<Alert severity="warning">
336+
You are offline, the data may be outdated
337+
</Alert>
338+
</IsOffline>
339+
<DataTable>
340+
<DataTable.Col source="title" />
341+
</DataTable>
342+
</ReferenceManyField>
343+
</RenderChildOnDemand>
344+
<SimulateOfflineButton />
345+
</Wrapper>
346+
);
347+
348+
const CustomOffline = () => {
349+
return <Alert severity="warning">You are offline!</Alert>;
350+
};
351+
352+
Offline.args = {
353+
offline: 'default',
354+
};
355+
356+
Offline.argTypes = {
357+
offline: {
358+
name: 'Offline component',
359+
control: { type: 'radio' },
360+
options: ['default', 'custom'],
361+
mapping: {
362+
default: undefined,
363+
custom: <CustomOffline />,
364+
},
365+
},
366+
};
367+
368+
const SimulateOfflineButton = () => {
369+
const isOffline = useIsOffline();
370+
return (
371+
<button
372+
type="button"
373+
onClick={() => onlineManager.setOnline(isOffline)}
374+
>
375+
{isOffline ? 'Simulate online' : 'Simulate offline'}
376+
</button>
377+
);
378+
};
379+
380+
const RenderChildOnDemand = ({ children }) => {
381+
const [showChild, setShowChild] = React.useState(false);
382+
return (
383+
<>
384+
<button onClick={() => setShowChild(!showChild)}>
385+
Toggle Child
386+
</button>
387+
{showChild && <div>{children}</div>}
388+
</>
389+
);
390+
};

packages/ra-ui-materialui/src/field/ReferenceManyField.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88

99
import { Typography } from '@mui/material';
1010
import type { FieldProps } from './types';
11+
import { Offline } from '../Offline';
1112

1213
/**
1314
* Render related records to the current one.
@@ -62,7 +63,15 @@ export const ReferenceManyField = <
6263
props: ReferenceManyFieldProps<RecordType, ReferenceRecordType>
6364
) => {
6465
const translate = useTranslate();
65-
const { children, pagination, empty, ...controllerProps } = props;
66+
const {
67+
children,
68+
pagination,
69+
empty,
70+
offline = defaultOffline,
71+
render,
72+
...controllerProps
73+
} = props;
74+
6675
return (
6776
<ReferenceManyFieldBase<RecordType, ReferenceRecordType>
6877
{...controllerProps}
@@ -75,17 +84,35 @@ export const ReferenceManyField = <
7584
empty
7685
)
7786
}
78-
>
79-
{children}
80-
{pagination}
81-
</ReferenceManyFieldBase>
87+
render={props => {
88+
const { isPaused, isPending, isPlaceholderData } = props;
89+
const shouldRenderOffline =
90+
isPaused &&
91+
(isPending || isPlaceholderData) &&
92+
offline !== undefined &&
93+
offline !== false;
94+
95+
return (
96+
<>
97+
{shouldRenderOffline
98+
? offline
99+
: render
100+
? render(props)
101+
: children}
102+
{pagination}
103+
</>
104+
);
105+
}}
106+
/>
82107
);
83108
};
84109

110+
const defaultOffline = <Offline variant="inline" />;
111+
85112
export interface ReferenceManyFieldProps<
86113
RecordType extends Record<string, any> = Record<string, any>,
87114
ReferenceRecordType extends RaRecord = RaRecord,
88115
> extends Omit<FieldProps<RecordType>, 'source'>,
89116
ReferenceManyFieldBaseProps<RecordType, ReferenceRecordType> {
90-
pagination?: React.ReactElement;
117+
pagination?: React.ReactNode;
91118
}

0 commit comments

Comments
 (0)