Skip to content

Commit b559604

Browse files
authored
Merge pull request #6266 from marmelab/ReferenceField-throw-when-no-associated-Resource
[DX] Thrown an error when using a Reference field without the associated Resource
2 parents f893eff + 5824673 commit b559604

6 files changed

+231
-8
lines changed

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

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, act } from '@testing-library/react';
3+
import { render, act, waitFor } from '@testing-library/react';
44
import { renderWithRedux } from 'ra-test';
55
import { MemoryRouter } from 'react-router-dom';
66
import { ListContextProvider, DataProviderContext } from 'ra-core';
@@ -244,4 +244,64 @@ describe('<ReferenceArrayField />', () => {
244244
await new Promise(resolve => setTimeout(resolve)); // wait for loaded to be true
245245
expect(queryByText('bar1')).not.toBeNull();
246246
});
247+
248+
it('should throw an error if used without a Resource for the reference', async () => {
249+
jest.spyOn(console, 'error').mockImplementation(() => {});
250+
class ErrorBoundary extends React.Component<
251+
{
252+
onError?: (
253+
error: Error,
254+
info: { componentStack: string }
255+
) => void;
256+
},
257+
{ error: Error | null }
258+
> {
259+
constructor(props) {
260+
super(props);
261+
this.state = { error: null };
262+
}
263+
264+
static getDerivedStateFromError(error) {
265+
// Update state so the next render will show the fallback UI.
266+
return { error };
267+
}
268+
269+
componentDidCatch(error, errorInfo) {
270+
// You can also log the error to an error reporting service
271+
this.props.onError(error, errorInfo);
272+
}
273+
274+
render() {
275+
if (this.state.error) {
276+
// You can render any custom fallback UI
277+
return <h1>Something went wrong.</h1>;
278+
}
279+
280+
return this.props.children;
281+
}
282+
}
283+
const onError = jest.fn();
284+
renderWithRedux(
285+
<ErrorBoundary onError={onError}>
286+
<ReferenceArrayField
287+
record={{ id: 123, barIds: [1, 2] }}
288+
className="myClass"
289+
resource="foos"
290+
reference="bars"
291+
source="barIds"
292+
basePath="/foos"
293+
>
294+
<SingleFieldList>
295+
<TextField source="title" />
296+
</SingleFieldList>
297+
</ReferenceArrayField>
298+
</ErrorBoundary>,
299+
{ admin: { resources: { comments: { data: {} } } } }
300+
);
301+
await waitFor(() => {
302+
expect(onError.mock.calls[0][0].message).toBe(
303+
'You must declare a <Resource name="bars"> in order to use a <ReferenceArrayField reference="bars">'
304+
);
305+
});
306+
});
247307
});

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

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { Children, cloneElement, FC, memo, ReactElement } from 'react';
33
import PropTypes from 'prop-types';
44
import { makeStyles } from '@material-ui/core/styles';
5+
import { useSelector } from 'react-redux';
56
import {
67
ListContextProvider,
78
useListContext,
@@ -11,6 +12,7 @@ import {
1112
FilterPayload,
1213
ResourceContextProvider,
1314
useRecordContext,
15+
ReduxState,
1416
} from 'ra-core';
1517

1618
import { fieldPropTypes, PublicFieldProps, InjectedFieldProps } from './types';
@@ -93,6 +95,17 @@ const ReferenceArrayField: FC<ReferenceArrayFieldProps> = props => {
9395
'<ReferenceArrayField> only accepts a single child (like <Datagrid>)'
9496
);
9597
}
98+
99+
const isReferenceDeclared = useSelector<ReduxState, boolean>(
100+
state => typeof state.admin.resources[props.reference] !== 'undefined'
101+
);
102+
103+
if (!isReferenceDeclared) {
104+
throw new Error(
105+
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceArrayField reference="${props.reference}">`
106+
);
107+
}
108+
96109
const controllerProps = useReferenceArrayFieldController({
97110
basePath,
98111
filter,

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

+67-5
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ describe('<ReferenceField />', () => {
131131
>
132132
<TextField source="title" />
133133
</ReferenceField>
134-
</DataProviderContext.Provider>
134+
</DataProviderContext.Provider>,
135+
{ admin: { resources: { posts: { data: {} } } } }
135136
);
136137
await new Promise(resolve => setTimeout(resolve, 10));
137138
expect(queryByRole('progressbar')).toBeNull();
@@ -156,7 +157,8 @@ describe('<ReferenceField />', () => {
156157
>
157158
<TextField source="title" />
158159
</ReferenceField>
159-
</DataProviderContext.Provider>
160+
</DataProviderContext.Provider>,
161+
{ admin: { resources: { posts: { data: {} } } } }
160162
);
161163
await new Promise(resolve => setTimeout(resolve, 10));
162164
expect(queryByRole('progressbar')).toBeNull();
@@ -176,7 +178,8 @@ describe('<ReferenceField />', () => {
176178
emptyText="EMPTY"
177179
>
178180
<TextField source="title" />
179-
</ReferenceField>
181+
</ReferenceField>,
182+
{ admin: { resources: { posts: { data: {} } } } }
180183
);
181184
expect(getByText('EMPTY')).not.toBeNull();
182185
});
@@ -260,7 +263,8 @@ describe('<ReferenceField />', () => {
260263
<TextField source="title" />
261264
</ReferenceField>
262265
</MemoryRouter>
263-
</DataProviderContext.Provider>
266+
</DataProviderContext.Provider>,
267+
{ admin: { resources: { posts: { data: {} } } } }
264268
);
265269
await waitFor(() => {
266270
const action = dispatch.mock.calls[0][0];
@@ -286,7 +290,8 @@ describe('<ReferenceField />', () => {
286290
>
287291
<TextField source="title" />
288292
</ReferenceField>
289-
</DataProviderContext.Provider>
293+
</DataProviderContext.Provider>,
294+
{ admin: { resources: { posts: { data: {} } } } }
290295
);
291296
await waitFor(() => {
292297
const ErrorIcon = getByRole('presentation', { hidden: true });
@@ -295,6 +300,63 @@ describe('<ReferenceField />', () => {
295300
});
296301
});
297302

303+
it('should throw an error if used without a Resource for the reference', async () => {
304+
jest.spyOn(console, 'error').mockImplementation(() => {});
305+
class ErrorBoundary extends React.Component<
306+
{
307+
onError?: (
308+
error: Error,
309+
info: { componentStack: string }
310+
) => void;
311+
},
312+
{ error: Error | null }
313+
> {
314+
constructor(props) {
315+
super(props);
316+
this.state = { error: null };
317+
}
318+
319+
static getDerivedStateFromError(error) {
320+
// Update state so the next render will show the fallback UI.
321+
return { error };
322+
}
323+
324+
componentDidCatch(error, errorInfo) {
325+
// You can also log the error to an error reporting service
326+
this.props.onError(error, errorInfo);
327+
}
328+
329+
render() {
330+
if (this.state.error) {
331+
// You can render any custom fallback UI
332+
return <h1>Something went wrong.</h1>;
333+
}
334+
335+
return this.props.children;
336+
}
337+
}
338+
const onError = jest.fn();
339+
renderWithRedux(
340+
<ErrorBoundary onError={onError}>
341+
<ReferenceField
342+
record={{ id: 123 }}
343+
resource="comments"
344+
source="postId"
345+
reference="posts"
346+
basePath="/comments"
347+
>
348+
<TextField source="title" />
349+
</ReferenceField>
350+
</ErrorBoundary>,
351+
{ admin: { resources: { comments: { data: {} } } } }
352+
);
353+
await waitFor(() => {
354+
expect(onError.mock.calls[0][0].message).toBe(
355+
'You must declare a <Resource name="posts"> in order to use a <ReferenceField reference="posts">'
356+
);
357+
});
358+
});
359+
298360
describe('ReferenceFieldView', () => {
299361
it('should render a link to specified resourceLinkPath', () => {
300362
const { container } = render(

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

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import get from 'lodash/get';
66
import { makeStyles } from '@material-ui/core/styles';
77
import { Typography } from '@material-ui/core';
88
import ErrorIcon from '@material-ui/icons/Error';
9+
import { useSelector } from 'react-redux';
910
import {
1011
useReference,
1112
UseReferenceProps,
@@ -15,6 +16,7 @@ import {
1516
RecordContextProvider,
1617
Record,
1718
useRecordContext,
19+
ReduxState,
1820
} from 'ra-core';
1921

2022
import LinearProgress from '../layout/LinearProgress';
@@ -70,6 +72,16 @@ import { ClassesOverride } from '../types';
7072
const ReferenceField: FC<ReferenceFieldProps> = props => {
7173
const { source, emptyText, ...rest } = props;
7274
const record = useRecordContext(props);
75+
const isReferenceDeclared = useSelector<ReduxState, boolean>(
76+
state => typeof state.admin.resources[props.reference] !== 'undefined'
77+
);
78+
79+
if (!isReferenceDeclared) {
80+
throw new Error(
81+
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceField reference="${props.reference}">`
82+
);
83+
}
84+
7385
return get(record, source) == null ? (
7486
emptyText ? (
7587
<Typography component="span" variant="body2">

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

+66-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import * as React from 'react';
2-
import { render } from '@testing-library/react';
2+
import expect from 'expect';
3+
import { render, waitFor } from '@testing-library/react';
34
import { createMemoryHistory } from 'history';
45
import { Router } from 'react-router-dom';
5-
import { ReferenceManyFieldView } from './ReferenceManyField';
6+
import { renderWithRedux } from 'ra-test';
7+
8+
import ReferenceManyField, {
9+
ReferenceManyFieldView,
10+
} from './ReferenceManyField';
611
import TextField from './TextField';
712
import SingleFieldList from '../list/SingleFieldList';
813

@@ -112,4 +117,63 @@ describe('<ReferenceManyField />', () => {
112117
expect(links[0].getAttribute('href')).toEqual('/posts/1');
113118
expect(links[1].getAttribute('href')).toEqual('/posts/2');
114119
});
120+
121+
it('should throw an error if used without a Resource for the reference', async () => {
122+
jest.spyOn(console, 'error').mockImplementation(() => {});
123+
class ErrorBoundary extends React.Component<
124+
{
125+
onError?: (
126+
error: Error,
127+
info: { componentStack: string }
128+
) => void;
129+
},
130+
{ error: Error | null }
131+
> {
132+
constructor(props) {
133+
super(props);
134+
this.state = { error: null };
135+
}
136+
137+
static getDerivedStateFromError(error) {
138+
// Update state so the next render will show the fallback UI.
139+
return { error };
140+
}
141+
142+
componentDidCatch(error, errorInfo) {
143+
// You can also log the error to an error reporting service
144+
this.props.onError(error, errorInfo);
145+
}
146+
147+
render() {
148+
if (this.state.error) {
149+
// You can render any custom fallback UI
150+
return <h1>Something went wrong.</h1>;
151+
}
152+
153+
return this.props.children;
154+
}
155+
}
156+
const onError = jest.fn();
157+
renderWithRedux(
158+
<ErrorBoundary onError={onError}>
159+
<ReferenceManyField
160+
record={{ id: 123 }}
161+
resource="comments"
162+
target="postId"
163+
reference="posts"
164+
basePath="/comments"
165+
>
166+
<SingleFieldList>
167+
<TextField source="title" />
168+
</SingleFieldList>
169+
</ReferenceManyField>
170+
</ErrorBoundary>,
171+
{ admin: { resources: { comments: { data: {} } } } }
172+
);
173+
await waitFor(() => {
174+
expect(onError.mock.calls[0][0].message).toBe(
175+
'You must declare a <Resource name="posts"> in order to use a <ReferenceManyField reference="posts">'
176+
);
177+
});
178+
});
115179
});

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

+12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
ListControllerProps,
99
ResourceContextProvider,
1010
useRecordContext,
11+
ReduxState,
1112
} from 'ra-core';
13+
import { useSelector } from 'react-redux';
1214

1315
import { PublicFieldProps, fieldPropTypes, InjectedFieldProps } from './types';
1416
import sanitizeFieldRestProps from './sanitizeFieldRestProps';
@@ -80,6 +82,16 @@ export const ReferenceManyField: FC<ReferenceManyFieldProps> = props => {
8082
);
8183
}
8284

85+
const isReferenceDeclared = useSelector<ReduxState, boolean>(
86+
state => typeof state.admin.resources[props.reference] !== 'undefined'
87+
);
88+
89+
if (!isReferenceDeclared) {
90+
throw new Error(
91+
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceManyField reference="${props.reference}">`
92+
);
93+
}
94+
8395
const controllerProps = useReferenceManyFieldController({
8496
basePath,
8597
filter,

0 commit comments

Comments
 (0)