Skip to content

Commit 42c8f8b

Browse files
committed
Add offline support to <ReferenceArrayInputBase> and <ReferenceArrayInput>
1 parent bc02cc4 commit 42c8f8b

File tree

7 files changed

+212
-8
lines changed

7 files changed

+212
-8
lines changed

docs/ReferenceArrayInput.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ See the [`children`](#children) section for more details.
105105
| `enableGet Choices` | Optional | `({q: string}) => boolean` | `() => true` | Function taking the `filterValues` and returning a boolean to enable the `getList` call. |
106106
| `filter` | Optional | `Object` | `{}` | Permanent filters to use for getting the suggestion list |
107107
| `label` | Optional | `string` | - | Useful only when `ReferenceArrayInput` is in a Filter array, the label is used as the Filter label. |
108+
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
108109
| `page` | Optional | `number` | 1 | The current page number |
109110
| `perPage` | Optional | `number` | 25 | Number of suggestions to show |
110111
| `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
@@ -217,6 +218,26 @@ const filters = [
217218
];
218219
```
219220

221+
## `offline`
222+
223+
`<ReferenceArrayInput>` can display a custom message when the referenced record is missing because there is no network connectivity, thanks to the `offline` prop.
224+
225+
```jsx
226+
<ReferenceArrayInput source="tags_ids" reference="tags" offline="No network, could not fetch data" />
227+
```
228+
229+
`<ReferenceArrayInput>` renders the `offline` element when:
230+
231+
- the referenced record is missing (no record in the `tags` table with the right `tags_ids`), and
232+
- there is no network connectivity
233+
234+
You can pass either a React element or a string to the `offline` prop:
235+
236+
```jsx
237+
<ReferenceArrayInput source="tags_ids" reference="tags" offline={<span>No network, could not fetch data</span>} />
238+
<ReferenceArrayInput source="tags_ids" reference="tags" offline="No network, could not fetch data" />
239+
```
240+
220241
## `parse`
221242

222243
By default, children of `<ReferenceArrayInput>` transform the empty form value (an empty string) into `null` before passing it to the `dataProvider`.

packages/ra-core/src/controller/input/ReferenceArrayInputBase.spec.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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 { testDataProvider } from 'ra-core';
4-
import { Basic, WithError } from './ReferenceArrayInputBase.stories';
4+
import { Basic, Offline, WithError } from './ReferenceArrayInputBase.stories';
55

66
describe('<ReferenceArrayInputBase>', () => {
77
afterEach(async () => {
@@ -51,4 +51,13 @@ describe('<ReferenceArrayInputBase>', () => {
5151
});
5252
});
5353
});
54+
55+
it('should render the offline prop node when offline', async () => {
56+
render(<Offline />);
57+
fireEvent.click(await screen.findByText('Simulate offline'));
58+
fireEvent.click(await screen.findByText('Toggle Child'));
59+
await screen.findByText('You are offline, cannot load data');
60+
fireEvent.click(await screen.findByText('Simulate online'));
61+
await screen.findByText('Architecture');
62+
});
5463
});

packages/ra-core/src/controller/input/ReferenceArrayInputBase.stories.tsx

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import {
2020
ChoicesProps,
2121
useChoicesContext,
2222
} from '../../form';
23-
import { useGetRecordRepresentation } from '../..';
23+
import { useGetRecordRepresentation, useIsOffline } from '../..';
24+
import { onlineManager } from '@tanstack/react-query';
2425

25-
export default { title: 'ra-core/controller/ReferenceArrayInputBase' };
26+
export default { title: 'ra-core/controller/input/ReferenceArrayInputBase' };
2627

