diff --git a/docs/ReferenceOneField.md b/docs/ReferenceOneField.md index a525793f50f..2837c5744b0 100644 --- a/docs/ReferenceOneField.md +++ b/docs/ReferenceOneField.md @@ -64,6 +64,7 @@ const BookShow = () => ( | `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty | | `filter` | Optional | `Object` | `{}` | Used to filter referenced records | | `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. | +| `offline` | Optional | `ReactNode` | - | The text or element to display when there is no network connectivity | | `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options | | `sort` | Optional | `{ field: String, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'ASC' }` | Used to order referenced records | @@ -156,6 +157,29 @@ You can also set the `link` prop to a string, which will be used as the link typ ``` + +## `offline` + +When the user is offline, `` is smart enough to display the referenced record if it was previously fetched. However, if the referenced record has never been fetched before, `` displays an error message explaining that the app has lost network connectivity. + +You can customize this error message by passing a React element or a string to the `offline` prop: + +```jsx +No network, could not fetch data

} +> + ... +
+ + ... + +``` + ## `queryOptions` `` uses `react-query` to fetch the related record. You can set [any of `useQuery` options](https://tanstack.com/query/v5/docs/react/reference/useQuery) via the `queryOptions` prop. diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md index 0f119d04502..26f0e8f75ee 100644 --- a/docs/ReferenceOneFieldBase.md +++ b/docs/ReferenceOneFieldBase.md @@ -78,6 +78,7 @@ const BookDetails = () => { | `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty | | `filter` | Optional | `Object` | `{}` | Used to filter referenced records | | `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. | +| `offline` | Optional | `ReactNode` | - | The text or element to display when there is no network connectivity | | `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options | | `sort` | Optional | `{ field: String, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'ASC' }` | Used to order referenced records | @@ -122,39 +123,6 @@ const BookDetails = () => { } ``` -## `render` - -Alternatively to children you can pass a `render` function prop to ``. The `render` function prop will receive the `ReferenceFieldContext` as its argument, allowing to inline the render logic. -When receiving a `render` function prop the `` component will ignore the children property. - -```jsx -const BookShow = () => ( - { - if (isPending) { - return

Loading...

; - } - - if (error) { - return

{error.toString()}

; - } - - if (!referenceRecord) { - return

No details found

; - } - return ( -
-

{referenceRecord.genre}

-

{referenceRecord.ISBN}

-
- ); - }} - /> -); -``` - ## `empty` Use `empty` to customize the text displayed when the related record is empty. @@ -228,6 +196,28 @@ You can also set the `link` prop to a string, which will be used as the link typ ``` {% endraw %} +## `offline` + +When the user is offline, `` is smart enough to display the referenced record if it was previously fetched. However, if the referenced record has never been fetched before, `` displays an error message explaining that the app has lost network connectivity. + +You can customize this error message by passing a React element or a string to the `offline` prop: + +```jsx +No network, could not fetch data

} +> + ... +
+ + ... + +``` + ## `queryOptions` `` uses `react-query` to fetch the related record. You can set [any of `useQuery` options](https://tanstack.com/query/v5/docs/react/reference/useQuery) via the `queryOptions` prop. @@ -247,6 +237,39 @@ For instance, if you want to disable the refetch on window focus for this query, ``` {% endraw %} +## `render` + +Alternatively to children you can pass a `render` function prop to ``. The `render` function prop will receive the `ReferenceFieldContext` as its argument, allowing to inline the render logic. +When receiving a `render` function prop the `` component will ignore the children property. + +```jsx +const BookShow = () => ( + { + if (isPending) { + return

Loading...

