Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DX] Thrown an error when using a Reference field without the associated Resource #6266

Merged
merged 1 commit into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import expect from 'expect';
import { render, act } from '@testing-library/react';
import { render, act, waitFor } from '@testing-library/react';
import { renderWithRedux } from 'ra-test';
import { MemoryRouter } from 'react-router-dom';
import { ListContextProvider, DataProviderContext } from 'ra-core';
Expand Down Expand Up @@ -244,4 +244,64 @@ describe('<ReferenceArrayField />', () => {
await new Promise(resolve => setTimeout(resolve)); // wait for loaded to be true
expect(queryByText('bar1')).not.toBeNull();
});

it('should throw an error if used without a Resource for the reference', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorBoundary extends React.Component<
{
onError?: (
error: Error,
info: { componentStack: string }
) => void;
},
{ error: Error | null }
> {
constructor(props) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}

componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
this.props.onError(error, errorInfo);
}

render() {
if (this.state.error) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}
const onError = jest.fn();
renderWithRedux(
<ErrorBoundary onError={onError}>
<ReferenceArrayField
record={{ id: 123, barIds: [1, 2] }}
className="myClass"
resource="foos"
reference="bars"
source="barIds"
basePath="/foos"
>
<SingleFieldList>
<TextField source="title" />
</SingleFieldList>
</ReferenceArrayField>
</ErrorBoundary>,
{ admin: { resources: { comments: { data: {} } } } }
);
await waitFor(() => {
expect(onError.mock.calls[0][0].message).toBe(
'You must declare a <Resource name="bars"> in order to use a <ReferenceArrayField reference="bars">'
);
});
});
});
13 changes: 13 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { Children, cloneElement, FC, memo, ReactElement } from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import { useSelector } from 'react-redux';
import {
ListContextProvider,
useListContext,
Expand All @@ -11,6 +12,7 @@ import {
FilterPayload,
ResourceContextProvider,
useRecordContext,
ReduxState,
} from 'ra-core';

import { fieldPropTypes, PublicFieldProps, InjectedFieldProps } from './types';
Expand Down Expand Up @@ -93,6 +95,17 @@ const ReferenceArrayField: FC<ReferenceArrayFieldProps> = props => {
'<ReferenceArrayField> only accepts a single child (like <Datagrid>)'
);
}

const isReferenceDeclared = useSelector<ReduxState, boolean>(
state => typeof state.admin.resources[props.reference] !== 'undefined'
);

