diff --git a/docs/ReferenceField.md b/docs/ReferenceField.md index 00ca19f5416..f3c362e758c 100644 --- a/docs/ReferenceField.md +++ b/docs/ReferenceField.md @@ -74,7 +74,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform | `source` | Required | `string` | - | Name of the property to display | | `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' | | `children` | Optional | `ReactNode` | - | One or more Field elements used to render the referenced record | -| `emptyText` | Optional | `string` | '' | Defines a text to be shown when the field has no value or when the reference is missing | +| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing | | `label` | Optional | `string | Function` | `resources. [resource]. fields.[source]` | Label to use for the field when rendered in layout components | | `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. | | `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options | @@ -82,18 +82,30 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform `` also accepts the [common field props](./Fields.md#common-field-props). -## `emptyText` +## `empty` -`` can display a custom message when the referenced record is missing, thanks to the `emptyText` prop. +`` can display a custom message when the referenced record is missing, thanks to the `empty` prop. ```jsx - + ``` -`` renders the `emptyText`: +`` renders the `empty` element when: -- when the referenced record is missing (no record in the `users` table with the right `user_id`), or -- when the field is empty (no `user_id` in the record). +- the referenced record is missing (no record in the `users` table with the right `user_id`), or +- the field is empty (no `user_id` in the record). + +When `empty` is a string, `` renders it as a `` and passes the text through the i18n system, so you can use translation keys to have one message for each language supported by the interface: + +```jsx + +``` + +You can also pass a React element to the `empty` prop: + +```jsx +Missing user} /> +``` ## `label` diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md index 7d0464e4f9d..c52bd2b4188 100644 --- a/docs/ReferenceManyField.md +++ b/docs/ReferenceManyField.md @@ -90,6 +90,7 @@ This example leverages [``](./SingleFieldList.md) to display an | -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- | | `children` | Required | `Element` | - | One or several elements that render a list of records based on a `ListContext` | | `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()` | | `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 | @@ -176,6 +177,44 @@ const PostCommentsField = () => ( ); ``` +## `empty` + +Use `empty` to customize the text displayed when the related record is empty. + +```jsx + + ... + +``` + +`empty` also accepts a translation key. + +```jsx + + ... + +``` + +`empty` also accepts a `ReactNode`. + +```jsx +} +> + ... + +``` + ## `filter`: Permanent Filter You can filter the query used to populate the possible values. Use the `filter` prop for that. diff --git a/docs/ReferenceOneField.md b/docs/ReferenceOneField.md index d0a3547c7d9..b82a559e769 100644 --- a/docs/ReferenceOneField.md +++ b/docs/ReferenceOneField.md @@ -59,6 +59,7 @@ const BookShow = () => ( | `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' | | `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' | | `children` | Optional | `Element` | - | The Field element used to render the referenced record | +| `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. | | `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options | @@ -78,32 +79,32 @@ For instance, if you want to render both the genre and the ISBN for a book: ``` -## `emptyText` +## `empty` -Use `emptyText` to customize the text displayed when the related record is empty. +Use `empty` to customize the text displayed when the related record is empty. ```jsx - + () ``` -`emptyText` also accepts a translation key. +`empty` also accepts a translation key. ```jsx - + () ``` -`emptyText` also accepts a `ReactElement`. +`empty` also accepts a `ReactNode`. ```jsx } + empty={} > () diff --git a/docs/WithRecord.md b/docs/WithRecord.md index e412889927e..abd3e4e46af 100644 --- a/docs/WithRecord.md +++ b/docs/WithRecord.md @@ -23,7 +23,7 @@ const BookShow = () => ( ); ``` -Note that if `record` is undefined, `` doesn't call the `render` callback and renders nothing, so you don't have to worry about this case in your render callback. +Note that if `record` is undefined, `` doesn't call the `render` callback and renders nothing (or the `empty` prop), so you don't have to worry about this case in your render callback. ## Availability diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx index 17e8a123e5b..6bdb4d49541 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx @@ -8,12 +8,6 @@ import { ReferenceFieldBase } from './ReferenceFieldBase'; import { Error, Loading, Meta } from './ReferenceFieldBase.stories'; describe('', () => { - const defaultProps = { - reference: 'posts', - resource: 'comments', - source: 'post_id', - }; - beforeAll(() => { window.scrollTo = jest.fn(); }); @@ -52,7 +46,7 @@ describe('', () => { }); render( - + diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx index dde3c35e727..7b4a6a065b8 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx @@ -6,6 +6,7 @@ import { RaRecord } from '../../types'; import { useReferenceFieldController } from './useReferenceFieldController'; import { ResourceContextProvider } from '../../core'; import { RecordContextProvider } from '../record'; +import { useFieldValue } from '../../util'; /** * Fetch reference record, and render its representation, or delegate rendering to child component. @@ -43,11 +44,22 @@ export const ReferenceFieldBase = < >( props: ReferenceFieldBaseProps ) => { - const { children } = props; - + const { children, empty = null } = props; + const id = useFieldValue(props); const controllerProps = useReferenceFieldController(props); + if ( + (empty && + // no foreign key value + !id) || + // no reference record + (!controllerProps.error && + !controllerProps.isPending && + !controllerProps.referenceRecord) + ) { + return empty; + } return ( @@ -64,6 +76,7 @@ export interface ReferenceFieldBaseProps< > { children?: ReactNode; className?: string; + empty?: ReactNode; error?: ReactNode; queryOptions?: Partial< UseQueryOptions & { diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx new file mode 100644 index 00000000000..d969805f7f3 --- /dev/null +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx @@ -0,0 +1,143 @@ +import React, { ReactNode } from 'react'; +import { ResourceContextProvider } from '../../core'; +import { ListContextProvider } from '../list/ListContextProvider'; +import { + useReferenceManyFieldController, + type UseReferenceManyFieldControllerParams, +} from './useReferenceManyFieldController'; +import type { RaRecord } from '../../types'; + +/** + * Render related records to the current one. + * + * You must define the fields to be passed to the iterator component as children. + * + * @example Display all the comments of the current post as a datagrid + * + * + * + * + * + * + * + * + * + * @example Display all the books by the current author, only the title + * + * + * + * + * + * + * By default, restricts the displayed values to 25. You can extend this limit + * by setting the `perPage` prop. + * + * @example + * + * ... + * + * + * By default, orders the possible values by id desc. You can change this order + * by setting the `sort` prop (an object with `field` and `order` properties). + * + * @example + * + * ... + * + * + * Also, you can filter the query used to populate the possible values. Use the + * `filter` prop for that. + * + * @example + * + * ... + * + */ +export const ReferenceManyFieldBase = < + RecordType extends RaRecord = RaRecord, + ReferenceRecordType extends RaRecord = RaRecord, +>( + props: ReferenceManyFieldBaseProps +) => { + const { + children, + debounce, + empty, + filter = defaultFilter, + page = 1, + pagination = null, + perPage = 25, + record, + reference, + resource, + sort = defaultSort, + source = 'id', + storeKey, + target, + queryOptions, + } = props; + + const controllerProps = useReferenceManyFieldController< + RecordType, + ReferenceRecordType + >({ + debounce, + filter, + page, + perPage, + record, + reference, + resource, + sort, + source, + storeKey, + target, + queryOptions, + }); + + if ( + // there is an empty page component + empty && + // there is no error + !controllerProps.error && + // the list is not loading data for the first time + !controllerProps.isPending && + // the API returned no data (using either normal or partial pagination) + (controllerProps.total === 0 || + (controllerProps.total == null && + // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it + controllerProps.hasPreviousPage === false && + // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it + controllerProps.hasNextPage === false && + // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it + controllerProps.data.length === 0)) && + // the user didn't set any filters + !Object.keys(controllerProps.filterValues).length + ) { + return empty; + } + + return ( + + + {children} + {pagination} + + + ); +}; + +export interface ReferenceManyFieldBaseProps< + RecordType extends Record = Record, + ReferenceRecordType extends Record = Record, +> extends UseReferenceManyFieldControllerParams< + RecordType, + ReferenceRecordType + > { + children: ReactNode; + empty?: ReactNode; + pagination?: ReactNode; +} + +const defaultFilter = {}; +const defaultSort = { field: 'id', order: 'DESC' as const }; diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx new file mode 100644 index 00000000000..c54f73aa5f1 --- /dev/null +++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx @@ -0,0 +1,100 @@ +import React, { ReactNode, useMemo } from 'react'; +import { + useReferenceOneFieldController, + UseReferenceOneFieldControllerParams, +} from './useReferenceOneFieldController'; +import { useRecordContext, RecordContextProvider } from '../record'; +import { ResourceContextProvider } from '../../core'; +import { ReferenceFieldContextProvider } from './ReferenceFieldContext'; +import { useGetPathForRecord } from '../../routing'; +import type { UseReferenceFieldControllerResult } from './useReferenceFieldController'; +import type { RaRecord } from '../../types'; +import type { LinkToType } from '../../routing'; + +/** + * Render the related record in a one-to-one relationship + * + * Expects a single field as child + * + * @example // display the bio of the current author + * + * + * + */ +export const ReferenceOneFieldBase = < + RecordType extends RaRecord = RaRecord, + ReferenceRecordType extends RaRecord = RaRecord, +>( + props: ReferenceOneFieldBaseProps +) => { + const { + children, + record, + reference, + source = 'id', + target, + empty, + sort, + filter, + link, + queryOptions, + } = props; + + const controllerProps = useReferenceOneFieldController< + RecordType, + ReferenceRecordType + >({ + record, + reference, + source, + target, + sort, + filter, + queryOptions, + }); + + const path = useGetPathForRecord({ + record: controllerProps.referenceRecord, + resource: reference, + link, + }); + + const context = useMemo( + () => ({ + ...controllerProps, + link: path, + }), + [controllerProps, path] + ); + + const recordFromContext = useRecordContext(props); + if ( + !recordFromContext || + (!controllerProps.isPending && controllerProps.referenceRecord == null) + ) { + return empty; + } + + return ( + + + + {children} + + + + ); +}; + +export interface ReferenceOneFieldBaseProps< + RecordType extends RaRecord = RaRecord, + ReferenceRecordType extends RaRecord = RaRecord, +> extends UseReferenceOneFieldControllerParams< + RecordType, + ReferenceRecordType + > { + children?: ReactNode; + link?: LinkToType; + empty?: ReactNode; + resource?: string; +} diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts index d03d0e239fd..873cce05bdb 100644 --- a/packages/ra-core/src/controller/field/index.ts +++ b/packages/ra-core/src/controller/field/index.ts @@ -1,6 +1,8 @@ +export * from './ReferenceOneFieldBase'; export * from './ReferenceFieldBase'; export * from './ReferenceFieldContext'; export * from './ReferenceManyCountBase'; +export * from './ReferenceManyFieldBase'; export * from './useReferenceArrayFieldController'; export * from './useReferenceFieldController'; export * from './useReferenceManyFieldController'; diff --git a/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx b/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx index b4ab292201d..fbba23a4106 100644 --- a/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx +++ b/packages/ra-core/src/controller/field/useReferenceOneFieldController.tsx @@ -1,6 +1,7 @@ import get from 'lodash/get'; import { UseQueryOptions } from '@tanstack/react-query'; +import { useRecordContext } from '../record'; import { useGetManyReference } from '../../dataProvider'; import { useNotify } from '../../notification'; import { RaRecord, SortPayload } from '../../types'; @@ -8,24 +9,25 @@ import { UseReferenceResult } from '../useReference'; export interface UseReferenceOneFieldControllerParams< RecordType extends RaRecord = any, + ReferenceRecordType extends RaRecord = any, ErrorType = Error, > { - record?: RaRecord; reference: string; - source?: string; target: string; - sort?: SortPayload; filter?: any; queryOptions?: Omit< UseQueryOptions< { - data: RecordType[]; + data: ReferenceRecordType[]; total: number; }, ErrorType >, 'queryFn' | 'queryKey' > & { meta?: any }; + record?: RecordType; + sort?: SortPayload; + source?: string; } /** @@ -53,24 +55,29 @@ export interface UseReferenceOneFieldControllerParams< */ export const useReferenceOneFieldController = < RecordType extends RaRecord = any, + ReferenceRecordType extends RaRecord = any, ErrorType = Error, >( - props: UseReferenceOneFieldControllerParams -): UseReferenceResult => { + props: UseReferenceOneFieldControllerParams< + RecordType, + ReferenceRecordType, + ErrorType + > +): UseReferenceResult => { const { reference, - record, target, source = 'id', sort = { field: 'id', order: 'ASC' }, filter = {}, queryOptions = {}, } = props; + const record = useRecordContext(props); const notify = useNotify(); const { meta, ...otherQueryOptions } = queryOptions; const { data, error, isFetching, isLoading, isPending, refetch } = - useGetManyReference( + useGetManyReference( reference, { target, diff --git a/packages/ra-core/src/controller/record/WithRecord.tsx b/packages/ra-core/src/controller/record/WithRecord.tsx index 094781f4015..e74f2a03cf6 100644 --- a/packages/ra-core/src/controller/record/WithRecord.tsx +++ b/packages/ra-core/src/controller/record/WithRecord.tsx @@ -16,12 +16,14 @@ import { useRecordContext } from './useRecordContext'; */ export const WithRecord = = any>({ render, + empty = null, }: WithRecordProps) => { const record = useRecordContext(); - return record ? <>{render(record)} : null; + return record ? <>{render(record)} : empty; }; export interface WithRecordProps = any> { render: (record: RecordType) => ReactNode; + empty?: ReactNode; label?: string; } diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx index d80e2e7f90e..00e483d2c88 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx @@ -20,8 +20,10 @@ import { LinkDefaultShowView, LinkMissingView, LinkFalse, - MissingReferenceIdEmptyTextTranslation, MissingReferenceEmptyText, + MissingReferenceIdEmptyTextTranslation, + MissingReferenceIdEmpty, + MissingReferenceIdEmptyTranslation, SXLink, SXNoLink, SlowAccessControl, @@ -293,29 +295,48 @@ describe('', () => { }); }); - it('should display the emptyText if the field is empty', () => { - render( - - - - - - - - ); - expect(screen.getByText('EMPTY')).not.toBeNull(); + describe('emptyText', () => { + it('should display the emptyText if the field is empty', () => { + render( + + + + + + + + ); + expect(screen.getByText('EMPTY')).not.toBeNull(); + }); + + it('should display the emptyText if there is no reference', async () => { + render(); + await screen.findByText('no detail'); + }); + + it('should translate emptyText', async () => { + render(); + + expect(await screen.findByText('Not found')).not.toBeNull(); + }); }); - it('should display the emptyText if there is no reference', async () => { - render(); - await screen.findByText('no detail'); + describe('empty', () => { + it('should render the empty prop when the record is not found', async () => { + render(); + await screen.findByText('no detail'); + }); + it('should translate empty if it is a string', async () => { + render(); + await screen.findByText('Not found'); + }); }); it('should use record from RecordContext', async () => { @@ -582,12 +603,6 @@ describe('', () => { expect(await screen.findByText('novel')).not.toBeNull(); }); - it('should translate emptyText', async () => { - render(); - - expect(await screen.findByText('Not found')).not.toBeNull(); - }); - it('should accept a queryOptions prop', async () => { const dataProvider = testDataProvider({ getMany: jest.fn().mockResolvedValue({ diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx index 6da8201be34..e75be74ba98 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx @@ -158,6 +158,32 @@ export const MissingReferenceIdEmptyTextTranslation = () => ( ); +export const MissingReferenceIdEmpty = () => ( + + no detail} + > + + + +); + +export const MissingReferenceIdEmptyTranslation = () => ( + + + + + + + +); + const missingReferenceDataProvider = { getMany: () => Promise.resolve({ diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx index 87c3098492b..2afe09b9e8d 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx @@ -16,7 +16,6 @@ import { type RaRecord, ReferenceFieldBase, useReferenceFieldContext, - useFieldValue, } from 'ra-core'; import type { UseQueryOptions } from '@tanstack/react-query'; import clsx from 'clsx'; @@ -68,20 +67,26 @@ export const ReferenceField = < props: inProps, name: PREFIX, }); - const { emptyText } = props; + const { emptyText, empty } = props; const translate = useTranslate(); - const id = useFieldValue(props); - - if (id == null) { - return emptyText ? ( - - {emptyText && translate(emptyText, { _: emptyText })} - - ) : null; - } return ( - {...props}> + + {...props} + empty={ + emptyText ? ( + + {emptyText && translate(emptyText, { _: emptyText })} + + ) : typeof empty === 'string' ? ( + + {empty && translate(empty, { _: empty })} + + ) : ( + empty ?? null + ) + } + > {...props} /> @@ -94,6 +99,11 @@ export interface ReferenceFieldProps< ReferenceRecordType extends RaRecord = RaRecord, > extends FieldProps { children?: ReactNode; + /** + * @deprecated Use the empty prop instead + */ + emptyText?: string; + empty?: ReactNode; queryOptions?: Omit< UseQueryOptions, 'queryFn' | 'queryKey' @@ -122,7 +132,6 @@ export const ReferenceFieldView = < useReferenceFieldContext(); const getRecordRepresentation = useGetRecordRepresentation(reference); - const translate = useTranslate(); if (error) { return ( @@ -140,13 +149,6 @@ export const ReferenceFieldView = < if (isLoading) { return ; } - if (!referenceRecord) { - return emptyText ? ( - - {emptyText && translate(emptyText, { _: emptyText })} - - ) : null; - } const child = children || ( diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx index 2580bccc86c..db966fcb3f0 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx @@ -11,6 +11,7 @@ import { SingleFieldList } from '../list/SingleFieldList'; import { Pagination } from '../list/pagination/Pagination'; import { Basic, + Empty, WithPagination, WithPaginationAndSelectAllLimit, } from './ReferenceManyField.stories'; @@ -274,6 +275,13 @@ describe('', () => { }); }); + describe('empty', () => { + it('should render the empty prop when the record is not found', async () => { + render(); + await screen.findByText('no books'); + }); + }); + describe('"Select all" button', () => { it('should be displayed if all the items of the page are selected', async () => { render(); diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx index af67b19b2dc..0b72a00b96d 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx @@ -123,6 +123,25 @@ export const Basic = () => ( ); +export const Empty = () => ( + + + + + + + +); + export const WithSingleFieldList = () => ( diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx index ee92d6c8dc1..c044075c2ac 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx @@ -1,13 +1,13 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React from 'react'; import { - useReferenceManyFieldController, - ListContextProvider, - ResourceContextProvider, - RaRecord, - UseReferenceManyFieldControllerParams, + ReferenceManyFieldBase, + useTranslate, + type ReferenceManyFieldBaseProps, + type RaRecord, } from 'ra-core'; -import { FieldProps } from './types'; +import { Typography } from '@mui/material'; +import type { FieldProps } from './types'; /** * Render related records to the current one. @@ -61,48 +61,20 @@ export const ReferenceManyField = < >( props: ReferenceManyFieldProps ) => { - const { - children, - debounce, - filter = defaultFilter, - page = 1, - pagination = null, - perPage = 25, - record, - reference, - resource, - sort = defaultSort, - source = 'id', - storeKey, - target, - queryOptions, - } = props; - - const controllerProps = useReferenceManyFieldController< - RecordType, - ReferenceRecordType - >({ - debounce, - filter, - page, - perPage, - record, - reference, - resource, - sort, - source, - storeKey, - target, - queryOptions, - }); - + const translate = useTranslate(); return ( - - - {children} - {pagination} - - + + {...props} + empty={ + typeof props.empty === 'string' ? ( + + {translate(props.empty, { _: props.empty })} + + ) : ( + props.empty + ) + } + /> ); }; @@ -110,10 +82,4 @@ export interface ReferenceManyFieldProps< RecordType extends Record = Record, ReferenceRecordType extends Record = Record, > extends Omit, 'source'>, - UseReferenceManyFieldControllerParams { - children: ReactNode; - pagination?: ReactElement; -} - -const defaultFilter = {}; -const defaultSort = { field: 'id', order: 'DESC' as const }; + ReferenceManyFieldBaseProps {} diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx index ad8e1e043b0..ee10bc6eb60 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx @@ -4,9 +4,10 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { RecordRepresentation, Basic, - EmptyWithTranslate, + EmptyTextWithTranslate, QueryOptions, EmptyText, + Empty, Themed, } from './ReferenceOneField.stories'; @@ -22,7 +23,7 @@ describe('ReferenceOneField', () => { }); it('should translate emptyText', async () => { - render(); + render(); await screen.findByText('Not found'); }); @@ -53,13 +54,26 @@ describe('ReferenceOneField', () => { }); }); - it('should render the "emptyContent" prop when the record is not found', async () => { - render(); - await waitFor(() => { - expect(screen.queryAllByText('no detail')).toHaveLength(3); + describe('emptyText', () => { + it('should render the emptyText prop when the record is not found', async () => { + render(); + await waitFor(() => { + expect(screen.queryAllByText('no detail')).toHaveLength(3); + }); + fireEvent.click(screen.getByText('War and Peace')); + await screen.findByText('Create'); + }); + }); + + describe('empty', () => { + it('should render the empty prop when the record is not found', async () => { + render(); + await waitFor(() => { + expect(screen.queryAllByText('no detail')).toHaveLength(3); + }); + fireEvent.click(screen.getByText('War and Peace')); + await screen.findByText('Create'); }); - fireEvent.click(screen.getByText('War and Peace')); - await screen.findByText('Create'); }); it('should be customized by a theme', async () => { diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx index eb24c8ed8b0..92ccaf4ef27 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx @@ -230,7 +230,7 @@ export const EmptyText = () => ( ); -export const EmptyWithTranslate = () => ( +export const EmptyTextWithTranslate = () => ( ( ); +export const Empty = () => ( + + + + ( + + + + + + + + + + + + )} + show={() => ( + + + + + + + + } + > + + + + + )} + /> + ( + + + + + + + + )} + create={() => ( + + + + + + + )} + /> + + + +); + +export const EmptyWithTranslate = () => ( + + + + + + + +); + export const Link = () => ( (props); const translate = useTranslate(); - const controllerProps = useReferenceOneFieldController( - { - record, - reference, - source, - target, - sort, - filter, - queryOptions, - } - ); - - const path = useGetPathForRecord({ - record: controllerProps.referenceRecord, - resource: reference, - link, - }); - - const context = useMemo( - () => ({ - ...controllerProps, - link: path, - }), - [controllerProps, path] - ); - - const empty = - typeof emptyText === 'string' ? ( - - {emptyText && translate(emptyText, { _: emptyText })} - - ) : emptyText ? ( - emptyText - ) : null; - - return !record || - (!controllerProps.isPending && - controllerProps.referenceRecord == null) ? ( - empty - ) : ( - - - - - {children} - - - - + return ( + + {emptyText && + translate(emptyText, { _: emptyText })} + + ) : ( + emptyText + ) + ) : typeof empty === 'string' ? ( + + {empty && translate(empty, { _: empty })} + + ) : ( + empty ?? null + ) + } + > + + {children} + + ); }; @@ -123,7 +94,11 @@ export interface ReferenceOneFieldProps< source?: string; filter?: any; link?: LinkToType; + /** + * @deprecated Use the empty prop instead + */ emptyText?: string | ReactElement; + empty?: ReactNode; queryOptions?: Omit< UseQueryOptions<{ data: ReferenceRecordType[];