Skip to content

Commit 3694427

Browse files
authored
Merge pull request #10833 from marmelab/reference-array-input-base
Introduce `<ReferenceArrayInputBase>`
2 parents 1bd04dd + a08fbf0 commit 3694427

File tree

7 files changed

+440
-56
lines changed

7 files changed

+440
-56
lines changed

docs/ReferenceArrayInput.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ See the [`children`](#children) section for more details.
114114

115115
## `children`
116116

117-
By default, `<ReferenceInput>` renders an [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) to let end users select the reference record.
117+
By default, `<ReferenceArrayInput>` renders an [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) to let end users select the reference record.
118118

119119
You can pass a child component to customize the way the reference selector is displayed.
120120

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 { render, screen, waitFor } from '@testing-library/react';
3+
import { testDataProvider } from 'ra-core';
4+
import { Basic, WithError } from './ReferenceArrayInputBase.stories';
5+
6+
describe('<ReferenceArrayInputBase>', () => {
7+
afterEach(async () => {
8+
// wait for the getManyAggregate batch to resolve
9+
await waitFor(() => new Promise(resolve => setTimeout(resolve, 0)));
10+
});
11+
12+
it('should pass down the error if any occurred', async () => {
13+
jest.spyOn(console, 'error').mockImplementation(() => {});
14+
render(<WithError />);
15+
await waitFor(() => {
16+
expect(screen.queryByText('Error: fetch error')).not.toBeNull();
17+
});
18+
});
19+
it('should pass the correct resource down to child component', async () => {
20+
render(<Basic />);
21+
// Check that the child component receives the correct resource (tags)
22+
await screen.findByText('Selected tags: 1, 3');
23+
});
24+
25+
it('should provide a ChoicesContext with all available choices', async () => {
26+
render(<Basic />);
27+
await screen.findByText('Total tags: 5');
28+
});
29+
30+
it('should apply default values', async () => {
31+
render(<Basic />);
32+
// Check that the default values are applied (1, 3)
33+
await screen.findByText('Selected tags: 1, 3');
34+
});
35+
36+
it('should accept meta in queryOptions', async () => {
37+
const getList = jest
38+
.fn()
39+
.mockImplementationOnce(() =>
40+
Promise.resolve({ data: [], total: 25 })
41+
);
42+
const dataProvider = testDataProvider({ getList });
43+
render(<Basic meta dataProvider={dataProvider} />);
44+
await waitFor(() => {
45+
expect(getList).toHaveBeenCalledWith('tags', {
46+
filter: {},
47+
pagination: { page: 1, perPage: 25 },
48+
sort: { field: 'id', order: 'DESC' },
49+
meta: { foo: 'bar' },
50+
signal: undefined,
51+
});
52+
});
53+
});
54+
});
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import * as React from 'react';
2+
import polyglotI18nProvider from 'ra-i18n-polyglot';
3+
import englishMessages from 'ra-language-english';
4+
5+
import { CoreAdmin } from '../../core/CoreAdmin';
6+
import { Resource } from '../../core/Resource';
7+
import { CreateBase } from '../../controller/create/CreateBase';
8+
import { testDataProvider } from '../../dataProvider/testDataProvider';
9+
import { DataProvider } from '../../types';
10+
import { Form } from '../../form/Form';
11+
import { useInput } from '../../form/useInput';
12+
import { InputProps } from '../../form/types';
13+
import { TestMemoryRouter } from '../../routing/TestMemoryRouter';
14+
import {
15+
ReferenceArrayInputBase,
16+
ReferenceArrayInputBaseProps,
17+
} from './ReferenceArrayInputBase';
18+
import {
19+
ChoicesContextValue,
20+
ChoicesProps,
21+
useChoicesContext,
22+
} from '../../form';
23+
import { useGetRecordRepresentation } from '../..';
24+
25+
export default { title: 'ra-core/controller/ReferenceArrayInputBase' };
26+
27+
const tags = [
28+
{ id: 0, name: '3D' },
29+
{ id: 1, name: 'Architecture' },
30+
{ id: 2, name: 'Design' },
31+
{ id: 3, name: 'Painting' },
32+
{ id: 4, name: 'Photography' },
33+
];
34+
35+
const defaultDataProvider = testDataProvider({
36+
getList: () =>
37+
// @ts-ignore
38+
Promise.resolve({
39+
data: tags,
40+
total: tags.length,
41+
}),
42+
// @ts-ignore
43+
getMany: (resource, params) => {
44+
if (process.env.NODE_ENV !== 'test') {
45+
console.log('getMany', resource, params);
46+
}
47+
return Promise.resolve({
48+
data: params.ids.map(id => tags.find(tag => tag.id === id)),
49+
});
50+
},
51+
});
52+
53+
const i18nProvider = polyglotI18nProvider(() => englishMessages);
54+
55+
const CheckboxGroupInput = (
56+
props: Omit<InputProps, 'source'> & ChoicesProps
57+
) => {
58+
const choicesContext = useChoicesContext(props);
59+
60+
return <CheckboxGroupInputBase {...props} {...choicesContext} />;
61+
};
62+
63+
const CheckboxGroupInputBase = (
64+
props: Omit<InputProps, 'source'> & ChoicesProps & ChoicesContextValue
65+
) => {
66+
const { allChoices, isPending, error, resource, source, total } = props;
67+
const input = useInput({ ...props, source });
68+
const getRecordRepresentation = useGetRecordRepresentation(resource);
69+
70+
if (isPending) {
71+
return <span>Loading...</span>;
72+
}
73+
74+
if (error) {
75+
return <span>Error: {error.message}</span>;
76+
}
77+
78+
return (
79+
<div>
80+
{allChoices.map(choice => (
81+
<label key={choice.id}>
82+
<input
83+
type="checkbox"
84+
// eslint-disable-next-line eqeqeq
85+
checked={input.field.value.some(id => id == choice.id)}
86+
onChange={() => {
87+
const newValue = input.field.value.some(
88+
// eslint-disable-next-line eqeqeq
89+
id => id == choice.id
90+
)
91+
? input.field.value.filter(
92+
// eslint-disable-next-line eqeqeq
93+
id => id != choice.id
94+
)
95+
: [...input.field.value, choice.id];
96+
input.field.onChange(newValue);
97+
}}
98+
/>
99+
{getRecordRepresentation(choice)}
100+
</label>
101+
))}
102+
<div>
103+
Selected {resource}: {input.field.value.join(', ')}
104+
</div>
105+
<div>
106+
Total {resource}: {total}
107+
</div>
108+
</div>
109+
);
110+
};
111+
112+
export const Basic = ({
113+
dataProvider = defaultDataProvider,
114+
meta,
115+
...props
116+
}: Partial<ReferenceArrayInputBaseProps> & {
117+
dataProvider?: DataProvider;
118+
meta?: boolean;
119+
}) => (
120+
<TestMemoryRouter initialEntries={['/posts/create']}>
121+
<CoreAdmin dataProvider={dataProvider} i18nProvider={i18nProvider}>
122+
<Resource
123+
name="posts"
124+
create={
125+
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
126+
<h1>Create Post</h1>
127+
<Form>
128+
<ReferenceArrayInputBase
129+
reference="tags"
130+
resource="posts"
131+
source="tags_ids"
132+
queryOptions={
133+
meta ? { meta: { foo: 'bar' } } : {}
134+
}
135+
{...props}
136+
>
137+
<CheckboxGroupInput />
138+
</ReferenceArrayInputBase>
139+
</Form>
140+
</CreateBase>
141+
}
142+
/>
143+
</CoreAdmin>
144+
</TestMemoryRouter>
145+
);
146+
147+
Basic.args = {
148+
meta: false,
149+
};
150+
151+
Basic.argTypes = {
152+
meta: { control: 'boolean' },
153+
};
154+
155+
export const WithRender = ({
156+
dataProvider = defaultDataProvider,
157+
meta,
158+
...props
159+
}: Partial<ReferenceArrayInputBaseProps> & {
160+
dataProvider?: DataProvider;
161+
meta?: boolean;
162+
}) => (
163+
<TestMemoryRouter initialEntries={['/posts/create']}>
164+
<CoreAdmin dataProvider={dataProvider} i18nProvider={i18nProvider}>
165+
<Resource
166+
name="posts"
167+
create={
168+
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
169+
<h1>Create Post</h1>
170+
<Form>
171+
<ReferenceArrayInputBase
172+
reference="tags"
173+
resource="posts"
174+
source="tags_ids"
175+
queryOptions={
176+
meta ? { meta: { foo: 'bar' } } : {}
177+
}
178+
{...props}
179+
render={context => (
180+
<CheckboxGroupInputBase
181+
{...context}
182+
source="tags_ids"
183+
/>
184+
)}
185+
/>
186+
</Form>
187+
</CreateBase>
188+
}
189+
/>
190+
</CoreAdmin>
191+
</TestMemoryRouter>
192+
);
193+
194+
WithRender.args = {
195+
meta: false,
196+
};
197+
198+
WithRender.argTypes = {
199+
meta: { control: 'boolean' },
200+
};
201+
202+
export const WithError = () => (
203+
<TestMemoryRouter initialEntries={['/posts/create']}>
204+
<CoreAdmin
205+
dataProvider={
206+
{
207+
getList: () => Promise.reject(new Error('fetch error')),
208+
getMany: () =>
209+
Promise.resolve({ data: [{ id: 5, name: 'test1' }] }),
210+
} as unknown as DataProvider
211+
}
212+
i18nProvider={i18nProvider}
213+
>
214+
<Resource
215+
name="posts"
216+
create={
217+
<CreateBase resource="posts" record={{ tags_ids: [1, 3] }}>
218+
<h1>Create Post</h1>
219+
<Form
220+
onSubmit={() => {}}
221+
defaultValues={{ tag_ids: [1, 3] }}
222+
>
223+
<ReferenceArrayInputBase
224+
reference="tags"
225+
resource="posts"
226+
source="tag_ids"
227+
>
228+
<CheckboxGroupInput />
229+
</ReferenceArrayInputBase>
230+
</Form>
231+
</CreateBase>
232+
}
233+
/>
234+
</CoreAdmin>
235+
</TestMemoryRouter>
236+
);

0 commit comments

Comments
 (0)