if (!isReferenceDeclared) {
throw new Error(
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceArrayField reference="${props.reference}">`
);
}

const controllerProps = useReferenceArrayFieldController({
basePath,
filter,
Expand Down
72 changes: 67 additions & 5 deletions packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ describe('<ReferenceField />', () => {
>
<TextField source="title" />
</ReferenceField>
</DataProviderContext.Provider>
</DataProviderContext.Provider>,
{ admin: { resources: { posts: { data: {} } } } }
);
await new Promise(resolve => setTimeout(resolve, 10));
expect(queryByRole('progressbar')).toBeNull();
Expand All @@ -156,7 +157,8 @@ describe('<ReferenceField />', () => {
>
<TextField source="title" />
</ReferenceField>
</DataProviderContext.Provider>
</DataProviderContext.Provider>,
{ admin: { resources: { posts: { data: {} } } } }
);
await new Promise(resolve => setTimeout(resolve, 10));
expect(queryByRole('progressbar')).toBeNull();
Expand All @@ -176,7 +178,8 @@ describe('<ReferenceField />', () => {
emptyText="EMPTY"
>
<TextField source="title" />
</ReferenceField>
</ReferenceField>,
{ admin: { resources: { posts: { data: {} } } } }
);
expect(getByText('EMPTY')).not.toBeNull();
});
Expand Down Expand Up @@ -260,7 +263,8 @@ describe('<ReferenceField />', () => {
<TextField source="title" />
</ReferenceField>
</MemoryRouter>
</DataProviderContext.Provider>
</DataProviderContext.Provider>,
{ admin: { resources: { posts: { data: {} } } } }
);
await waitFor(() => {
const action = dispatch.mock.calls[0][0];
Expand All @@ -286,7 +290,8 @@ describe('<ReferenceField />', () => {
>
<TextField source="title" />
</ReferenceField>
</DataProviderContext.Provider>
</DataProviderContext.Provider>,
{ admin: { resources: { posts: { data: {} } } } }
);
await waitFor(() => {
const ErrorIcon = getByRole('presentation', { hidden: true });
Expand All @@ -295,6 +300,63 @@ describe('<ReferenceField />', () => {
});
});

it('should throw an error if used without a Resource for the reference', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorBoundary extends React.Component<
{
onError?: (
error: Error,
info: { componentStack: string }
) => void;
},
{ error: Error | null }
> {
constructor(props) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}

componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
this.props.onError(error, errorInfo);
}

render() {
if (this.state.error) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}
const onError = jest.fn();
renderWithRedux(
<ErrorBoundary onError={onError}>
<ReferenceField
record={{ id: 123 }}
resource="comments"
source="postId"
reference="posts"
basePath="/comments"
>
<TextField source="title" />
</ReferenceField>
</ErrorBoundary>,
{ admin: { resources: { comments: { data: {} } } } }
);
await waitFor(() => {
expect(onError.mock.calls[0][0].message).toBe(
'You must declare a <Resource name="posts"> in order to use a <ReferenceField reference="posts">'
);
});
});

describe('ReferenceFieldView', () => {
it('should render a link to specified resourceLinkPath', () => {
const { container } = render(
Expand Down
12 changes: 12 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import get from 'lodash/get';
import { makeStyles } from '@material-ui/core/styles';
import { Typography } from '@material-ui/core';
import ErrorIcon from '@material-ui/icons/Error';
import { useSelector } from 'react-redux';
import {
useReference,
UseReferenceProps,
Expand All @@ -15,6 +16,7 @@ import {
RecordContextProvider,
Record,
useRecordContext,
ReduxState,
} from 'ra-core';

import LinearProgress from '../layout/LinearProgress';
Expand Down Expand Up @@ -70,6 +72,16 @@ import { ClassesOverride } from '../types';
const ReferenceField: FC<ReferenceFieldProps> = props => {
const { source, emptyText, ...rest } = props;
const record = useRecordContext(props);
const isReferenceDeclared = useSelector<ReduxState, boolean>(
state => typeof state.admin.resources[props.reference] !== 'undefined'
);

if (!isReferenceDeclared) {
throw new Error(
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceField reference="${props.reference}">`
);
}

return get(record, source) == null ? (
emptyText ? (
<Typography component="span" variant="body2">
Expand Down
68 changes: 66 additions & 2 deletions packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import expect from 'expect';
import { render, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import { ReferenceManyFieldView } from './ReferenceManyField';
import { renderWithRedux } from 'ra-test';

import ReferenceManyField, {
ReferenceManyFieldView,
} from './ReferenceManyField';
import TextField from './TextField';
import SingleFieldList from '../list/SingleFieldList';

Expand Down Expand Up @@ -112,4 +117,63 @@ describe('<ReferenceManyField />', () => {
expect(links[0].getAttribute('href')).toEqual('/posts/1');
expect(links[1].getAttribute('href')).toEqual('/posts/2');
});

it('should throw an error if used without a Resource for the reference', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
class ErrorBoundary extends React.Component<
{
onError?: (
error: Error,
info: { componentStack: string }
) => void;
},
{ error: Error | null }
> {
constructor(props) {
super(props);
this.state = { error: null };
}

static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}

componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
this.props.onError(error, errorInfo);
}

render() {
if (this.state.error) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}
const onError = jest.fn();
renderWithRedux(
<ErrorBoundary onError={onError}>
<ReferenceManyField
record={{ id: 123 }}
resource="comments"
target="postId"
reference="posts"
basePath="/comments"
>
<SingleFieldList>
<TextField source="title" />
</SingleFieldList>
</ReferenceManyField>
</ErrorBoundary>,
{ admin: { resources: { comments: { data: {} } } } }
);
await waitFor(() => {
expect(onError.mock.calls[0][0].message).toBe(
'You must declare a <Resource name="posts"> in order to use a <ReferenceManyField reference="posts">'
);
});
});
});
12 changes: 12 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
ListControllerProps,
ResourceContextProvider,
useRecordContext,
ReduxState,
} from 'ra-core';
import { useSelector } from 'react-redux';

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

const isReferenceDeclared = useSelector<ReduxState, boolean>(
state => typeof state.admin.resources[props.reference] !== 'undefined'
);

if (!isReferenceDeclared) {
throw new Error(
`You must declare a <Resource name="${props.reference}"> in order to use a <ReferenceManyField reference="${props.reference}">`
);
}

const controllerProps = useReferenceManyFieldController({
basePath,
filter,
Expand Down