Skip to content

Commit 4d9fb1b

Browse files
authored
Merge pull request #10939 from marmelab/editguesser-TextArrayInput
Introduce `<TextArrayField>` and use `<TextArrayInput>` / `<TextArrayField>` in guessers for scalar arrays
2 parents 0d93bb5 + c518eb1 commit 4d9fb1b

21 files changed

+429
-36
lines changed

docs/ArrayField.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,13 @@ Check [the `useListContext` documentation](./useListContext.md) for more informa
260260

261261
## Rendering An Array Of Strings
262262

263-
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), it's often simpler to write your own component:
263+
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you can use the [`<TextArrayField>`](./TextArrayField.md) component.
264+
265+
```jsx
266+
<TextArrayField source="tags" />
267+
```
268+
269+
You can also create your own field component, using the `useRecordContext` hook:
264270

265271
```jsx
266272
import { useRecordContext } from 'react-admin';

docs/ChipField.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ The `<ChipField>` component accepts the usual `className` prop. You can also ove
4141
| `&.RaChipField-chip` | Applied to the underlying Material UI's `Chip` component |
4242

4343
To override the style of all instances of `<ChipField>` using the [application-wide style overrides](./AppTheme.md#theming-individual-components), use the `RaChipField` key.
44+
45+
## Rendering A Scalar Value
46+
47+
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you may be tempted to use `<ChipField source="." />`, but that won't work.
48+
49+
What you probably need in that case instead is the [`<TextArrayField>`](./TextArrayField.md) component, which will render each item of a scalar array in its own Chip.
50+
51+
```jsx
52+
<TextArrayField source="tags" />
53+
```

docs/Reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ title: "Index"
211211
* [`<TabbedForm>`](./TabbedForm.md)
212212
* [`<TabbedFormWithRevision>`](./TabbedForm.md#versioning)<img class="icon" src="./img/premium.svg" alt="React Admin Enterprise Edition icon" />
213213
* [`<TabbedShowLayout>`](./TabbedShowLayout.md)
214+
* [`<TextArrayField>`](./TextArrayField.md)
214215
* [`<TextArrayInput>`](./TextArrayInput.md)
215216
* [`<TextField>`](./TextField.md)
216217
* [`<TextInput>`](./TextInput.md)

docs/SingleFieldList.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,11 @@ The `<SingleFieldList>` component accepts the usual `className` prop. You can al
143143
| `& .RaSingleFieldList-link` | Applied to each link |
144144

145145
**Tip**: You can override these classes for all `<SingleFieldList>` instances by overriding them in a Material UI theme, using the key "RaSingleFieldList".
146+
147+
## Rendering An Array Of Strings
148+
149+
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you may want to use the [`<TextArrayField>`](./TextArrayField.md) component instead.
150+
151+
```jsx
152+
<TextArrayField source="tags" />
153+
```

docs/TextArrayField.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
layout: default
3+
title: "The TextArrayField Component"
4+
storybook_path: ra-ui-materialui-fields-textarrayfield--basic
5+
---
6+
7+
# `<TextArrayField>`
8+
9+
`<TextArrayField>` renders an array of scalar values using Material-UI's Stack and Chips.
10+
11+
![TextArrayField](./img/text-array-field.png)
12+
13+
`<TextArrayField>` is ideal for displaying lists of simple text values, such as genres or tags, in a visually appealing way.
14+
15+
## Usage
16+
17+
`<TextArrayField>` can be used in a Show view to display an array of values from a record. For example:
18+
19+
```js
20+
const book = {
21+
id: 1,
22+
title: 'War and Peace',
23+
genres: [
24+
'Fiction',
25+
'Historical Fiction',
26+
'Classic Literature',
27+
'Russian Literature',
28+
],
29+
};
30+
```
31+
32+
You can render the `TextArrayField` like this:
33+
34+
```jsx
35+
import { Show, SimpleShowLayout, TextArrayField } from 'react-admin';
36+
37+
const BookShow = () => (
38+
<Show>
39+
<SimpleShowLayout>
40+
<TextField source="title" />
41+
<TextArrayField source="genres" />
42+
</SimpleShowLayout>
43+
</Show>
44+
);
45+
```
46+
47+
## Props
48+
49+
The following props are available for `<TextArrayField>`:
50+
51+
| Prop | Type | Required | Description |
52+
| ----------- | ------------ | -------- | ------------------------------------------------------------- |
53+
| `source` | `string` | Yes | The name of the record field containing the array to display. |
54+
| `color` | `string` | - | The color of the Chip components. |
55+
| `emptyText` | `ReactNode` | - | Text to display when the array is empty. |
56+
| `record` | `RecordType` | - | The record containing the data to display. |
57+
| `size` | `string` | - | The size of the Chip components. |
58+
| `variant` | `string` | - | The variant of the Chip components. |
59+
60+
Additional props are passed to the underlying [Material-UI `Stack` component](https://mui.com/material-ui/react-stack/).
61+
62+
## `color`
63+
64+
The color of the Chip components. Accepts any value supported by [MUI's Chip](https://mui.com/material-ui/react-chip/) (`primary`, `secondary`, etc).
65+
66+
```jsx
67+
<TextArrayField source="genres" color="secondary" />
68+
```
69+
70+
## `direction`
71+
72+
The direction of the Stack layout. Accepts `row` or `column`. The default is `row`.
73+
74+
```jsx
75+
<TextArrayField source="genres" direction="column" />
76+
```
77+
78+
## `emptyText`
79+
80+
Text to display when the array is empty.
81+
82+
```jsx
83+
<TextArrayField source="genres" emptyText="No genres available" />
84+
```
85+
86+
## `record`
87+
88+
The record containing the data to display. Usually provided by react-admin automatically.
89+
90+
```jsx
91+
const book = {
92+
id: 1,
93+
title: 'War and Peace',
94+
genres: [
95+
'Fiction',
96+
'Historical Fiction',
97+
'Classic Literature',
98+
'Russian Literature',
99+
],
100+
};
101+
102+
<TextArrayField source="genres" record={book} />
103+
```
104+
105+
## `size`
106+
107+
The size of the Chip components. Accepts any value supported by [MUI's Chip](https://mui.com/material-ui/react-chip/) (`small`, `medium`). The default is `small`.
108+
109+
```jsx
110+
<TextArrayField source="genres" size="medium" />
111+
```
112+
113+
## `source`
114+
115+
The name of the record field containing the array to display.
116+
117+
```jsx
118+
<TextArrayField source="genres" />
119+
```
120+
121+
## `sx`
122+
123+
Custom styles for the Stack, using MUI's `sx` prop.
124+
125+
{% raw %}
126+
```jsx
127+
<TextArrayField source="genres" sx={{ gap: 2 }} />
128+
```
129+
{% endraw %}
130+
131+
## `variant`
132+
133+
The variant of the Chip components. Accepts any value supported by [MUI's Chip](https://mui.com/material-ui/react-chip/) (`filled`, `outlined`). The default is `filled`.
134+
135+
```jsx
136+
<TextArrayField source="genres" variant="outlined" />
137+
```
138+

docs/img/text-array-field.png

15.2 KB
Loading

docs/navigation.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
<li {% if page.path == 'ReferenceOneField.md' %} class="active" {% endif %}><a class="nav-link" href="./ReferenceOneField.html"><code>&lt;ReferenceOneField&gt;</code></a></li>
195195
<li {% if page.path == 'RichTextField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./RichTextField.html"><code>&lt;RichTextField&gt;</code></a></li>
196196
<li {% if page.path == 'SelectField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./SelectField.html"><code>&lt;SelectField&gt;</code></a></li>
197+
<li {% if page.path == 'TextArrayField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextArrayField.html"><code>&lt;TextArrayField&gt;</code></a></li>
197198
<li {% if page.path == 'TextField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextField.html"><code>&lt;TextField&gt;</code></a></li>
198199
<li {% if page.path == 'TranslatableFields.md' %} class="active" {% endif %}><a class="nav-link" href="./TranslatableFields.html"><code>&lt;TranslatableFields&gt;</code></a></li>
199200
<li {% if page.path == 'UrlField.md' %} class="active" {% endif %}><a class="nav-link" href="./UrlField.html"><code>&lt;UrlField&gt;</code></a></li>

packages/ra-core/src/inference/inferElementFromValues.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ const inferElementFromValues = (
158158
)
159159
);
160160
}
161+
if (
162+
typeof values[0][0] === 'string' &&
163+
hasType('scalar_array', types)
164+
) {
165+
return (
166+
types.scalar_array &&
167+
new InferredElement(types.scalar_array, {
168+
source: name,
169+
})
170+
);
171+
}
161172
// FIXME introspect further
162173
return new InferredElement(types.string, { source: name });
163174
}

packages/ra-ui-materialui/src/detail/EditGuesser.spec.tsx

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,35 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, screen, waitFor } from '@testing-library/react';
4-
import { CoreAdminContext } from 'ra-core';
3+
import { render, screen } from '@testing-library/react';
54

6-
import { EditGuesser } from './EditGuesser';
7-
import { ThemeProvider } from '../theme/ThemeProvider';
5+
import { EditGuesser } from './EditGuesser.stories';
86

97
describe('<EditGuesser />', () => {
108
it('should log the guessed Edit view based on the fetched record', async () => {
119
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
12-
const dataProvider = {
13-
getOne: () =>
14-
Promise.resolve({
15-
data: {
16-
id: 123,
17-
author: 'john doe',
18-
post_id: 6,
19-
score: 3,
20-
body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.",
21-
created_at: new Date('2012-08-02'),
22-
tags_ids: [1, 2],
23-
},
24-
}),
25-
getMany: () => Promise.resolve({ data: [] }),
26-
};
27-
render(
28-
<ThemeProvider>
29-
<CoreAdminContext dataProvider={dataProvider as any}>
30-
<EditGuesser resource="comments" id={123} enableLog />
31-
</CoreAdminContext>
32-
</ThemeProvider>
33-
);
34-
await waitFor(() => {
35-
screen.getByDisplayValue('john doe');
36-
});
10+
render(<EditGuesser />);
11+
await screen.findByDisplayValue('john doe');
3712
expect(logSpy).toHaveBeenCalledWith(`Guessed Edit:
3813
39-
import { DateInput, Edit, NumberInput, ReferenceArrayInput, ReferenceInput, SimpleForm, TextInput } from 'react-admin';
14+
import { ArrayInput, BooleanInput, DateInput, Edit, NumberInput, ReferenceArrayInput, ReferenceInput, SimpleForm, SimpleFormIterator, TextArrayInput, TextInput } from 'react-admin';
4015
41-
export const CommentEdit = () => (
16+
export const BookEdit = () => (
4217
<Edit>
4318
<SimpleForm>
4419
<TextInput source="id" />
45-
<TextInput source="author" />
20+
<ArrayInput source="authors"><SimpleFormIterator><TextInput source="id" />
21+
<TextInput source="name" />
22+
<DateInput source="dob" /></SimpleFormIterator></ArrayInput>
4623
<ReferenceInput source="post_id" reference="posts" />
4724
<NumberInput source="score" />
4825
<TextInput source="body" />
26+
<TextInput source="description" />
4927
<DateInput source="created_at" />
5028
<ReferenceArrayInput source="tags_ids" reference="tags" />
29+
<TextInput source="url" />
30+
<TextInput source="email" />
31+
<BooleanInput source="isAlreadyPublished" />
32+
<TextArrayInput source="genres" />
5133
</SimpleForm>
5234
</Edit>
5335
);`);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as React from 'react';
2+
import { Admin } from 'react-admin';
3+
import { Resource, TestMemoryRouter } from 'ra-core';
4+
import fakeRestProvider from 'ra-data-fakerest';
5+
6+
import { EditGuesser as RAEditGuesser } from './EditGuesser';
7+
8+
export default { title: 'ra-ui-materialui/detail/EditGuesser' };
9+
10+
const data = {
11+
books: [
12+
{
13+
id: 123,
14+
authors: [
15+
{ id: 1, name: 'john doe', dob: '1990-01-01' },
16+
{ id: 2, name: 'jane doe', dob: '1992-01-01' },
17+
],
18+
post_id: 6,
19+
score: 3,
20+
body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.",
21+
description: `<p><strong>War and Peace</strong> is a novel by the Russian author <a href="https://en.wikipedia.org/wiki/Leo_Tolstoy">Leo Tolstoy</a>,
22+
published serially, then in its entirety in 1869.</p>
23+
<p>It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.</p>`,
24+
created_at: new Date('2012-08-02'),
25+
tags_ids: [1, 2],
26+
url: 'https://www.myshop.com/tags/top-seller',
27+
28+
isAlreadyPublished: true,
29+
genres: [
30+
'Fiction',
31+
'Historical Fiction',
32+
'Classic Literature',
33+
'Russian Literature',
34+
],
35+
},
36+
],
37+
tags: [
38+
{ id: 1, name: 'top seller' },
39+
{ id: 2, name: 'new' },
40+
],
41+
posts: [
42+
{ id: 6, title: 'War and Peace', body: 'A great novel by Leo Tolstoy' },
43+
],
44+
};
45+
46+
const EditGuesserWithProdLogs = () => <RAEditGuesser enableLog />;
47+
48+
export const EditGuesser = () => (
49+
<TestMemoryRouter initialEntries={['/books/123']}>
50+
<Admin dataProvider={fakeRestProvider(data)}>
51+
<Resource name="books" edit={EditGuesserWithProdLogs} />
52+
</Admin>
53+
</TestMemoryRouter>
54+
);

0 commit comments

Comments
 (0)