; + } + + if (error) { + return

{error.toString()}

; + } + + if (!referenceRecord) { + return

No details found

; + } + return ( +
+

{referenceRecord.genre}

+

{referenceRecord.ISBN}

+
+ ); + }} + /> +); +``` + ## `reference` The name of the resource to fetch for the related records. diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx index 3a003c2439a..aa64a28765e 100644 --- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Basic, Loading, + Offline, WithRenderProp, } from './ReferenceOneFieldBase.stories'; @@ -61,4 +62,13 @@ describe('ReferenceOneFieldBase', () => { }); }); }); + + it('should render the offline prop node when offline', async () => { + render(); + fireEvent.click(await screen.findByText('Simulate offline')); + fireEvent.click(await screen.findByText('Toggle Child')); + await screen.findByText('Offline'); + fireEvent.click(await screen.findByText('Simulate online')); + await screen.findByText('9780393966473'); + }); }); diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx index 64dad507bac..6cfee8b9d99 100644 --- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx +++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx @@ -6,8 +6,10 @@ import { ReferenceOneFieldBase, ResourceContextProvider, TestMemoryRouter, + useIsOffline, useReferenceFieldContext, } from '../..'; +import { onlineManager } from '@tanstack/react-query'; export default { title: 'ra-core/controller/field/ReferenceOneFieldBase' }; @@ -100,3 +102,46 @@ export const WithRenderProp = ({ ); }; + +export const Offline = () => { + return ( + +
+ + Offline} + > + + + +
+ +
+ ); +}; + +const SimulateOfflineButton = () => { + const isOffline = useIsOffline(); + return ( + + ); +}; + +const RenderChildOnDemand = ({ children }) => { + const [showChild, setShowChild] = React.useState(false); + return ( + <> + + {showChild &&
{children}
} + + ); +}; diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx index ed056b20827..7d0f5c7ee31 100644 --- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx +++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx @@ -41,6 +41,7 @@ export const ReferenceOneFieldBase = < sort, filter, link, + offline, queryOptions, } = props; @@ -78,38 +79,46 @@ export const ReferenceOneFieldBase = < } const recordFromContext = useRecordContext(props); - if (controllerProps.isPending && loading) { - return ( - - {loading} - - ); - } - if (controllerProps.error && error) { - return ( - - - {error} - - - ); - } - if ( - !recordFromContext || - (!controllerProps.isPending && controllerProps.referenceRecord == null) - ) { - return ( - - {empty} - - ); - } + const { + error: controllerError, + isPending, + isPaused, + referenceRecord, + } = controllerProps; + + const shouldRenderLoading = + !isPaused && isPending && loading !== false && loading !== undefined; + const shouldRenderOffline = + isPaused && + !referenceRecord && + offline !== false && + offline !== undefined; + const shouldRenderError = + !!controllerError && error !== false && error !== undefined; + const shouldRenderEmpty = + (!recordFromContext || + (!isPaused && + referenceRecord == null && + !controllerError && + !isPending)) && + empty !== false && + empty !== undefined; return ( - - {render ? render(controllerProps) : children} + + {shouldRenderLoading + ? loading + : shouldRenderOffline + ? offline + : shouldRenderError + ? error + : shouldRenderEmpty + ? empty + : render + ? render(controllerProps) + : children} @@ -127,6 +136,7 @@ export interface ReferenceOneFieldBaseProps< loading?: ReactNode; error?: ReactNode; empty?: ReactNode; + offline?: ReactNode; render?: (props: UseReferenceResult) => ReactNode; link?: LinkToType; resource?: string; diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx index fe863bac4e3..2ee5da8b8cd 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx @@ -10,6 +10,7 @@ import { Empty, Themed, WithRenderProp, + Offline, } from './ReferenceOneField.stories'; describe('ReferenceOneField', () => { @@ -86,4 +87,13 @@ describe('ReferenceOneField', () => { render(); expect(await screen.findByTestId('themed')).toBeDefined(); }); + + it('should render the offline prop node when offline', async () => { + render(); + 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('9780393966473'); + }); }); diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx index bc988013257..89a86286479 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx @@ -11,12 +11,14 @@ import { I18nContextProvider, TestMemoryRouter, Resource, + useIsOffline, } from 'ra-core'; import fakeRestDataProvider from 'ra-data-fakerest'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; import { createTheme, Stack, ThemeOptions } from '@mui/material'; import { deepmerge } from '@mui/utils'; +import { onlineManager } from '@tanstack/react-query'; import { ReferenceOneField, @@ -590,3 +592,47 @@ export const WithRenderProp = () => ( /> ); + +export const Offline = () => { + return ( + + +
+ + + + + +
+ +
+
+ ); +}; + +const SimulateOfflineButton = () => { + const isOffline = useIsOffline(); + return ( + + ); +}; + +const RenderChildOnDemand = ({ children }) => { + const [showChild, setShowChild] = React.useState(false); + return ( + <> + + {showChild &&
{children}
} + + ); +}; diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx index 438c27ea228..d112ed34307 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx @@ -9,11 +9,12 @@ import { ReferenceOneFieldBase, UseReferenceResult, } from 'ra-core'; +import { useThemeProps } from '@mui/material/styles'; import { FieldProps } from './types'; import { ReferenceFieldView } from './ReferenceField'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; -import { useThemeProps } from '@mui/material/styles'; +import { Offline } from '../Offline'; /** * Render the related record in a one-to-one relationship @@ -47,6 +48,7 @@ export const ReferenceOneField = < sort, filter, link, + offline = defaultOffline, queryOptions, ...rest } = props; @@ -73,6 +75,7 @@ export const ReferenceOneField = < empty ?? null ) } + offline={offline} > ; + export interface ReferenceOneFieldProps< RecordType extends RaRecord = RaRecord, ReferenceRecordType extends RaRecord = RaRecord, @@ -103,6 +108,7 @@ export interface ReferenceOneFieldProps< */ emptyText?: string | ReactElement; empty?: ReactNode; + offline?: ReactNode; queryOptions?: Omit< UseQueryOptions<{ data: ReferenceRecordType[];