Skip to content

Commit 359493d

Browse files
committed
Add unit tests
1 parent 5a0eca3 commit 359493d

File tree

4 files changed

+272
-25
lines changed

4 files changed

+272
-25
lines changed

docs/RecordField.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ The `source`, `field`, `children`, and `render` props are mutually exclusive.
8080
| Prop | Required | Type | Default | Description |
8181
| ----------- | -------- | ------------------ | ------- | -------------------------------------------------------------------------------- |
8282
| `children` | Optional | ReactNode | '' | Elements rendering the actual field. |
83+
| `className` | Optional | string | '' | CSS class name to apply to the field. |
84+
| `defaultValue` | Optional | any | undefined | Default value to display when the field is empty. |
85+
| `emptyText` | Optional | string | '-' | Text to display when the field is empty. |
8386
| `field` | Optional | ReactElement | `TextField` | Field component used to render the field. Ignored if `children` or `render` are set. |
8487
| `label` | Optional | string | '' | Label to render. Can be a translation key. |
8588
| `render` | Optional | record => JSX | | Function to render the field value. Ignored if `children` is set. |
@@ -124,6 +127,38 @@ import { RecordField, NumberField } from 'react-admin';
124127
</RecordField>
125128
```
126129

130+
## `defaultValue`
131+
132+
When the record contains no value for the `source` prop, `RecordField` renders an empty string. You can use the `defaultValue` prop to specify a default value to display instead.
133+
134+
```jsx
135+
<RecordField source="rating" defaultValue={0} />
136+
```
137+
138+
If you're using the `render` prop, the `defaultValue` will be passed to the `render` function as the second argument:
139+
140+
```jsx
141+
<RecordField
142+
label="Rating"
143+
defaultValue={0}
144+
render={(record, defaultValue) =>
145+
record.rating ? `${record.rating} stars` : defaultValue
146+
}
147+
/>
148+
```
149+
150+
**Tip**: If you need the default value to be translated, use the `emptyText` prop instead.
151+
152+
## `emptyText`
153+
154+
When the record contains no value for the `source` prop, `RecordField` renders an empty string. If you need to render a locale-specific string in this case, you can use the `emptyText` prop to specify a translation key:
155+
156+
```jsx
157+
<RecordField source="title" emptyText="resources.books.fields.title.missing" />
158+
```
159+
160+
**Tip**: If you don't need the default value to be translated, use the `defaultValue` prop instead.
161+
127162
## `field`
128163

129164
By default, `<RecordField>` uses the [`<TextField>`](./TextField.md) component to render the field value.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as React from 'react';
2+
import expect from 'expect';
3+
import { render, screen } from '@testing-library/react';
4+
5+
import {
6+
Basic,
7+
Source,
8+
Label,
9+
DefaultValue,
10+
EmptyText,
11+
Render,
12+
Field,
13+
Children,
14+
} from './RecordField.stories';
15+
export default {
16+
title: 'ra-ui-materialui/fields/RecordField',
17+
};
18+
19+
describe('<RecordField />', () => {
20+
describe('source', () => {
21+
it('should render the source field from the record in context', () => {
22+
render(<Basic />);
23+
expect(screen.queryByText('War and Peace')).not.toBeNull();
24+
});
25+
it('should render nothing when the source is not found', () => {
26+
render(<Source />);
27+
expect(screen.queryByText('Missing field')).not.toBeNull();
28+
});
29+
it('should support paths with dots', () => {
30+
render(<Source />);
31+
expect(screen.queryByText('Leo Tolstoy')).not.toBeNull();
32+
});
33+
});
34+
describe('label', () => {
35+
it('should render the humanized source as label by default', () => {
36+
render(<Basic />);
37+
expect(screen.queryByText('Title')).not.toBeNull();
38+
});
39+
it('should render the label prop as label', () => {
40+
render(<Label />);
41+
expect(screen.queryByText('Identifier')).not.toBeNull();
42+
});
43+
it('should render no label when label is false', () => {
44+
render(<Label />);
45+
expect(screen.queryByText('Summary')).toBeNull();
46+
});
47+
});
48+
describe('defaultValue', () => {
49+
it('should render the defaultValue when the record is undefined', () => {
50+
render(<DefaultValue />);
51+
expect(screen.queryByText('N/A')).not.toBeNull();
52+
});
53+
it('should render the defaultValue when using a render prop', () => {
54+
render(<DefaultValue />);
55+
expect(screen.queryByText('Unknown')).not.toBeNull();
56+
});
57+
it('should render the defaultValue when using a field prop', () => {
58+
render(<DefaultValue />);
59+
expect(screen.queryByText('0')).not.toBeNull();
60+
});
61+
});
62+
describe('emptyText', () => {
63+
it('should render the translated emptyText when the record is undefined', () => {
64+
render(<EmptyText />);
65+
expect(screen.queryByText('No title')).not.toBeNull();
66+
});
67+
it('should render the translated emptyText when using a render prop', () => {
68+
render(<EmptyText />);
69+
expect(screen.queryByText('Unknown author')).not.toBeNull();
70+
});
71+
it('should render the translated emptyText when using a field prop', () => {
72+
render(<EmptyText />);
73+
expect(screen.queryByText('0')).not.toBeNull();
74+
});
75+
});
76+
describe('render', () => {
77+
it('should render the value using the render prop', () => {
78+
render(<Render />);
79+
expect(screen.queryByText('WAR AND PEACE')).not.toBeNull();
80+
});
81+
it('should allow to render a React element', () => {
82+
render(<Render />);
83+
expect(screen.queryByText('LEO TOLSTOY')).not.toBeNull();
84+
});
85+
it('should not fail when the record is undefined', () => {
86+
render(<Render />);
87+
expect(screen.queryByText('Summary')).not.toBeNull();
88+
});
89+
});
90+
describe('field', () => {
91+
it('should use the field component to render the field', () => {
92+
render(<Field />);
93+
expect(screen.queryByText('1,869')).not.toBeNull();
94+
});
95+
});
96+
describe('children', () => {
97+
it('should render the field using the children rather than a TextField', () => {
98+
render(<Children />);
99+
expect(screen.queryByText('Leo Tolstoy')).not.toBeNull();
100+
expect(screen.queryByText('(DECD)')).not.toBeNull();
101+
});
102+
});
103+
});

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

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ export const Basic = () => (
3333
</ResourceContext.Provider>
3434
);
3535

36+
export const Source = () => (
37+
<ResourceContext.Provider value="books">
38+
<RecordContextProvider
39+
value={{
40+
id: 1,
41+
'author.name': 'Leo Tolstoy',
42+
}}
43+
>
44+
<Stack>
45+
<RecordField source="author.name" />
46+
<RecordField source="missing.field" />
47+
</Stack>
48+
</RecordContextProvider>
49+
</ResourceContext.Provider>
50+
);
51+
3652
export const Field = () => (
3753
<ResourceContext.Provider value="books">
3854
<RecordContextProvider value={record}>
@@ -43,14 +59,91 @@ export const Field = () => (
4359
</ResourceContext.Provider>
4460
);
4561

62+
export const DefaultValue = () => (
63+
<ResourceContext.Provider value="books">
64+
<RecordContextProvider value={{}}>
65+
<Stack>
66+
<RecordField source="title" defaultValue="N/A" />
67+
<RecordField
68+
source="author"
69+
defaultValue="Unknown"
70+
render={(record, defaultValue) =>
71+
record.author || defaultValue
72+
}
73+
/>
74+
<RecordField
75+
source="year"
76+
field={NumberField}
77+
defaultValue={0}
78+
/>
79+
</Stack>
80+
</RecordContextProvider>
81+
</ResourceContext.Provider>
82+
);
83+
84+
const translations = {
85+
'books.title.missing': 'No title',
86+
'books.author.missing': 'Unknown author',
87+
'books.year.missing': '0',
88+
};
89+
90+
export const EmptyText = () => (
91+
<I18nContextProvider
92+
value={{
93+
getLocale: () => 'en',
94+
translate: m => translations[m] || m,
95+
changeLocale: async () => {},
96+
}}
97+
>
98+
<ResourceContext.Provider value="books">
99+
<RecordContextProvider value={{}}>
100+
<Stack>
101+
<RecordField
102+
source="title"
103+
emptyText="books.title.missing"
104+
/>
105+
<RecordField
106+
source="author"
107+
emptyText="books.author.missing"
108+
render={(record, defaultValue) =>
109+
record.author || defaultValue
110+
}
111+
/>
112+
<RecordField
113+
source="year"
114+
field={NumberField}
115+
emptyText="books.year.missing"
116+
/>
117+
</Stack>
118+
</RecordContextProvider>
119+
</ResourceContext.Provider>
120+
</I18nContextProvider>
121+
);
122+
46123
export const Render = () => (
47124
<ResourceContext.Provider value="books">
48125
<RecordContextProvider value={record}>
49126
<Stack>
127+
<RecordField
128+
label="Title"
129+
render={record => record.title.toUpperCase()}
130+
/>
50131
<RecordField
51132
source="author"
52-
render={record => <span>{record.author}</span>}
133+
render={record => (
134+
<span>{record.author.toUpperCase()}</span>
135+
)}
136+
/>
137+
<RecordField
138+
label="Missing field"
139+
render={record => record.missingField}
53140
/>
141+
<RecordContextProvider value={undefined}>
142+
<RecordField
143+
label="Summary"
144+
render={record => record.summary}
145+
/>
146+
</RecordContextProvider>
54147
</Stack>
55148
</RecordContextProvider>
56149
</ResourceContext.Provider>
@@ -60,8 +153,9 @@ export const Children = () => (
60153
<ResourceContext.Provider value="books">
61154
<RecordContextProvider value={record}>
62155
<Stack>
63-
<RecordField source="title">
64-
<TextField source="title" variant="body1" />
156+
<RecordField label="Author">
157+
<TextField source="author" variant="body1" />{' '}
158+
<Typography component="span">(DECD)</Typography>
65159
</RecordField>
66160
</Stack>
67161
</RecordContextProvider>

packages/ra-ui-materialui/src/field/RecordField.tsx

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
FieldTitle,
1313
useRecordContext,
1414
useResourceContext,
15+
useTranslate,
1516
type ExtractRecordPaths,
1617
type HintedString,
1718
type RaRecord,
@@ -25,6 +26,8 @@ export const RecordField = <
2526
>({
2627
children,
2728
className,
29+
defaultValue,
30+
emptyText,
2831
field,
2932
label,
3033
render,
@@ -36,6 +39,7 @@ export const RecordField = <
3639
}: RecordFieldProps<RecordType>) => {
3740
const resource = useResourceContext();
3841
const record = useRecordContext<RecordType>();
42+
const translate = useTranslate();
3943
if (!source && !label) return null;
4044
return (
4145
<Root
@@ -58,28 +62,37 @@ export const RecordField = <
5862
/>
5963
</Typography>
6064
) : null}
61-
{children ??
62-
(render ? (
63-
record && (
64-
<Typography
65-
component="span"
66-
variant="body2"
67-
className={RecordFieldClasses.value}
68-
>
69-
{render(record)}
70-
</Typography>
71-
)
72-
) : field ? (
73-
React.createElement(field, {
74-
source,
75-
className: RecordFieldClasses.value,
76-
})
77-
) : source ? (
78-
<TextField
79-
source={source}
65+
{children ? (
66+
<span className={RecordFieldClasses.value}>{children}</span>
67+
) : render ? (
68+
record && (
69+
<Typography
70+
component="span"
71+
variant="body2"
8072
className={RecordFieldClasses.value}
81-
/>
82-
) : null)}
73+
>
74+
{render(record, defaultValue) ||
75+
(emptyText
76+
? translate(emptyText, { _: emptyText })
77+
: null)}
78+
</Typography>
79+
)
80+
) : field ? (
81+
React.createElement(field, {
82+
source,
83+
defaultValue,
84+
emptyText,
85+
className: RecordFieldClasses.value,
86+
})
87+
) : source ? (
88+
<TextField
89+
source={source}
90+
defaultValue={defaultValue}
91+
emptyText={emptyText}
92+
resource={resource}
93+
className={RecordFieldClasses.value}
94+
/>
95+
) : null}
8396
</Root>
8497
);
8598
};
@@ -92,9 +105,11 @@ export interface RecordFieldProps<
92105
> extends StackProps {
93106
children?: ReactNode;
94107
className?: string;
108+
defaultValue?: any;
109+
emptyText?: string;
95110
field?: ElementType;
96111
label?: ReactNode;
97-
render?: (record: RecordType) => React.ReactNode;
112+
render?: (record: RecordType, defaultValue?: any) => React.ReactNode;
98113
source?: NoInfer<HintedString<ExtractRecordPaths<RecordType>>>;
99114
record?: RaRecord;
100115
sx?: SxProps;

0 commit comments

Comments
 (0)