Skip to content

Commit b66f0ac

Browse files
authored
Merge pull request #10817 from marmelab/empty
Add support for `empty` in Reference fields
2 parents a450382 + c6f73a9 commit b66f0ac

20 files changed

+636
-207
lines changed

docs/ReferenceField.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,38 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
7474
| `source` | Required | `string` | - | Name of the property to display |
7575
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
7676
| `children` | Optional | `ReactNode` | - | One or more Field elements used to render the referenced record |
77-
| `emptyText` | Optional | `string` | '' | Defines a text to be shown when the field has no value or when the reference is missing |
77+
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
7878
| `label` | Optional | `string | Function` | `resources. [resource]. fields.[source]` | Label to use for the field when rendered in layout components |
7979
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
8080
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
8181
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
8282

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

85-
## `emptyText`
85+
## `empty`
8686

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

8989
```jsx
90-
<ReferenceField source="user_id" reference="users" emptyText="Missing user" />
90+
<ReferenceField source="user_id" reference="users" empty="Missing user" />
9191
```
9292

93-
`<ReferenceField>` renders the `emptyText`:
93+
`<ReferenceField>` renders the `empty` element when:
9494

95-
- when the referenced record is missing (no record in the `users` table with the right `user_id`), or
96-
- when the field is empty (no `user_id` in the record).
95+
- the referenced record is missing (no record in the `users` table with the right `user_id`), or
96+
- the field is empty (no `user_id` in the record).
97+
98+
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:
99+
100+
```jsx
101+
<ReferenceField source="user_id" reference="users" empty="resources.users.missing" />
102+
```
103+
104+
You can also pass a React element to the `empty` prop:
105+
106+
```jsx
107+
<ReferenceField source="user_id" reference="users" empty={<span>Missing user</span>} />
108+
```
97109

98110
## `label`
99111

docs/ReferenceManyField.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ This example leverages [`<SingleFieldList>`](./SingleFieldList.md) to display an
9090
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
9191
| `children` | Required | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
9292
| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
93+
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
9394
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
9495
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
9596
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
@@ -176,6 +177,44 @@ const PostCommentsField = () => (
176177
);
177178
```
178179

180+
## `empty`
181+
182+
Use `empty` to customize the text displayed when the related record is empty.
183+
184+
```jsx
185+
<ReferenceManyField
186+
reference="books"
187+
target="author_id"
188+
empty="no books"
189+
>
190+
...
191+
</ReferenceManyField>
192+
```
193+
194+
`empty` also accepts a translation key.
195+
196+
```jsx
197+
<ReferenceManyField
198+
reference="books"
199+
target="author_id"
200+
empty="resources.authors.fields.books.empty"
201+
>
202+
...
203+
</ReferenceManyField>
204+
```
205+
206+
`empty` also accepts a `ReactNode`.
207+
208+
```jsx
209+
<ReferenceManyField
210+
reference="books"
211+
target="author_id"
212+
empty={<CreateButton resource="books" />}
213+
>
214+
...
215+
</ReferenceManyField>
216+
```
217+
179218
## `filter`: Permanent Filter
180219

181220
You can filter the query used to populate the possible values. Use the `filter` prop for that.

docs/ReferenceOneField.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const BookShow = () => (
5959
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' |
6060
| `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' |
6161
| `children` | Optional | `Element` | - | The Field element used to render the referenced record |
62+
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
6263
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
6364
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
6465
| `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:
7879
</ReferenceOneField>
7980
```
8081

81-
## `emptyText`
82+
## `empty`
8283

83-
Use `emptyText` to customize the text displayed when the related record is empty.
84+
Use `empty` to customize the text displayed when the related record is empty.
8485

8586
```jsx
86-
<ReferenceOneField label="Details" reference="book_details" target="book_id" emptyText="no detail">
87+
<ReferenceOneField label="Details" reference="book_details" target="book_id" empty="no detail">
8788
<TextField source="genre" /> (<TextField source="ISBN" />)
8889
</ReferenceOneField>
8990
```
9091

91-
`emptyText` also accepts a translation key.
92+
`empty` also accepts a translation key.
9293

