Skip to content

Commit 47dd9bb

Browse files
committed
Add offline support to <ReferenceArrayFieldBase> and <ReferenceArrayField>
1 parent 6540e3b commit 47dd9bb

File tree

8 files changed

+324
-116
lines changed

8 files changed

+324
-116
lines changed

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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
Errored,
66
Loading,
7+
Offline,
78
WithRenderProp,
89
} from './ReferenceArrayFieldBase.stories';
910

@@ -83,4 +84,18 @@ describe('ReferenceArrayFieldBase', () => {
8384
expect(screen.queryByText('Charlie Watts')).not.toBeNull();
8485
});
8586
});
87+
88+
it('should render the offline prop node when offline', async () => {
89+
render(<Offline />);
90+
await screen.findByText('The Beatles');
91+
fireEvent.click(await screen.findByText('Simulate offline'));
92+
fireEvent.click(await screen.findByText('Toggle Child'));
93+
await screen.findByText('You are offline, cannot load data');
94+
fireEvent.click(await screen.findByText('Simulate online'));
95+
await screen.findByText('John Lennon');
96+
// Ensure the data is still displayed when going offline after it was loaded
97+
fireEvent.click(await screen.findByText('Simulate offline'));
98+
await screen.findByText('You are offline, the data may be outdated');
99+
await screen.findByText('John Lennon');
100+
});
86101
});

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

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import { ReferenceArrayFieldBase } from './ReferenceArrayFieldBase';
55
import {
66
CoreAdmin,
77
DataProvider,
8+
IsOffline,
89
Resource,
910
ShowBase,
1011
TestMemoryRouter,
12+
useIsOffline,
1113
useListContext,
14+
WithRecord,
1215
} from '../..';
13-
import { QueryClient } from '@tanstack/react-query';
16+
import { onlineManager, QueryClient } from '@tanstack/react-query';
1417

1518
export default { title: 'ra-core/controller/field/ReferenceArrayFieldBase' };
1619

@@ -154,3 +157,104 @@ export const WithRenderProp = ({
154157
</CoreAdmin>
155158
</TestMemoryRouter>
156159
);
160+
161+
export const Offline = () => (
162+
<TestMemoryRouter initialEntries={['/bands/1/show']}>
163+
<CoreAdmin
164+
dataProvider={defaultDataProvider}
165+
queryClient={
166+
new QueryClient({
167+
defaultOptions: {
168+
queries: {
169+
retry: false,
170+
},
171+
},
172+
})
173+
}
174+
>
175+
<Resource
176+
name="bands"
177+
show={
178+
<ShowBase>
179+
<div>
180+
<WithRecord render={band => <p>{band.name}</p>} />
181+
<RenderChildOnDemand>
182+
<ReferenceArrayFieldBase
183+
source="members"
184+
reference="artists"
185+
offline={
186+
<p style={{ color: 'orange' }}>
187+
You are offline, cannot load data
188+
</p>
189+
}
190+
render={({ data, isPending, error }) => {
191+
if (isPending) {
192+
return <p>Loading...</p>;
193+
}
194+
195+
if (error) {
196+
return (
197+
<p style={{ color: 'red' }}>
198+
{error.toString()}
199+
</p>
200+
);
201+
}
202+
203+
return (
204+
<>
205+
<IsOffline>
206+
<p
207+
style={{
208+
color: 'orange',
209+
}}
210+
>
211+
You are offline, the
212+
data may be outdated
213+
</p>
214+
</IsOffline>
215+
<p>
216+
{data?.map(
217+
(datum, index) => (
218+
<li key={index}>
219+
{datum.name}
220+
</li>
221+
)
222+
)}
223+
</p>
224+
</>
225+
);
226+
}}
227+
/>
228+
</RenderChildOnDemand>
229+
</div>
230+
<SimulateOfflineButton />
231+
</ShowBase>
232+
}
233+
/>
234+
</CoreAdmin>
235+
</TestMemoryRouter>
236+
);
237+
238+
const SimulateOfflineButton = () => {
239+
const isOffline = useIsOffline();
240+
return (
241+
<button
242+
type="button"
243+
onClick={() => onlineManager.setOnline(isOffline)}
244+
>
245+
{isOffline ? 'Simulate online' : 'Simulate offline'}
246+
</button>
247+
);
248+
};
249+
250+
const RenderChildOnDemand = ({ children }) => {
251+
const [showChild, setShowChild] = React.useState(false);
252+
return (
253+
<>
254+
<button onClick={() => setShowChild(!showChild)}>
255+
Toggle Child
256+
</button>
257+
{showChild && <div>{children}</div>}
258+
</>
259+
);
260+
};

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

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const ReferenceArrayFieldBase = <
7878
loading,
7979
empty,
8080
filter,
81+
offline,
8182
page = 1,
8283
perPage,
8384
reference,
@@ -107,25 +108,19 @@ export const ReferenceArrayFieldBase = <
107108
"<ReferenceArrayFieldBase> requires either a 'render' prop or 'children' prop"
108109
);
109110
}
111+
const { error: controllerError, isPending, isPaused } = controllerProps;
110112

