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
26 changes: 19 additions & 7 deletions docs/ReferenceField.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,38 @@ 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 |
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |

`<ReferenceField>` also accepts the [common field props](./Fields.md#common-field-props).

## `emptyText`
## `empty`

`<ReferenceField>` can display a custom message when the referenced record is missing, thanks to the `emptyText` prop.
`<ReferenceField>` can display a custom message when the referenced record is missing, thanks to the `empty` prop.

```jsx
<ReferenceField source="user_id" reference="users" emptyText="Missing user" />
<ReferenceField source="user_id" reference="users" empty="Missing user" />
```

`<ReferenceField>` renders the `emptyText`:
`<ReferenceField>` 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, `<ReferenceField>` renders it as a `<Typography>` 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
<ReferenceField source="user_id" reference="users" empty="resources.users.missing" />
```

You can also pass a React element to the `empty` prop:

```jsx
<ReferenceField source="user_id" reference="users" empty={<span>Missing user</span>} />
```

## `label`

Expand Down
39 changes: 39 additions & 0 deletions docs/ReferenceManyField.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ This example leverages [`<SingleFieldList>`](./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 |
Expand Down Expand Up @@ -176,6 +177,44 @@ const PostCommentsField = () => (
);
```

## `empty`

Use `empty` to customize the text displayed when the related record is empty.

```jsx
<ReferenceManyField
reference="books"
target="author_id"
empty="no books"
>
...
</ReferenceManyField>
```

`empty` also accepts a translation key.

```jsx
<ReferenceManyField
reference="books"
target="author_id"
empty="resources.authors.fields.books.empty"
>
...
</ReferenceManyField>
```

`empty` also accepts a `ReactNode`.

```jsx
<ReferenceManyField
reference="books"
target="author_id"
empty={<CreateButton resource="books" />}
>
...
</ReferenceManyField>
```

## `filter`: Permanent Filter

You can filter the query used to populate the possible values. Use the `filter` prop for that.
Expand Down
15 changes: 8 additions & 7 deletions docs/ReferenceOneField.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -78,32 +79,32 @@ For instance, if you want to render both the genre and the ISBN for a book:
</ReferenceOneField>
```

## `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
<ReferenceOneField label="Details" reference="book_details" target="book_id" emptyText="no detail">
<ReferenceOneField label="Details" reference="book_details" target="book_id" empty="no detail">
<TextField source="genre" /> (<TextField source="ISBN" />)
</ReferenceOneField>
```

`emptyText` also accepts a translation key.
`empty` also accepts a translation key.

```jsx
<ReferenceOneField label="Details" reference="book_details" target="book_id" emptyText="resources.books.not_found">
<ReferenceOneField label="Details" reference="book_details" target="book_id" empty="resources.books.not_found">
<TextField source="genre" /> (<TextField source="ISBN" />)
</ReferenceOneField>
```

`emptyText` also accepts a `ReactElement`.
`empty` also accepts a `ReactNode`.

```jsx
<ReferenceOneField
label="Details"
reference="book_details"
target="book_id"
emptyText={<CreateButton to="/book_details/create" />}
empty={<CreateButton to="/book_details/create" />}
>
<TextField source="genre" /> (<TextField source="ISBN" />)
</ReferenceOneField>
Expand Down
2 changes: 1 addition & 1 deletion docs/WithRecord.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const BookShow = () => (
);
```

Note that if `record` is undefined, `<WithRecord>` 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, `<WithRecord>` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ import { ReferenceFieldBase } from './ReferenceFieldBase';
import { Error, Loading, Meta } from './ReferenceFieldBase.stories';

describe('<ReferenceFieldBase />', () => {
const defaultProps = {
reference: 'posts',
resource: 'comments',
source: 'post_id',
};

beforeAll(() => {
window.scrollTo = jest.fn();
});
Expand Down Expand Up @@ -52,7 +46,7 @@ describe('<ReferenceFieldBase />', () => {
});
render(
<CoreAdminContext dataProvider={dataProvider}>
<ReferenceFieldBase {...defaultProps}>
<ReferenceFieldBase reference="posts" source="post_id">
<MyComponent />
</ReferenceFieldBase>
</CoreAdminContext>
Expand Down
17 changes: 15 additions & 2 deletions packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -43,11 +44,22 @@ export const ReferenceFieldBase = <
>(
props: ReferenceFieldBaseProps<ReferenceRecordType>
) => {
const { children } = props;

const { children, empty = null } = props;
const id = useFieldValue(props);
const controllerProps =
useReferenceFieldController<ReferenceRecordType>(props);

if (
(empty &&
// no foreign key value
!id) ||
// no reference record
(!controllerProps.error &&
!controllerProps.isPending &&
!controllerProps.referenceRecord)
) {
return empty;
}
return (
<ResourceContextProvider value={props.reference}>
<ReferenceFieldContextProvider value={controllerProps}>
Expand All @@ -64,6 +76,7 @@ export interface ReferenceFieldBaseProps<
> {
children?: ReactNode;
className?: string;
empty?: ReactNode;
error?: ReactNode;
queryOptions?: Partial<
UseQueryOptions<ReferenceRecordType[], Error> & {
Expand Down
143 changes: 143 additions & 0 deletions packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
Original file line number Diff line number Diff line change
@@ -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
* <ReferenceManyFieldBase reference="comments" target="post_id">
* <Datagrid>
* <TextField source="id" />
* <TextField source="body" />
* <DateField source="created_at" />
* <EditButton />
* </Datagrid>
* </ReferenceManyFieldBase>
*
* @example Display all the books by the current author, only the title
* <ReferenceManyFieldBase reference="books" target="author_id">
* <SingleFieldList>
* <ChipField source="title" />
* </SingleFieldList>
* </ReferenceManyFieldBase>
*
* By default, restricts the displayed values to 25. You can extend this limit
* by setting the `perPage` prop.
*
* @example
* <ReferenceManyFieldBase perPage={10} reference="comments" target="post_id">
* ...
* </ReferenceManyFieldBase>
*
* 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
* <ReferenceManyFieldBase sort={{ field: 'created_at', order: 'DESC' }} reference="comments" target="post_id">
* ...
* </ReferenceManyFieldBase>
*
* Also, you can filter the query used to populate the possible values. Use the
* `filter` prop for that.
*
* @example
* <ReferenceManyFieldBase filter={{ is_published: true }} reference="comments" target="post_id">
* ...
* </ReferenceManyFieldBase>
*/
export const ReferenceManyFieldBase = <
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
>(
props: ReferenceManyFieldBaseProps<RecordType, ReferenceRecordType>
) => {
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 (
<ResourceContextProvider value={reference}>
<ListContextProvider value={controllerProps}>
{children}
{pagination}
</ListContextProvider>
</ResourceContextProvider>
);
};

export interface ReferenceManyFieldBaseProps<
RecordType extends Record<string, any> = Record<string, any>,
ReferenceRecordType extends Record<string, any> = Record<string, any>,
> extends UseReferenceManyFieldControllerParams<
RecordType,
ReferenceRecordType
> {
children: ReactNode;
empty?: ReactNode;
pagination?: ReactNode;
}

const defaultFilter = {};
const defaultSort = { field: 'id', order: 'DESC' as const };
Loading
Loading