Skip to content

Commit 3fcc16d

Browse files
committed
Add <ReferenceOneField empty> prop
1 parent 836fffc commit 3fcc16d

File tree

6 files changed

+254
-80
lines changed

6 files changed

+254
-80
lines changed

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 `ReactElement`.
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>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { ReactNode, useMemo } from 'react';
2+
import {
3+
useReferenceOneFieldController,
4+
UseReferenceOneFieldControllerParams,
5+
} from './useReferenceOneFieldController';
6+
import { useRecordContext, RecordContextProvider } from '../record';
7+
import { ResourceContextProvider } from '../../core';
8+
import { ReferenceFieldContextProvider } from './ReferenceFieldContext';
9+
import { useGetPathForRecord } from '../../routing';
10+
import type { UseReferenceFieldControllerResult } from './useReferenceFieldController';
11+
import type { RaRecord } from '../../types';
12+
import type { LinkToType } from '../../routing';
13+
14+
/**
15+
* Render the related record in a one-to-one relationship
16+
*
17+
* Expects a single field as child
18+
*
19+
* @example // display the bio of the current author
20+
* <ReferenceOneFieldBase reference="bios" target="author_id">
21+
* <TextField source="body" />
22+
* </ReferenceOneFieldBase>
23+
*/
24+
export const ReferenceOneFieldBase = <
25+
RecordType extends RaRecord = RaRecord,
26+
ReferenceRecordType extends RaRecord = RaRecord,
27+
>(
28+
props: ReferenceOneFieldBaseProps<RecordType, ReferenceRecordType>
29+
) => {
30+
const {
31+
children,
32+
record,
33+
reference,
34+
source = 'id',
35+
target,
36+
empty,
37+
sort,
38+
filter,
39+
link,
40+
queryOptions,
41+
} = props;
42+
43+
const controllerProps = useReferenceOneFieldController<
44+
RecordType,
45+
ReferenceRecordType
46+
>({
47+
record,
48+
reference,
49+
source,
50+
target,
51+
sort,
52+
filter,
53+
queryOptions,
54+
});
55+
56+
const path = useGetPathForRecord({
57+
record: controllerProps.referenceRecord,
58+
resource: reference,
59+
link,
60+
});
61+
62+
const context = useMemo<UseReferenceFieldControllerResult>(
63+
() => ({
64+
...controllerProps,
65+
link: path,
66+
}),
67+
[controllerProps, path]
68+
);
69+
70+
const recordFromContext = useRecordContext<RecordType>(props);
71+
if (
72+
!recordFromContext ||
73+
(!controllerProps.isPending && controllerProps.referenceRecord == null)
74+
) {
75+
return empty;
76+
}
77+
78+
return (
79+
<ResourceContextProvider value={reference}>
80+
<ReferenceFieldContextProvider value={context}>
81+
<RecordContextProvider value={context.referenceRecord}>
82+
{children}
83+
</RecordContextProvider>
84+
</ReferenceFieldContextProvider>
85+
</ResourceContextProvider>
86+
);
87+
};
88+
89+
export interface ReferenceOneFieldBaseProps<
90+
RecordType extends RaRecord = RaRecord,
91+
ReferenceRecordType extends RaRecord = RaRecord,
92+
> extends UseReferenceOneFieldControllerParams<
93+
RecordType,
94+
ReferenceRecordType
95+
> {
96+
children?: ReactNode;
97+
link?: LinkToType<ReferenceRecordType>;
98+
empty?: ReactNode;
99+
resource?: string;
100+
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import get from 'lodash/get';
22
import { UseQueryOptions } from '@tanstack/react-query';
33

4+
import { useRecordContext } from '../record';
45
import { useGetManyReference } from '../../dataProvider';
56
import { useNotify } from '../../notification';
67
import { RaRecord, SortPayload } from '../../types';
78
import { UseReferenceResult } from '../useReference';
89

910
export interface UseReferenceOneFieldControllerParams<
1011
RecordType extends RaRecord = any,
12+
ReferenceRecordType extends RaRecord = any,
1113
ErrorType = Error,
1214
> {
13-
record?: RaRecord;
1415
reference: string;
15-
source?: string;
1616
target: string;
17-
sort?: SortPayload;
1817
filter?: any;
1918
queryOptions?: Omit<
2019
UseQueryOptions<
2120
{
22-
data: RecordType[];
21+
data: ReferenceRecordType[];
2322
total: number;
2423
},
2524
ErrorType
2625
>,
2726
'queryFn' | 'queryKey'
2827
> & { meta?: any };
28+
record?: RecordType;
29+
sort?: SortPayload;
30+
source?: string;
2931
}
3032

3133
/**
@@ -53,24 +55,29 @@ export interface UseReferenceOneFieldControllerParams<
5355
*/
5456
export const useReferenceOneFieldController = <
5557
RecordType extends RaRecord = any,
58+
ReferenceRecordType extends RaRecord = any,
5659
ErrorType = Error,
5760
>(
58-
props: UseReferenceOneFieldControllerParams<RecordType, ErrorType>
59-
): UseReferenceResult<RecordType, ErrorType> => {
61+
props: UseReferenceOneFieldControllerParams<
62+
RecordType,
63+
ReferenceRecordType,
64+
ErrorType
65+
>
66+
): UseReferenceResult<ReferenceRecordType, ErrorType> => {
6067
const {
6168
reference,
62-
record,
6369
target,
6470
source = 'id',
6571
sort = { field: 'id', order: 'ASC' },
6672
filter = {},
6773
queryOptions = {},
6874
} = props;
75+
const record = useRecordContext<RecordType>(props);
6976
const notify = useNotify();
7077
const { meta, ...otherQueryOptions } = queryOptions;
7178

7279
const { data, error, isFetching, isLoading, isPending, refetch } =
73-
useGetManyReference<RecordType, ErrorType>(
80+
useGetManyReference<ReferenceRecordType, ErrorType>(
7481
reference,
7582
{
7683
target,

packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
44
import {
55
RecordRepresentation,
66
Basic,
7-
EmptyWithTranslate,
7+
EmptyTextWithTranslate,
88
QueryOptions,
99
EmptyText,
1010
Themed,
@@ -22,7 +22,7 @@ describe('ReferenceOneField', () => {
2222
});
2323

2424
it('should translate emptyText', async () => {
25-
render(<EmptyWithTranslate />);
25+
render(<EmptyTextWithTranslate />);
2626

2727
await screen.findByText('Not found');
2828
});

packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export const EmptyText = () => (
230230
</TestMemoryRouter>
231231
);
232232

233-
export const EmptyWithTranslate = () => (
233+
export const EmptyTextWithTranslate = () => (
234234
<Wrapper dataProvider={emptyDataProvider}>
235235
<I18nContextProvider value={i18nProvider}>
236236
<ReferenceOneField
@@ -244,6 +244,97 @@ export const EmptyWithTranslate = () => (
244244
</Wrapper>
245245
);
246246

247+
export const Empty = () => (
248+
<TestMemoryRouter>
249+
<AdminContext dataProvider={dataProvider} i18nProvider={i18nProvider}>
250+
<AdminUI>
251+
<Resource
252+
name="books"
253+
list={() => (
254+
<List>
255+
<Datagrid>
256+
<TextField source="id" />
257+
<TextField source="title" />
258+
<TextField source="year" />
259+
<TextField source="Genre" />
260+
<ReferenceOneField
261+
reference="book_details"
262+
target="book_id"
263+
label="ISBN"
264+
empty="no detail"
265+
>
266+
<TextField source="ISBN" />
267+
</ReferenceOneField>
268+
</Datagrid>
269+
</List>
270+
)}
271+
show={() => (
272+
<Show>
273+
<SimpleShowLayout>
274+
<TextField source="id" />
275+
<TextField source="title" />
276+
<TextField source="year" />
277+
<TextField source="Genre" />
278+
<ReferenceOneField
279+
reference="book_details"
280+
target="book_id"
281+
label="ISBN"
282+
empty={
283+
<CreateButton to="/book_details/create" />
284+
}
285+
>
286+
<TextField source="ISBN" />
287+
</ReferenceOneField>
288+
</SimpleShowLayout>
289+
</Show>
290+
)}
291+
/>
292+
<Resource
293+
name="book_details"
294+
list={() => (
295+
<List>
296+
<Datagrid>
297+
<TextField source="id" />
298+
<TextField source="ISBN" />
299+
<ReferenceField
300+
source="book_id"
301+
reference="books"
302+
/>
303+
</Datagrid>
304+
</List>
305+
)}
306+
create={() => (
307+
<Create>
308+
<SimpleForm>
309+
<TextInput source="ISBN" />
310+
<ReferenceInput
311+
source="book_id"
312+
reference="books"
313+
label="Book"
314+
/>
315+
</SimpleForm>
316+
</Create>
317+
)}
318+
/>
319+
</AdminUI>
320+
</AdminContext>
321+
</TestMemoryRouter>
322+
);
323+
324+
export const EmptyWithTranslate = () => (
325+
<Wrapper dataProvider={emptyDataProvider}>
326+
<I18nContextProvider value={i18nProvider}>
327+
<ReferenceOneField
328+
reference="book_details"
329+
target="book_id"
330+
empty="resources.books.not_found"
331+
>
332+
<TextField source="ISBN" />
333+
</ReferenceOneField>
334+
</I18nContextProvider>
335+
</Wrapper>
336+
);
337+
247338
export const Link = () => (
248339
<Wrapper>
249340
<ReferenceOneField

0 commit comments

Comments
 (0)