Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

## `offline`

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:

```jsx
<ReferenceManyField
reference="books"
target="author_id"
offline="Offline, could not load data"
>
...
</ReferenceManyField>
```

`offline` also accepts a `ReactNode`.

```jsx
<ReferenceManyField
reference="books"
target="author_id"
empty={<Alert severity="warning">Offline, could not load data</Alert>}
>
...
</ReferenceManyField>
```

**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:

```jsx
<ReferenceManyField
reference="books"
target="author_id"
empty={<Alert severity="warning">Offline, could not load data</Alert>}
>
<IsOffline>
<Alert severity="warning">
You are offline, the data may be outdated
</Alert>
</IsOffline>
...
</ReferenceManyField>
```

## `pagination`

If you want to allow users to paginate the list, pass a `<Pagination>` element as the `pagination` prop:
Expand Down
22 changes: 14 additions & 8 deletions packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,20 @@ export const ReferenceManyFieldBase = <
hasPreviousPage,
isPaused,
isPending,
isPlaceholderData,
total,
} = controllerProps;

const showLoading =
const shouldRenderLoading =
isPending && !isPaused && loading !== false && loading !== undefined;
const showOffline = isPaused && offline !== false && offline !== undefined;
const showError = controllerError && error !== false && error !== undefined;
const showEmpty =
const shouldRenderOffline =
isPaused &&
(isPending || isPlaceholderData) &&
offline !== false &&
offline !== undefined;
const shouldRenderError =
controllerError && error !== false && error !== undefined;
const shouldRenderEmpty =
empty !== false &&
empty !== undefined &&
// there is no error
Expand All @@ -142,13 +148,13 @@ export const ReferenceManyFieldBase = <
return (
<ResourceContextProvider value={reference}>
<ListContextProvider value={controllerProps}>
{showLoading
{shouldRenderLoading
? loading
: showOffline
: shouldRenderOffline
? offline
: showError
: shouldRenderError
? error
: showEmpty
: shouldRenderEmpty
? empty
: render
? render(controllerProps)
Expand Down
41 changes: 40 additions & 1 deletion packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import { Pagination } from '../list/pagination/Pagination';
import {
Basic,
Empty,
Offline,
WithPagination,
WithPaginationAndSelectAllLimit,
WithRenderProp,
} from './ReferenceManyField.stories';
import { Alert } from '@mui/material';

const theme = createTheme();

Expand Down Expand Up @@ -207,7 +209,7 @@ describe('<ReferenceManyField />', () => {
});
});

it('should use render prop when provides', async () => {
it('should use render prop when provided', async () => {
render(<WithRenderProp />);
await waitFor(() => {
expect(screen.queryAllByRole('progressbar')).toHaveLength(0);
Expand Down Expand Up @@ -437,4 +439,41 @@ describe('<ReferenceManyField />', () => {
);
});
});
it('should render the default offline component node when offline', async () => {
render(<Offline />);
fireEvent.click(await screen.findByText('Simulate offline'));
fireEvent.click(await screen.findByText('Toggle Child'));
await screen.findByText('No connectivity. Could not fetch data.');
fireEvent.click(await screen.findByText('Simulate online'));
await screen.findByText("Harry Potter and the Philosopher's Stone");
expect(
screen.queryByText('No connectivity. Could not fetch data.')
).toBeNull();
fireEvent.click(await screen.findByText('Simulate offline'));
await screen.findByText('You are offline, the data may be outdated');
fireEvent.click(screen.getByLabelText('Go to page 2'));
await screen.findByText('No connectivity. Could not fetch data.');
fireEvent.click(screen.getByLabelText('Go to page 1'));
await screen.findByText("Harry Potter and the Philosopher's Stone");
fireEvent.click(await screen.findByText('Simulate online'));
});
it('should render the custom offline component node when offline', async () => {
const CustomOffline = () => {
return <Alert severity="warning">You are offline!</Alert>;
};
render(<Offline offline={<CustomOffline />} />);
fireEvent.click(await screen.findByText('Simulate offline'));
fireEvent.click(await screen.findByText('Toggle Child'));
await screen.findByText('You are offline!');
fireEvent.click(await screen.findByText('Simulate online'));
await screen.findByText("Harry Potter and the Philosopher's Stone");
expect(screen.queryByText('You are offline!')).toBeNull();
fireEvent.click(await screen.findByText('Simulate offline'));
await screen.findByText('You are offline, the data may be outdated');
fireEvent.click(screen.getByLabelText('Go to page 2'));
await screen.findByText('You are offline!');
fireEvent.click(screen.getByLabelText('Go to page 1'));
await screen.findByText("Harry Potter and the Philosopher's Stone");
fireEvent.click(await screen.findByText('Simulate online'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
RecordContextProvider,
ResourceContextProvider,
TestMemoryRouter,
useIsOffline,
IsOffline,
} from 'ra-core';
import { Admin, ListGuesser, Resource } from 'react-admin';
import type { AdminProps } from 'react-admin';
import { ThemeProvider, Box, Stack } from '@mui/material';
import { Alert, ThemeProvider, Box, Stack } from '@mui/material';
import { createTheme } from '@mui/material/styles';
import fakeDataProvider from 'ra-data-fakerest';
import polyglotI18nProvider from 'ra-i18n-polyglot';
Expand All @@ -29,6 +31,7 @@ import { TextInput } from '../input';
import { Edit } from '../detail';
import { SimpleForm } from '../form';
import { SelectAllButton, BulkDeleteButton } from '../button';
import { onlineManager } from '@tanstack/react-query';

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

Expand Down Expand Up @@ -313,3 +316,75 @@ export const WithRenderProp = () => (
/>
</Wrapper>
);

export const Offline = ({ offline }: { offline?: React.ReactNode }) => (
<Wrapper
i18nProvider={polyglotI18nProvider(() => englishMessages)}
dataProvider={defaultDataProvider}
record={authors[3]}
>
<RenderChildOnDemand>
<ReferenceManyField
reference="books"
target="author_id"
pagination={<Pagination />}
perPage={5}
offline={offline}
>
<IsOffline>
<Alert severity="warning">
You are offline, the data may be outdated
</Alert>
</IsOffline>
<DataTable>
<DataTable.Col source="title" />
</DataTable>
</ReferenceManyField>
</RenderChildOnDemand>
<SimulateOfflineButton />
</Wrapper>
);

const CustomOffline = () => {
return <Alert severity="warning">You are offline!</Alert>;
};

Offline.args = {
offline: 'default',
};

Offline.argTypes = {
offline: {
name: 'Offline component',
control: { type: 'radio' },
options: ['default', 'custom'],
mapping: {
default: undefined,
custom: <CustomOffline />,
},
},
};

const SimulateOfflineButton = () => {
const isOffline = useIsOffline();
return (
<button
type="button"
onClick={() => onlineManager.setOnline(isOffline)}
>
{isOffline ? 'Simulate online' : 'Simulate offline'}
</button>
);
};

const RenderChildOnDemand = ({ children }) => {
const [showChild, setShowChild] = React.useState(false);
return (
<>
<button onClick={() => setShowChild(!showChild)}>
Toggle Child
</button>
{showChild && <div>{children}</div>}
</>
);
};
39 changes: 33 additions & 6 deletions packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {

import { Typography } from '@mui/material';
import type { FieldProps } from './types';
import { Offline } from '../Offline';

/**
* Render related records to the current one.
Expand Down Expand Up @@ -62,7 +63,15 @@ export const ReferenceManyField = <
props: ReferenceManyFieldProps<RecordType, ReferenceRecordType>
) => {
const translate = useTranslate();
const { children, pagination, empty, ...controllerProps } = props;
const {
children,
pagination,
empty,
offline = defaultOffline,
render,
...controllerProps
} = props;

return (
<ReferenceManyFieldBase<RecordType, ReferenceRecordType>
{...controllerProps}
Expand All @@ -75,17 +84,35 @@ export const ReferenceManyField = <
empty
)
}
>
{children}
{pagination}
</ReferenceManyFieldBase>
render={props => {
const { isPaused, isPending, isPlaceholderData } = props;
const shouldRenderOffline =
isPaused &&
(isPending || isPlaceholderData) &&
offline !== undefined &&
offline !== false;

return (
<>
{shouldRenderOffline
? offline
: render
? render(props)
: children}
{pagination}
</>
);
}}
/>
);
};

const defaultOffline = <Offline variant="inline" />;

export interface ReferenceManyFieldProps<
RecordType extends Record<string, any> = Record<string, any>,
ReferenceRecordType extends RaRecord = RaRecord,
> extends Omit<FieldProps<RecordType>, 'source'>,
ReferenceManyFieldBaseProps<RecordType, ReferenceRecordType> {
pagination?: React.ReactElement;
pagination?: React.ReactNode;
}
Loading