9394
```jsx
94-
<ReferenceOneField label="Details" reference="book_details" target="book_id" emptyText="resources.books.not_found">
95+
<ReferenceOneField label="Details" reference="book_details" target="book_id" empty="resources.books.not_found">
9596
<TextField source="genre" /> (<TextField source="ISBN" />)
9697
</ReferenceOneField>
9798
```
9899

99-
`emptyText` also accepts a `ReactElement`.
100+
`empty` also accepts a `ReactNode`.
100101

101102
```jsx
102103
<ReferenceOneField
103104
label="Details"
104105
reference="book_details"
105106
target="book_id"
106-
emptyText={<CreateButton to="/book_details/create" />}
107+
empty={<CreateButton to="/book_details/create" />}
107108
>
108109
<TextField source="genre" /> (<TextField source="ISBN" />)
109110
</ReferenceOneField>

docs/WithRecord.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const BookShow = () => (
2323
);
2424
```
2525

26-
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.
26+
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.
2727

2828
## Availability
2929

packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ import { ReferenceFieldBase } from './ReferenceFieldBase';
88
import { Error, Loading, Meta } from './ReferenceFieldBase.stories';
99

1010
describe('<ReferenceFieldBase />', () => {
11-
const defaultProps = {
12-
reference: 'posts',
13-
resource: 'comments',
14-
source: 'post_id',
15-
};
16-
1711
beforeAll(() => {
1812
window.scrollTo = jest.fn();
1913
});
@@ -52,7 +46,7 @@ describe('<ReferenceFieldBase />', () => {
5246
});
5347
render(
5448
<CoreAdminContext dataProvider={dataProvider}>
55-
<ReferenceFieldBase {...defaultProps}>
49+
<ReferenceFieldBase reference="posts" source="post_id">
5650
<MyComponent />
5751
</ReferenceFieldBase>
5852
</CoreAdminContext>

packages/ra-core/src/controller/field/ReferenceFieldBase.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RaRecord } from '../../types';
66
import { useReferenceFieldController } from './useReferenceFieldController';
77
import { ResourceContextProvider } from '../../core';
88
import { RecordContextProvider } from '../record';
9+
import { useFieldValue } from '../../util';
910

1011
/**
1112
* Fetch reference record, and render its representation, or delegate rendering to child component.
@@ -43,11 +44,22 @@ export const ReferenceFieldBase = <
4344
>(
4445
props: ReferenceFieldBaseProps<ReferenceRecordType>
4546
) => {
46-
const { children } = props;
47-
47+
const { children, empty = null } = props;
48+
const id = useFieldValue(props);
4849
const controllerProps =
4950
useReferenceFieldController<ReferenceRecordType>(props);
5051

52+
if (
53+
(empty &&
54+
// no foreign key value
55+
!id) ||
56+
// no reference record
57+
(!controllerProps.error &&
58+
!controllerProps.isPending &&
59+
!controllerProps.referenceRecord)
60+
) {
61+
return empty;
62+
}
5163
return (
5264
<ResourceContextProvider value={props.reference}>
5365
<ReferenceFieldContextProvider value={controllerProps}>
@@ -64,6 +76,7 @@ export interface ReferenceFieldBaseProps<
6476
> {
6577
children?: ReactNode;
6678
className?: string;
79+
empty?: ReactNode;
6780
error?: ReactNode;
6881
queryOptions?: Partial<
6982
UseQueryOptions<ReferenceRecordType[], Error> & {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { ReactNode } from 'react';
2+
import { ResourceContextProvider } from '../../core';
3+
import { ListContextProvider } from '../list/ListContextProvider';
4+
import {
5+
useReferenceManyFieldController,
6+
type UseReferenceManyFieldControllerParams,
7+
} from './useReferenceManyFieldController';
8+
import type { RaRecord } from '../../types';
9+
10+
/**
11+
* Render related records to the current one.
12+
*
13+
* You must define the fields to be passed to the iterator component as children.
14+
*
15+
* @example Display all the comments of the current post as a datagrid
16+
* <ReferenceManyFieldBase reference="comments" target="post_id">
17+
* <Datagrid>
18+
* <TextField source="id" />
19+
* <TextField source="body" />
20+
* <DateField source="created_at" />
21+
* <EditButton />
22+
* </Datagrid>
23+
* </ReferenceManyFieldBase>
24+
*
25+
* @example Display all the books by the current author, only the title
26+
* <ReferenceManyFieldBase reference="books" target="author_id">
27+
* <SingleFieldList>
28+
* <ChipField source="title" />
29+
* </SingleFieldList>
30+
* </ReferenceManyFieldBase>
31+
*
32+
* By default, restricts the displayed values to 25. You can extend this limit
33+
* by setting the `perPage` prop.
34+
*
35+
* @example
36+
* <ReferenceManyFieldBase perPage={10} reference="comments" target="post_id">
37+
* ...
38+
* </ReferenceManyFieldBase>
39+
*
40+
* By default, orders the possible values by id desc. You can change this order
41+
* by setting the `sort` prop (an object with `field` and `order` properties).
42+
*
43+
* @example
44+
* <ReferenceManyFieldBase sort={{ field: 'created_at', order: 'DESC' }} reference="comments" target="post_id">
45+
* ...
46+
* </ReferenceManyFieldBase>
47+
*
48+
* Also, you can filter the query used to populate the possible values. Use the
49+
* `filter` prop for that.
50+
*
51+
* @example
52+
* <ReferenceManyFieldBase filter={{ is_published: true }} reference="comments" target="post_id">
53+
* ...
54+
* </ReferenceManyFieldBase>
55+
*/
56+
export const ReferenceManyFieldBase = <
57+
RecordType extends RaRecord = RaRecord,
58+
ReferenceRecordType extends RaRecord = RaRecord,
59+
>(
60+
props: ReferenceManyFieldBaseProps<RecordType, ReferenceRecordType>
61+
) => {
62+
const {
63+
children,
64+
debounce,
65+
empty,
66+
filter = defaultFilter,
67+
page = 1,
68+
pagination = null,
69+
perPage = 25,
70+
record,
71+
reference,
72+
resource,
73+
sort = defaultSort,
74+
source = 'id',
75+
storeKey,
76+
target,
77+
queryOptions,
78+
} = props;
79+
80+
const controllerProps = useReferenceManyFieldController<
81+
RecordType,
82+
ReferenceRecordType
83+
>({
84+
debounce,
85+
filter,
86+
page,
87+
perPage,
88+
record,
89+
reference,
90+
resource,
91+
sort,
92+
source,
93+
storeKey,
94+
target,
95+
queryOptions,
96+
});
97+
98+
if (
99+
// there is an empty page component
100+
empty &&
101+
// there is no error
102+
!controllerProps.error &&
103+
// the list is not loading data for the first time
104+
!controllerProps.isPending &&
105+
// the API returned no data (using either normal or partial pagination)
106+
(controllerProps.total === 0 ||
107+
(controllerProps.total == null &&
108+
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
109+
controllerProps.hasPreviousPage === false &&
110+
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
111+
controllerProps.hasNextPage === false &&
112+
// @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
113+
controllerProps.data.length === 0)) &&
114+
// the user didn't set any filters
115+
!Object.keys(controllerProps.filterValues).length
116+
) {
117+
return empty;
118+
}
119+
120+
return (
121+
<ResourceContextProvider value={reference}>
122+
<ListContextProvider value={controllerProps}>
123+
{children}
124+
{pagination}
125+
</ListContextProvider>
126+
</ResourceContextProvider>
127+
);
128+
};
129+
130+
export interface ReferenceManyFieldBaseProps<
131+
RecordType extends Record<string, any> = Record<string, any>,
132+
ReferenceRecordType extends Record<string, any> = Record<string, any>,
133+
> extends UseReferenceManyFieldControllerParams<
134+
RecordType,
135+
ReferenceRecordType
136+
> {
137+
children: ReactNode;
138+
empty?: ReactNode;
139+
pagination?: ReactNode;
140+
}
141+
142+
const defaultFilter = {};
143+
const defaultSort = { field: 'id', order: 'DESC' as const };

0 commit comments

Comments
 (0)