111-
if (controllerProps.isPending && loading) {
112-
return (
113-
<ResourceContextProvider value={reference}>
114-
{loading}
115-
</ResourceContextProvider>
116-
);
117-
}
118-
if (controllerProps.error && error) {
119-
return (
120-
<ResourceContextProvider value={reference}>
121-
<ListContextProvider value={controllerProps}>
122-
{error}
123-
</ListContextProvider>
124-
</ResourceContextProvider>
125-
);
126-
}
127-
if (
128-
// there is an empty page component
113+
const shouldRenderLoading =
114+
isPending && !isPaused && loading !== undefined && loading !== false;
115+
const shouldRenderOffline =
116+
isPending && isPaused && offline !== undefined && offline !== false;
117+
const shouldRenderError =
118+
!isPending &&
119+
!isPaused &&
120+
controllerError &&
121+
error !== undefined &&
122+
error !== false;
123+
const shouldRenderEmpty = // there is an empty page component
129124
empty &&
130125
// there is no error
131126
!controllerProps.error &&
@@ -141,19 +136,22 @@ export const ReferenceArrayFieldBase = <
141136
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
142137
controllerProps.data.length === 0)) &&
143138
// the user didn't set any filters
144-
!Object.keys(controllerProps.filterValues).length
145-
) {
146-
return (
147-
<ResourceContextProvider value={reference}>
148-
{empty}
149-
</ResourceContextProvider>
150-
);
151-
}
139+
!Object.keys(controllerProps.filterValues).length;
152140

153141
return (
154142
<ResourceContextProvider value={reference}>
155143
<ListContextProvider value={controllerProps}>
156-
{render ? render(controllerProps) : children}
144+
{shouldRenderLoading
145+
? loading
146+
: shouldRenderOffline
147+
? offline
148+
: shouldRenderError
149+
? error
150+
: shouldRenderEmpty
151+
? empty
152+
: render
153+
? render(controllerProps)
154+
: children}
157155
</ListContextProvider>
158156
</ResourceContextProvider>
159157
);
@@ -169,6 +167,7 @@ export interface ReferenceArrayFieldBaseProps<
169167
loading?: ReactNode;
170168
empty?: ReactNode;
171169
filter?: FilterPayload;
170+
offline?: ReactNode;
172171
page?: number;
173172
perPage?: number;
174173
reference: string;

packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,40 +76,50 @@ export const useReferenceArrayFieldController = <
7676
const { meta, ...otherQueryOptions } = queryOptions;
7777
const ids = Array.isArray(value) ? value : emptyArray;
7878

79-
const { data, error, isLoading, isFetching, isPending, refetch } =
80-
useGetManyAggregate<ReferenceRecordType, ErrorType>(
81-
reference,
82-
{ ids, meta },
83-
{
84-
onError: error =>
85-
notify(
86-
typeof error === 'string'
87-
? error
88-
: (error as Error)?.message ||
89-
'ra.notification.http_error',
90-
{
91-
type: 'error',
92-
messageArgs: {
93-
_:
94-
typeof error === 'string'
95-
? error
96-
: (error as Error)?.message
97-
? (error as Error).message
98-
: undefined,
99-
},
100-
}
101-
),
102-
...otherQueryOptions,
103-
}
104-
);
79+
const {
80+
data,
81+
error,
82+
isLoading,
83+
isFetching,
84+
isPaused,
85+
isPending,
86+
isPlaceholderData,
87+
refetch,
88+
} = useGetManyAggregate<ReferenceRecordType, ErrorType>(
89+
reference,
90+
{ ids, meta },
91+
{
92+
onError: error =>
93+
notify(
94+
typeof error === 'string'
95+
? error
96+
: (error as Error)?.message ||
97+
'ra.notification.http_error',
98+
{
99+
type: 'error',
100+
messageArgs: {
101+
_:
102+
typeof error === 'string'
103+
? error
104+
: (error as Error)?.message
105+
? (error as Error).message
106+
: undefined,
107+
},
108+
}
109+
),
110+
...otherQueryOptions,
111+
}
112+
);
105113

106114
const listProps = useList<ReferenceRecordType, ErrorType>({
107115
data,
108116
error,
109117
filter,
110118
isFetching,
111119
isLoading,
120+
isPaused,
112121
isPending,
122+
isPlaceholderData,
113123
page,
114124
perPage,
115125
sort,

0 commit comments

Comments
 (0)