diff --git a/docs/AutocompleteArrayInput.md b/docs/AutocompleteArrayInput.md index 68dd30c4851..6e92fd5a3f8 100644 --- a/docs/AutocompleteArrayInput.md +++ b/docs/AutocompleteArrayInput.md @@ -68,6 +68,7 @@ The form value for the source must be an array of the selected values, e.g. | `filterToQuery` | Optional | `string` => `Object` | `q => ({ q })` | How to transform the searchText into a parameter for the data provider | | `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. | | `matchSuggestion` | Optional | `Function` | `-` | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` | +| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when fetching the choices | | `onChange` | Optional | `Function` | `-` | A function called with the new value, along with the selected records, when the input value changes | | `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | | `optionText` | Optional | `string` | `Function` | `Component` | `name` | Field name of record to display in the suggestion item or function which accepts the correct record as argument (`(record)=> {string}`) | @@ -323,6 +324,27 @@ const filterToQuery = searchText => ({ name_ilike: `%${searchText}%` }); ``` +## `offline` + +`` can display a custom message when it can't fetch the choices because there is no network connectivity, thanks to the `offline` prop. + +```jsx + + No network, could not fetch data} /> + +``` + +You can pass either a React element or a string to the `offline` prop: + +```jsx + + No network, could not fetch data} /> + + + + +``` + ## `onChange` Use the `onChange` prop to get notified when the input value changes. diff --git a/docs/AutocompleteInput.md b/docs/AutocompleteInput.md index 3719fc22270..722c9463eec 100644 --- a/docs/AutocompleteInput.md +++ b/docs/AutocompleteInput.md @@ -54,27 +54,28 @@ The form value for the source must be the selected value, e.g. ## Props -| Prop | Required | Type | Default | Description | -|--------------------------- |----------|-------------------------------------------------|-------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `choices` | Optional | `Object[]` | `-` | List of items to autosuggest. Required if not inside a ReferenceInput. | -| `create` | Optional | `Element` | `-` | A React Element to render when users want to create a new choice | -| `createLabel` | Optional | `string` | `ReactNode` | - | The label used as hint to let users know they can create a new choice. Displayed when the filter is empty. | -| `createItemLabel` | Optional | `string` | `(filter: string) => ReactNode` | `ra.action .create_item` | The label for the menu item allowing users to create a new choice. Used when the filter is not empty. | -| `debounce` | Optional | `number` | `250` | The delay to wait before calling the setFilter function injected when used in a ReferenceInput. | -| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | -| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | -| `filterToQuery` | Optional | `string` => `Object` | `q => ({ q })` | How to transform the searchText into a parameter for the data provider | -| `isPending` | Optional | `boolean` | `false` | If `true`, the component will display a loading indicator. | -| `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. | -| `matchSuggestion` | Optional | `Function` | `-` | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` | -| `onChange` | Optional | `Function` | `-` | A function called with the new value, along with the selected record, when the input value changes | -| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| Prop | Required | Type | Default | Description | +|--------------------------- |----------|-------------------------------------------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `choices` | Optional | `Object[]` | `-` | List of items to autosuggest. Required if not inside a ReferenceInput. | +| `create` | Optional | `Element` | `-` | A React Element to render when users want to create a new choice | +| `createLabel` | Optional | `string` | `ReactNode` | - | The label used as hint to let users know they can create a new choice. Displayed when the filter is empty. | +| `createItemLabel` | Optional | `string` | `(filter: string) => ReactNode` | `ra.action .create_item` | The label for the menu item allowing users to create a new choice. Used when the filter is not empty. | +| `debounce` | Optional | `number` | `250` | The delay to wait before calling the setFilter function injected when used in a ReferenceInput. | +| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | +| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | +| `filterToQuery` | Optional | `string` => `Object` | `q => ({ q })` | How to transform the searchText into a parameter for the data provider | +| `isPending` | Optional | `boolean` | `false` | If `true`, the component will display a loading indicator. | +| `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. | +| `matchSuggestion` | Optional | `Function` | `-` | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` | +| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when fetching the choices | +| `onChange` | Optional | `Function` | `-` | A function called with the new value, along with the selected record, when the input value changes | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | | `optionText` | Optional | `string` | `Function` | `Component` | `undefined` | `record Representation` | Field name of record to display in the suggestion item or function using the choice object as argument | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceInput`. | -| `shouldRender Suggestions` | Optional | `Function` | `() => true` | A function that returns a `boolean` to determine whether or not suggestions are rendered. | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | -| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceInput`. | +| `shouldRender Suggestions` | Optional | `Function` | `() => true` | A function that returns a `boolean` to determine whether or not suggestions are rendered. | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -370,6 +371,27 @@ const UserCountry = () => { } ``` +## `offline` + +`` can display a custom message when it can't fetch the choices because there is no network connectivity, thanks to the `offline` prop. + +```jsx + + No network, could not fetch data} /> + +``` + +You can pass either a React element or a string to the `offline` prop: + +```jsx + + No network, could not fetch data} /> + + + + +``` + ## `onChange` Use the `onChange` prop to get notified when the input value changes. diff --git a/docs/ReferenceInput.md b/docs/ReferenceInput.md index b5bd66e15c1..973af781c68 100644 --- a/docs/ReferenceInput.md +++ b/docs/ReferenceInput.md @@ -110,6 +110,7 @@ See the [`children`](#children) section for more details. | `label` | Optional | `string` | - | Useful only when `ReferenceInput` is in a Filter array, the label is used as the Filter label. | | `page` | Optional | `number` | 1 | The current page number | | `perPage` | Optional | `number` | 25 | Number of suggestions to show | +| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record | | `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:'DESC' }` | How to order the list of suggestions | @@ -191,6 +192,26 @@ const filters = [ ]; ``` +## `offline` + +`` can display a custom message when the referenced record is missing because there is no network connectivity, thanks to the `offline` prop. + +```jsx + +``` + +`` renders the `offline` element when: + +- the referenced record is missing (no record in the `users` table with the right `user_id`), and +- there is no network connectivity + +You can pass either a React element or a string to the `offline` prop: + +```jsx +No network, could not fetch data} /> + +``` + ## `parse` By default, children of `` transform the empty form value (an empty string) into `null` before passing it to the `dataProvider`. diff --git a/packages/ra-core/src/controller/input/ReferenceInputBase.spec.tsx b/packages/ra-core/src/controller/input/ReferenceInputBase.spec.tsx index 3de9b574334..2eaedc5e15c 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputBase.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputBase.spec.tsx @@ -17,6 +17,7 @@ import { SelfReference, QueryOptions, Meta, + Offline, } from './ReferenceInputBase.stories'; describe('', () => { @@ -68,8 +69,8 @@ describe('', () => { return
{resource}
; }; const dataProvider = testDataProvider({ - // @ts-ignore getList: () => + // @ts-ignore Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }), }); render( @@ -92,8 +93,8 @@ describe('', () => { return
{total}
; }; const dataProvider = testDataProvider({ - // @ts-ignore getList: () => + // @ts-ignore Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }), }); render( @@ -187,6 +188,15 @@ describe('', () => { screen.getByText('Save').click(); await screen.findByText('Proust', undefined, { timeout: 5000 }); }); + + 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('You are offline, cannot load data'); + fireEvent.click(await screen.findByText('Simulate online')); + await screen.findByText('lorem'); + }); }); const AutocompleteInput = ( diff --git a/packages/ra-core/src/controller/input/ReferenceInputBase.stories.tsx b/packages/ra-core/src/controller/input/ReferenceInputBase.stories.tsx index 3b60a44b893..9dabf015cd9 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputBase.stories.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputBase.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { QueryClient } from '@tanstack/react-query'; +import { onlineManager, QueryClient } from '@tanstack/react-query'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; import fakeRestDataProvider from 'ra-data-fakerest'; @@ -22,12 +22,13 @@ import { ListBase, Resource, testDataProvider, + useIsOffline, useListContext, useRedirect, } from '../..'; export default { - title: 'ra-core/input/ReferenceInputBase', + title: 'ra-core/controller/input/ReferenceInputBase', excludeStories: ['dataProviderWithAuthors'], }; @@ -213,7 +214,7 @@ const dataProvider = testDataProvider({ data: tags, total: tags.length, }), - 1500 + process.env.NODE_ENV === 'test' ? 0 : 1500 ); }), // @ts-ignore @@ -522,3 +523,54 @@ export const Meta = ({ ); + +export const Offline = () => ( + +
{}} defaultValues={{ tag_ids: 5 }}> +
+ + You are offline, cannot load data

} + > + +
+
+ +
+
+
+); + +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/input/ReferenceInputBase.tsx b/packages/ra-core/src/controller/input/ReferenceInputBase.tsx index d0425f2a18b..e18f3f05cc9 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputBase.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputBase.tsx @@ -70,6 +70,7 @@ export const ReferenceInputBase = (props: ReferenceInputBaseProps) => { reference, sort = { field: 'id', order: 'DESC' }, filter = {}, + offline, } = props; const controllerProps = useReferenceInputController({ @@ -78,10 +79,17 @@ export const ReferenceInputBase = (props: ReferenceInputBaseProps) => { filter, }); + const { isPaused, isPending } = controllerProps; + // isPending is true: there's no cached data and no query attempt was finished yet + // isPaused is true: the query was paused (e.g. due to a network issue) + // Both true: we're offline and have no data to show + const shouldRenderOffline = + isPaused && isPending && offline !== undefined && offline !== false; + return ( - {children} + {shouldRenderOffline ? offline : children} ); @@ -91,4 +99,5 @@ export interface ReferenceInputBaseProps extends InputProps, UseReferenceInputControllerParams { children?: ReactNode; + offline?: ReactNode; } diff --git a/packages/ra-core/src/controller/input/useReferenceInputController.ts b/packages/ra-core/src/controller/input/useReferenceInputController.ts index 2a384ea084b..928fe0762a9 100644 --- a/packages/ra-core/src/controller/input/useReferenceInputController.ts +++ b/packages/ra-core/src/controller/input/useReferenceInputController.ts @@ -84,12 +84,14 @@ export const useReferenceInputController = ( // fetch possible values const { - data: possibleValuesData = [], + data: possibleValuesData, total, pageInfo, isFetching: isFetchingPossibleValues, isLoading: isLoadingPossibleValues, + isPaused: isPausedPossibleValues, isPending: isPendingPossibleValues, + isPlaceholderData: isPlaceholderDataPossibleValues, error: errorPossibleValues, refetch: refetchGetList, } = useGetList( @@ -119,7 +121,9 @@ export const useReferenceInputController = ( error: errorReference, isLoading: isLoadingReference, isFetching: isFetchingReference, + isPaused: isPausedReference, isPending: isPendingReference, + isPlaceholderData: isPlaceholderDataReference, } = useReference({ id: currentValue, reference, @@ -136,6 +140,8 @@ export const useReferenceInputController = ( (currentValue != null && currentValue !== '' && isPendingReference) || isPendingPossibleValues; + const isPaused = isPausedReference || isPausedPossibleValues; + // We need to delay the update of the referenceRecord and the finalData // to the next React state update, because otherwise it can raise a warning // with AutocompleteInput saying the current value is not in the list of choices @@ -147,17 +153,31 @@ export const useReferenceInputController = ( }, [currentReferenceRecord]); // add current value to possible sources - let finalData: RecordType[], finalTotal: number | undefined; - if ( - !referenceRecord || - possibleValuesData.find(record => record.id === referenceRecord.id) - ) { - finalData = possibleValuesData; - finalTotal = total; - } else { - finalData = [referenceRecord, ...possibleValuesData]; - finalTotal = total == null ? undefined : total + 1; - } + const { finalData, finalTotal } = useMemo(() => { + if (isPaused && possibleValuesData == null) { + return { + finalData: null, + finalTotal: null, + }; + } + if ( + !referenceRecord || + possibleValuesData == null || + (possibleValuesData ?? []).find( + record => record.id === referenceRecord.id + ) + ) { + return { + finalData: possibleValuesData, + finalTotal: total, + }; + } else { + return { + finalData: [referenceRecord, ...(possibleValuesData ?? [])], + finalTotal: total == null ? undefined : total + 1, + }; + } + }, [isPaused, referenceRecord, possibleValuesData, total]); const refetch = useCallback(() => { refetchGetList(); @@ -171,39 +191,85 @@ export const useReferenceInputController = ( }), [params.sort, params.order] ); - return { - sort: currentSort, - allChoices: finalData, - availableChoices: possibleValuesData, - selectedChoices: referenceRecord ? [referenceRecord] : [], - displayedFilters: params.displayedFilters, - error: errorReference || errorPossibleValues, - filter: params.filter, - filterValues: params.filterValues, - hideFilter: paramsModifiers.hideFilter, - isFetching: isFetchingReference || isFetchingPossibleValues, - isLoading: isLoadingReference || isLoadingPossibleValues, - isPending: isPending, - page: params.page, - perPage: params.perPage, - refetch, - resource: reference, - setFilters: paramsModifiers.setFilters, - setPage: paramsModifiers.setPage, - setPerPage: paramsModifiers.setPerPage, - setSort: paramsModifiers.setSort, - showFilter: paramsModifiers.showFilter, - // we return source and not finalSource because child inputs (e.g. AutocompleteInput) already call useInput and compute the final source - source, - total: finalTotal, - hasNextPage: pageInfo - ? pageInfo.hasNextPage - : total != null - ? params.page * params.perPage < total - : undefined, - hasPreviousPage: pageInfo ? pageInfo.hasPreviousPage : params.page > 1, - isFromReference: true, - } as ChoicesContextValue; + return useMemo( + () => + ({ + sort: currentSort, + // TODO v6: we shouldn't return a default empty array. This is actually enforced at the type level in other hooks such as useListController + allChoices: finalData ?? [], + // TODO v6: same as above + availableChoices: possibleValuesData ?? [], + // TODO v6: same as above + selectedChoices: referenceRecord ? [referenceRecord] : [], + displayedFilters: params.displayedFilters, + error: errorReference || errorPossibleValues, + filter: params.filter, + filterValues: params.filterValues, + hideFilter: paramsModifiers.hideFilter, + isFetching: isFetchingReference || isFetchingPossibleValues, + isLoading: isLoadingReference || isLoadingPossibleValues, + isPaused: isPausedReference || isPausedPossibleValues, + isPending, + isPlaceholderData: + isPlaceholderDataReference || + isPlaceholderDataPossibleValues, + page: params.page, + perPage: params.perPage, + refetch, + resource: reference, + setFilters: paramsModifiers.setFilters, + setPage: paramsModifiers.setPage, + setPerPage: paramsModifiers.setPerPage, + setSort: paramsModifiers.setSort, + showFilter: paramsModifiers.showFilter, + // we return source and not finalSource because child inputs (e.g. AutocompleteInput) already call useInput and compute the final source + source, + total: finalTotal, + hasNextPage: pageInfo + ? pageInfo.hasNextPage + : total != null + ? params.page * params.perPage < total + : undefined, + hasPreviousPage: pageInfo + ? pageInfo.hasPreviousPage + : params.page > 1, + isFromReference: true, + }) as ChoicesContextValue, + [ + currentSort, + errorPossibleValues, + errorReference, + finalData, + finalTotal, + isFetchingPossibleValues, + isFetchingReference, + isLoadingPossibleValues, + isLoadingReference, + isPausedPossibleValues, + isPausedReference, + isPending, + isPlaceholderDataReference, + isPlaceholderDataPossibleValues, + pageInfo, + params.displayedFilters, + params.filter, + params.filterValues, + params.page, + params.perPage, + paramsModifiers.hideFilter, + paramsModifiers.setFilters, + paramsModifiers.setPage, + paramsModifiers.setPerPage, + paramsModifiers.setSort, + paramsModifiers.showFilter, + possibleValuesData, + reference, + referenceRecord, + refetch, + source, + total, + ] + ); }; export interface UseReferenceInputControllerParams< diff --git a/packages/ra-core/src/form/choices/ChoicesContext.ts b/packages/ra-core/src/form/choices/ChoicesContext.ts index 6e0b53dee80..a420a6351b0 100644 --- a/packages/ra-core/src/form/choices/ChoicesContext.ts +++ b/packages/ra-core/src/form/choices/ChoicesContext.ts @@ -20,6 +20,8 @@ export type ChoicesContextBaseValue = { hideFilter: (filterName: string) => void; isFetching: boolean; isLoading: boolean; + isPaused: boolean; + isPlaceholderData: boolean; page: number; perPage: number; refetch: (() => void) | UseGetListHookValue['refetch']; diff --git a/packages/ra-core/src/form/choices/useChoicesContext.ts b/packages/ra-core/src/form/choices/useChoicesContext.ts index e7a31c13684..03f057d521c 100644 --- a/packages/ra-core/src/form/choices/useChoicesContext.ts +++ b/packages/ra-core/src/form/choices/useChoicesContext.ts @@ -41,9 +41,11 @@ export const useChoicesContext = ( hasPreviousPage: options.hasPreviousPage ?? list.hasPreviousPage, hideFilter: options.hideFilter ?? list.hideFilter, - isLoading: list.isLoading ?? false, // we must take the one for useList, otherwise the loading state isn't synchronized with the data + isFetching: list.isFetching ?? false, // we must take the one for useList, otherwise the loading state isn't synchronized with the data + isLoading: list.isLoading ?? false, // same + isPaused: list.isPaused ?? false, // same isPending: list.isPending ?? false, // same - isFetching: list.isFetching ?? false, // same + isPlaceholderData: list.isPlaceholderData ?? false, // same page: options.page ?? list.page, perPage: options.perPage ?? list.perPage, refetch: options.refetch ?? list.refetch, diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index a21ebf4ab9c..3a84dce2582 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -48,6 +48,7 @@ import { import type { CommonInputProps } from './CommonInputProps'; import { InputHelperText } from './InputHelperText'; import { sanitizeInputRestProps } from './sanitizeInputRestProps'; +import { Offline } from '../Offline'; const defaultFilterOptions = createFilterOptions(); @@ -161,6 +162,7 @@ export const AutocompleteInput = < isRequired: isRequiredOverride, label, limitChoicesToValue, + loadingText = 'ra.message.loading', matchSuggestion, margin, fieldState: fieldStateOverride, @@ -168,6 +170,7 @@ export const AutocompleteInput = < formState: formStateOverride, multiple = false, noOptionsText, + offline = defaultOffline, onBlur, onChange, onCreate, @@ -196,7 +199,9 @@ export const AutocompleteInput = < const { allChoices, + isPaused, isPending, + isPlaceholderData, error: fetchError, resource, source, @@ -621,12 +626,33 @@ If you provided a React element for the optionText prop, you must also provide t const renderHelperText = !!fetchError || helperText !== false || invalid; const handleInputRef = useForkRef(field.ref, TextFieldProps?.inputRef); + // isPending is true: there's no cached data and no query attempt was finished yet + // isPaused is true: the query was paused (e.g. due to a network issue) + // Both true: we're offline, have no data to show + // If the component that provides the ChoicesContext does not handle this case, we should should render the offline element + if (isPending && isPaused && offline !== false && offline !== undefined) { + return offline; + } + + const finalLoadingText = + typeof loadingText === 'string' + ? translate(loadingText, { + _: loadingText, + }) + : loadingText; return ( <> option?.id} getOptionLabel={getOptionLabelString} inputValue={filterValue} loading={ - isPending && - (!finalChoices || finalChoices.length === 0) && - oneSecondHasPassed + (isPending && + (!finalChoices || finalChoices.length === 0) && + oneSecondHasPassed) || + (isPaused && isPlaceholderData) } value={selectedChoice} onChange={handleAutocompleteChange} @@ -810,6 +839,7 @@ export interface AutocompleteInputProps< emptyValue?: any; filterToQuery?: (searchText: string) => any; inputText?: (option: any) => string; + offline?: ReactNode; onChange?: ( // We can't know upfront what the value type will be value: Multiple extends true ? any[] : any, @@ -924,6 +954,7 @@ const areSelectedItemsEqual = ( }; const DefaultFilterToQuery = searchText => ({ q: searchText }); +const defaultOffline = ; declare module '@mui/material/styles' { interface ComponentNameToClassKey { diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx index d2537c5ba42..27041a336b8 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.stories.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; import { Admin, DataTable, List } from 'react-admin'; -import { QueryClient } from '@tanstack/react-query'; +import { onlineManager, QueryClient } from '@tanstack/react-query'; import { Resource, Form, testDataProvider, useRedirect, TestMemoryRouter, + useIsOffline, } from 'ra-core'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; @@ -726,3 +727,109 @@ export const InArrayInput = () => ( ); + +export const Offline = () => ( + + + + `${record.first_name} ${record.last_name}` + } + /> + + + + + + + + +

+ +

+ + } + /> +
+
+); + +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/input/ReferenceInput.tsx b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx index 0394f49622c..5b3fa743f66 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.tsx +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { ReferenceInputBase, ReferenceInputBaseProps } from 'ra-core'; - +import { Offline } from '../Offline'; import { AutocompleteInput } from './AutocompleteInput'; /** @@ -64,7 +64,11 @@ import { AutocompleteInput } from './AutocompleteInput'; * a `setFilters` function. You can call this function to filter the results. */ export const ReferenceInput = (props: ReferenceInputProps) => { - const { children = defaultChildren, ...rest } = props; + const { + children = defaultChildren, + offline = defaultOffline, + ...rest + } = props; if (props.validate && process.env.NODE_ENV !== 'production') { throw new Error( @@ -72,11 +76,15 @@ export const ReferenceInput = (props: ReferenceInputProps) => { ); } - return {children}; + return ( + + {children} + + ); }; const defaultChildren = ; - +const defaultOffline = ; export interface ReferenceInputProps extends ReferenceInputBaseProps { /** * Call validate on the child component instead