Skip to content

Commit e8764c6

Browse files
committed
Add offline support to <ReferenceManyCountBase> and <ReferenceManyCount>
1 parent 6540e3b commit e8764c6

File tree

7 files changed

+207
-15
lines changed

7 files changed

+207
-15
lines changed

docs/ReferenceManyCount.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const PostList = () => (
6868
| `target` | Required | string | - | Name of the field in the related resource that points to the current one. |
6969
| `filter` | Optional | Object | - | Filter to apply to the query. |
7070
| `link` | Optional | bool | `false` | If true, the count is wrapped in a `<Link>` to the filtered list view. |
71+
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
7172
| `resource` | Optional | string | - | Resource to count. Default to the current `ResourceContext` |
7273
| `sort` | Optional | `{ field: string, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'DESC' }` | The sort option sent to `getManyReference` |
7374
| `timeout` | Optional | number | 1000 | Number of milliseconds to wait before displaying the loading indicator. |
@@ -118,6 +119,22 @@ When used in conjunction to the `filter` prop, the link will point to the list v
118119
```
119120
{% endraw %}
120121

122+
## `offline`
123+
124+
By default, `<ReferenceManyCount>` renders the `<Offline variant="inline">` component when there is no connectivity and the count hasn't been cached yet. You can provide your own component via the `offline` prop:
125+
126+
```jsx
127+
<ReferenceManyCount
128+
reference="comments"
129+
target="post_id"
130+
offline={
131+
<Alert severity="warning">
132+
You are offline, data cannot be loaded
133+
</Alert>
134+
}
135+
/>
136+
```
137+
121138
## `reference`
122139

123140
The `reference` prop is required and must be the name of the related resource to fetch. For instance, to fetch the number of comments related to the current post:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import {
4+
Basic,
5+
ErrorState,
6+
LoadingState,
7+
Offline,
8+
} from './ReferenceManyCountBase.stories';
9+
10+
describe('ReferenceManyCountBase', () => {
11+
it('should display an error if error is defined', async () => {
12+
jest.spyOn(console, 'error')
13+
.mockImplementationOnce(() => {})
14+
.mockImplementationOnce(() => {});
15+
16+
render(<ErrorState />);
17+
await screen.findByText('Error!');
18+
});
19+
20+
it('should display the loading state', async () => {
21+
render(<LoadingState />);
22+
await screen.findByText('loading...', undefined, { timeout: 2000 });
23+
});
24+
25+
it('should render the total', async () => {
26+
render(<Basic />);
27+
await screen.findByText('3');
28+
});
29+
30+
it('should render the offline prop node when offline', async () => {
31+
render(<Offline />);
32+
fireEvent.click(await screen.findByText('Simulate offline'));
33+
fireEvent.click(await screen.findByText('Toggle Child'));
34+
await screen.findByText('You are offline, cannot load data');
35+
fireEvent.click(await screen.findByText('Simulate online'));
36+
await screen.findByText('3');
37+
fireEvent.click(await screen.findByText('Simulate offline'));
38+
expect(
39+
screen.queryByText('You are offline, cannot load data')
40+
).toBeNull();
41+
await screen.findByText('3');
42+
fireEvent.click(await screen.findByText('Simulate online'));
43+
});
44+
});

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import * as React from 'react';
2-
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
2+
import {
3+
QueryClientProvider,
4+
QueryClient,
5+
onlineManager,
6+
} from '@tanstack/react-query';
37
import { RecordContextProvider } from '../record';
48
import { DataProviderContext } from '../../dataProvider';
5-
import { ResourceContextProvider } from '../../core';
9+
import { ResourceContextProvider, useIsOffline } from '../../core';
610
import { TestMemoryRouter } from '../../routing';
711
import { ReferenceManyCountBase } from './ReferenceManyCountBase';
812

@@ -141,3 +145,57 @@ export const Slow = () => (
141145
/>
142146
</Wrapper>
143147
);
148+
149+
export const Offline = () => {
150+
return (
151+
<Wrapper
152+
dataProvider={{
153+
getManyReference: () =>
154+
Promise.resolve({
155+
data: [comments.filter(c => c.post_id === 1)[0]],
156+
total: comments.filter(c => c.post_id === 1).length,
157+
}),
158+
}}
159+
>
160+
<div>
161+
<RenderChildOnDemand>
162+
<ReferenceManyCountBase
163+
reference="comments"
164+
target="post_id"
165+
loading="Loading..."
166+
offline={
167+
<span style={{ color: 'orange' }}>
168+
You are offline, cannot load data
169+
</span>
170+
}
171+
/>
172+
</RenderChildOnDemand>
173+
</div>
174+
<SimulateOfflineButton />
175+
</Wrapper>
176+
);
177+
};
178+
179+
const SimulateOfflineButton = () => {
180+
const isOffline = useIsOffline();
181+
return (
182+
<button
183+
type="button"
184+
onClick={() => onlineManager.setOnline(isOffline)}
185+
>
186+
{isOffline ? 'Simulate online' : 'Simulate offline'}
187+
</button>
188+
);
189+
};
190+
191+
const RenderChildOnDemand = ({ children }) => {
192+
const [showChild, setShowChild] = React.useState(false);
193+
return (
194+
<>
195+
<button onClick={() => setShowChild(!showChild)}>
196+
Toggle Child
197+
</button>
198+
{showChild && <div>{children}</div>}
199+
</>
200+
);
201+
};

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import { useTimeout } from '../../util/hooks';
1717
* <ReferenceManyCountBase reference="comments" target="post_id" filter={{ is_published: true }} />
1818
*/
1919
export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
20-
const { loading = null, error = null, timeout = 1000, ...rest } = props;
20+
const { loading, error, offline, timeout = 1000, ...rest } = props;
2121
const oneSecondHasPassed = useTimeout(timeout);
2222

2323
const {
24+
isPaused,
2425
isPending,
2526
error: fetchError,
2627
total,
@@ -30,15 +31,24 @@ export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
3031
perPage: 1,
3132
});
3233

34+
const shouldRenderLoading =
35+
isPending && !isPaused && loading !== undefined && loading !== false;
36+
const shouldRenderOffline =
37+
isPending && isPaused && offline !== undefined && offline !== false;
38+
const shouldRenderError =
39+
!isPending && fetchError && error !== undefined && error !== false;
40+
3341
return (
3442
<>
35-
{isPending
43+
{shouldRenderLoading
3644
? oneSecondHasPassed
3745
? loading
3846
: null
39-
: fetchError
40-
? error
41-
: total}
47+
: shouldRenderOffline
48+
? offline
49+
: shouldRenderError
50+
? error
51+
: total}
4252
</>
4353
);
4454
};
@@ -48,4 +58,5 @@ export interface ReferenceManyCountBaseProps
4858
timeout?: number;
4959
loading?: React.ReactNode;
5060
error?: React.ReactNode;
61+
offline?: React.ReactNode;
5162
}

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

Lines changed: 17 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 } from '@testing-library/react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
33

44
import {
55
Basic,
66
ErrorState,
7+
Offline,
78
Themed,
89
WithFilter,
910
Wrapper,
@@ -52,4 +53,19 @@ describe('<ReferenceManyCount />', () => {
5253
render(<Themed />);
5354
expect(screen.getByTestId('themed')).toBeDefined();
5455
});
56+
57+
it('should render the offline prop node when offline', async () => {
58+
render(<Offline />);
59+
fireEvent.click(await screen.findByText('Simulate offline'));
60+
fireEvent.click(await screen.findByText('Toggle Child'));
61+
await screen.findByText('No connectivity. Could not fetch data.');
62+
fireEvent.click(await screen.findByText('Simulate online'));
63+
await screen.findByText('3');
64+
fireEvent.click(await screen.findByText('Simulate offline'));
65+
expect(
66+
screen.queryByText('No connectivity. Could not fetch data.')
67+
).toBeNull();
68+
await screen.findByText('3');
69+
fireEvent.click(await screen.findByText('Simulate online'));
70+
});
5571
});

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import * as React from 'react';
2-
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
2+
import {
3+
QueryClientProvider,
4+
QueryClient,
5+
onlineManager,
6+
} from '@tanstack/react-query';
37
import {
48
DataProviderContext,
59
RecordContextProvider,
610
ResourceContextProvider,
711
TestMemoryRouter,
12+
useIsOffline,
813
} from 'ra-core';
914
import { deepmerge } from '@mui/utils';
1015
import { createTheme, ThemeOptions } from '@mui/material';
@@ -238,3 +243,44 @@ export const Themed = () => (
238243
<ReferenceManyCount reference="comments" target="post_id" />
239244
</Wrapper>
240245
);
246+
247+
export const Offline = () => (
248+
<Wrapper
249+
dataProvider={{
250+
getManyReference: () =>
251+
Promise.resolve({
252+
data: [comments.filter(c => c.post_id === 1)[0]],
253+
total: comments.filter(c => c.post_id === 1).length,
254+
}),
255+
}}
256+
>
257+
<RenderChildOnDemand>
258+
<ReferenceManyCount reference="comments" target="post_id" />
259+
</RenderChildOnDemand>
260+
<SimulateOfflineButton />
261+
</Wrapper>
262+
);
263+
264+
const SimulateOfflineButton = () => {
265+
const isOffline = useIsOffline();
266+
return (
267+
<button
268+
type="button"
269+
onClick={() => onlineManager.setOnline(isOffline)}
270+
>
271+
{isOffline ? 'Simulate online' : 'Simulate offline'}
272+
</button>
273+
);
274+
};
275+
276+
const RenderChildOnDemand = ({ children }) => {
277+
const [showChild, setShowChild] = React.useState(false);
278+
return (
279+
<>
280+
<button onClick={() => setShowChild(!showChild)}>
281+
Toggle Child
282+
</button>
283+
{showChild && <div>{children}</div>}
284+
</>
285+
);
286+
};

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {
33
useRecordContext,
44
useCreatePath,
55
ReferenceManyCountBase,
6-
SortPayload,
76
RaRecord,
7+
ReferenceManyCountBaseProps,
88
} from 'ra-core';
99
import clsx from 'clsx';
1010
import { Typography, TypographyProps, CircularProgress } from '@mui/material';
@@ -19,6 +19,7 @@ import get from 'lodash/get';
1919
import { FieldProps } from './types';
2020
import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
2121
import { Link } from '../Link';
22+
import { Offline } from '../Offline';
2223

2324
/**
2425
* Fetch and render the number of records related to the current one
@@ -51,6 +52,7 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
5152
link,
5253
resource,
5354
source = 'id',
55+
offline = defaultOffline,
5456
...rest
5557
} = props;
5658
const record = useRecordContext(props);
@@ -63,6 +65,7 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
6365
error={
6466
<ErrorIcon color="error" fontSize="small" titleAccess="error" />
6567
}
68+
offline={offline}
6669
/>
6770
);
6871
return (
@@ -98,17 +101,14 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
98101

99102
// This is a hack that replaces react support for defaultProps. We currently need this for the Datagrid.
100103
ReferenceManyCount.textAlign = 'right';
104+
const defaultOffline = <Offline variant="inline" />;
101105

102106
export interface ReferenceManyCountProps<RecordType extends RaRecord = RaRecord>
103107
extends Omit<FieldProps<RecordType>, 'source'>,
108+
Omit<ReferenceManyCountBaseProps, 'source' | 'record'>,
104109
Omit<TypographyProps, 'textAlign'> {
105-
reference: string;
106110
source?: string;
107-
target: string;
108-
sort?: SortPayload;
109-
filter?: any;
110111
link?: boolean;
111-
timeout?: number;
112112
}
113113

114114
const PREFIX = 'RaReferenceManyCount';

0 commit comments

Comments
 (0)