2728
const tags = [
2829
{ id: 0, name: '3D' },
@@ -234,3 +235,80 @@ export const WithError = () => (
234235
</CoreAdmin>
235236
</TestMemoryRouter>
236237
);
238+
239+
export const Offline = () => (
240+
<TestMemoryRouter initialEntries={['/posts/create']}>
241+
<CoreAdmin
242+
dataProvider={defaultDataProvider}
243+
i18nProvider={i18nProvider}
244+
>
245+
<Resource
246+
name="posts"
247+
create={
248+
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
249+
<h1>Create Post</h1>
250+
<Form>
251+
<div
252+
style={{
253+
width: '200px',
254+
display: 'flex',
255+
flexDirection: 'column',
256+
gap: '10px',
257+
}}
258+
>
259+
<RenderChildOnDemand>
260+
<ReferenceArrayInputBase
261+
reference="tags"
262+
resource="posts"
263+
source="tags_ids"
264+
offline={
265+
<p>
266+
You are offline, cannot load
267+
data
268+
</p>
269+
}
270+
>
271+
<CheckboxGroupInput optionText="name" />
272+
</ReferenceArrayInputBase>
273+
</RenderChildOnDemand>
274+
<SimulateOfflineButton />
275+
</div>
276+
</Form>
277+
</CreateBase>
278+
}
279+
/>
280+
</CoreAdmin>
281+
</TestMemoryRouter>
282+
);
283+
284+
const SimulateOfflineButton = () => {
285+
const isOffline = useIsOffline();
286+
return (
287+
<button
288+
type="button"
289+
onClick={event => {
290+
event.preventDefault();
291+
onlineManager.setOnline(isOffline);
292+
}}
293+
>
294+
{isOffline ? 'Simulate online' : 'Simulate offline'}
295+
</button>
296+
);
297+
};
298+
299+
const RenderChildOnDemand = ({ children }) => {
300+
const [showChild, setShowChild] = React.useState(false);
301+
return (
302+
<>
303+
<button
304+
onClick={event => {
305+
event.preventDefault();
306+
setShowChild(!showChild);
307+
}}
308+
>
309+
Toggle Child
310+
</button>
311+
{showChild && <div>{children}</div>}
312+
</>
313+
);
314+
};

packages/ra-core/src/controller/input/ReferenceArrayInputBase.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,14 @@ import { ChoicesContextValue } from '../../form';
7979
export const ReferenceArrayInputBase = <RecordType extends RaRecord = any>(
8080
props: ReferenceArrayInputBaseProps<RecordType>
8181
) => {
82-
const { children, filter = defaultFilter, reference, render, sort } = props;
82+
const {
83+
children,
84+
filter = defaultFilter,
85+
offline,
86+
reference,
87+
render,
88+
sort,
89+
} = props;
8390
if (children && React.Children.count(children) !== 1) {
8491
throw new Error(
8592
'<ReferenceArrayInputBase> only accepts a single child (like <AutocompleteArrayInput>)'
@@ -97,11 +104,21 @@ export const ReferenceArrayInputBase = <RecordType extends RaRecord = any>(
97104
sort,
98105
filter,
99106
});
107+
const { isPaused, isPending } = controllerProps;
108+
// isPending is true: there's no cached data and no query attempt was finished yet
109+
// isPaused is true: the query was paused (e.g. due to a network issue)
110+
// Both true: we're offline and have no data to show
111+
const shouldRenderOffline =
112+
isPaused && isPending && offline !== undefined && offline !== false;
100113

101114
return (
102115
<ResourceContextProvider value={reference}>
103116
<ChoicesContextProvider value={controllerProps}>
104-
{render ? render(controllerProps) : children}
117+
{shouldRenderOffline
118+
? offline
119+
: render
120+
? render(controllerProps)
121+
: children}
105122
</ChoicesContextProvider>
106123
</ResourceContextProvider>
107124
);
@@ -114,4 +131,5 @@ export interface ReferenceArrayInputBaseProps<RecordType extends RaRecord = any>
114131
UseReferenceArrayInputParams<RecordType> {
115132
children?: React.ReactNode;
116133
render?: (context: ChoicesContextValue<RecordType>) => React.ReactNode;
134+
offline?: React.ReactNode;
117135
}

packages/ra-core/src/controller/input/useReferenceArrayInputController.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ export const useReferenceArrayInputController = <
6161
error: errorGetMany,
6262
isLoading: isLoadingGetMany,
6363
isFetching: isFetchingGetMany,
64+
isPaused: isPausedGetMany,
6465
isPending: isPendingGetMany,
66+
isPlaceholderData: isPlaceholderDataGetMany,
6567
refetch: refetchGetMany,
6668
} = useGetManyAggregate<RecordType>(
6769
reference,
@@ -99,7 +101,9 @@ export const useReferenceArrayInputController = <
99101
error: errorGetList,
100102
isLoading: isLoadingGetList,
101103
isFetching: isFetchingGetList,
104+
isPaused: isPausedGetList,
102105
isPending: isPendingGetList,
106+
isPlaceholderData: isPlaceholderDataGetList,
103107
refetch: refetchGetMatching,
104108
} = useGetList<RecordType>(
105109
reference,
@@ -153,7 +157,9 @@ export const useReferenceArrayInputController = <
153157
hideFilter: paramsModifiers.hideFilter,
154158
isFetching: isFetchingGetMany || isFetchingGetList,
155159
isLoading: isLoadingGetMany || isLoadingGetList,
160+
isPaused: isPausedGetMany || isPausedGetList,
156161
isPending: isPendingGetMany || isPendingGetList,
162+
isPlaceholderData: isPlaceholderDataGetMany || isPlaceholderDataGetList,
157163
page: params.page,
158164
perPage: params.perPage,
159165
refetch,

packages/ra-ui-materialui/src/input/ReferenceArrayInput.stories.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Resource,
66
testDataProvider,
77
TestMemoryRouter,
8+
useIsOffline,
89
} from 'ra-core';
910
import polyglotI18nProvider from 'ra-i18n-polyglot';
1011
import englishMessages from 'ra-language-english';
@@ -20,6 +21,7 @@ import { ReferenceArrayInput } from './ReferenceArrayInput';
2021
import { AutocompleteArrayInput } from './AutocompleteArrayInput';
2122
import { SelectArrayInput } from './SelectArrayInput';
2223
import { CheckboxGroupInput } from './CheckboxGroupInput';
24+
import { onlineManager } from '@tanstack/react-query';
2325

2426
export default { title: 'ra-ui-materialui/input/ReferenceArrayInput' };
2527

@@ -274,3 +276,65 @@ export const DifferentIdTypes = () => {
274276
</AdminContext>
275277
);
276278
};
279+
280+
export const Offline = () => {
281+
const fakeData = {
282+
bands: [{ id: 1, name: 'band_1', members: [1, '2'] }],
283+
artists: [
284+
{ id: 1, name: 'artist_1' },
285+
{ id: 2, name: 'artist_2' },
286+
{ id: 3, name: 'artist_3' },
287+
],
288+
};
289+
return (
290+
<TestMemoryRouter>
291+
<AdminContext
292+
dataProvider={fakeRestProvider(
293+
fakeData,
294+
process.env.NODE_ENV !== 'test'
295+
)}
296+
i18nProvider={i18nProvider}
297+
>
298+
<>
299+
<Edit resource="bands" id={1} sx={{ width: 600 }}>
300+
<SimpleForm>
301+
<RenderChildOnDemand>
302+
<ReferenceArrayInput
303+
source="members"
304+
reference="artists"
305+
/>
306+
</RenderChildOnDemand>
307+
</SimpleForm>
308+
</Edit>
309+
<p>
310+
<SimulateOfflineButton />
311+
</p>
312+
</>
313+
</AdminContext>
314+
</TestMemoryRouter>
315+
);
316+
};
317+
318+
const SimulateOfflineButton = () => {
319+
const isOffline = useIsOffline();
320+
return (
321+
<button
322+
type="button"
323+
onClick={() => onlineManager.setOnline(isOffline)}
324+
>
325+
{isOffline ? 'Simulate online' : 'Simulate offline'}
326+
</button>
327+
);
328+
};
329+
330+
const RenderChildOnDemand = ({ children }) => {
331+
const [showChild, setShowChild] = React.useState(false);
332+
return (
333+
<>
334+
<button type="button" onClick={() => setShowChild(!showChild)}>
335+
Toggle Child
336+
</button>
337+
{showChild && <div>{children}</div>}
338+
</>
339+
);
340+
};

packages/ra-ui-materialui/src/input/ReferenceArrayInput.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { ReferenceArrayInputBase, ReferenceArrayInputBaseProps } from 'ra-core';
33
import { AutocompleteArrayInput } from './AutocompleteArrayInput';
4+
import { Offline } from '../Offline';
45

56
/**
67
* An Input component for fields containing a list of references to another resource.
@@ -70,19 +71,26 @@ import { AutocompleteArrayInput } from './AutocompleteArrayInput';
7071
* a `setFilters` function. You can call this function to filter the results.
7172
*/
7273
export const ReferenceArrayInput = (props: ReferenceArrayInputProps) => {
73-
const { children = defaultChildren, ...rest } = props;
74+
const {
75+
children = defaultChildren,
76+
offline = defaultOffline,
77+
...rest
78+
} = props;
7479
if (React.Children.count(children) !== 1) {
7580
throw new Error(
7681
'<ReferenceArrayInput> only accepts a single child (like <AutocompleteArrayInput>)'
7782
);
7883
}
7984

8085
return (
81-
<ReferenceArrayInputBase {...rest}>{children}</ReferenceArrayInputBase>
86+
<ReferenceArrayInputBase {...rest} offline={offline}>
87+
{children}
88+
</ReferenceArrayInputBase>
8289
);
8390
};
8491

8592
const defaultChildren = <AutocompleteArrayInput />;
93+
const defaultOffline = <Offline variant="inline" />;
8694

8795
export interface ReferenceArrayInputProps extends ReferenceArrayInputBaseProps {
8896
label?: string;

0 commit comments

Comments
 (0)