From 58246739f7f5d16a56412bf01037b07b826c14d0 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 10 May 2021 11:36:49 +0200 Subject: [PATCH] [DX] Thrown an error when using a Reference field without the associated Resource Closes #6258 --- .../src/field/ReferenceArrayField.spec.tsx | 62 +++++++++++++++- .../src/field/ReferenceArrayField.tsx | 13 ++++ .../src/field/ReferenceField.spec.tsx | 72 +++++++++++++++++-- .../src/field/ReferenceField.tsx | 12 ++++ .../src/field/ReferenceManyField.spec.tsx | 68 +++++++++++++++++- .../src/field/ReferenceManyField.tsx | 12 ++++ 6 files changed, 231 insertions(+), 8 deletions(-) diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx index 6e202918a10..78d96edb9d5 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.tsx @@ -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'; @@ -244,4 +244,64 @@ describe('', () => { 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

Something went wrong.

; + } + + return this.props.children; + } + } + const onError = jest.fn(); + renderWithRedux( + + + + + + + , + { admin: { resources: { comments: { data: {} } } } } + ); + await waitFor(() => { + expect(onError.mock.calls[0][0].message).toBe( + 'You must declare a in order to use a ' + ); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx index 31f913d4a96..a733dd7827f 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx @@ -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, @@ -11,6 +12,7 @@ import { FilterPayload, ResourceContextProvider, useRecordContext, + ReduxState, } from 'ra-core'; import { fieldPropTypes, PublicFieldProps, InjectedFieldProps } from './types'; @@ -93,6 +95,17 @@ const ReferenceArrayField: FC = props => { ' only accepts a single child (like )' ); } + + const isReferenceDeclared = useSelector( + state => typeof state.admin.resources[props.reference] !== 'undefined' + ); + + if (!isReferenceDeclared) { + throw new Error( + `You must declare a in order to use a ` + ); + } + const controllerProps = useReferenceArrayFieldController({ basePath, filter, diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx index b522b9de872..9ff80cd2efa 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx @@ -131,7 +131,8 @@ describe('', () => { > - + , + { admin: { resources: { posts: { data: {} } } } } ); await new Promise(resolve => setTimeout(resolve, 10)); expect(queryByRole('progressbar')).toBeNull(); @@ -156,7 +157,8 @@ describe('', () => { > - + , + { admin: { resources: { posts: { data: {} } } } } ); await new Promise(resolve => setTimeout(resolve, 10)); expect(queryByRole('progressbar')).toBeNull(); @@ -176,7 +178,8 @@ describe('', () => { emptyText="EMPTY" > - + , + { admin: { resources: { posts: { data: {} } } } } ); expect(getByText('EMPTY')).not.toBeNull(); }); @@ -260,7 +263,8 @@ describe('', () => { - + , + { admin: { resources: { posts: { data: {} } } } } ); await waitFor(() => { const action = dispatch.mock.calls[0][0]; @@ -286,7 +290,8 @@ describe('', () => { > - + , + { admin: { resources: { posts: { data: {} } } } } ); await waitFor(() => { const ErrorIcon = getByRole('presentation', { hidden: true }); @@ -295,6 +300,63 @@ describe('', () => { }); }); + 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

Something went wrong.

; + } + + return this.props.children; + } + } + const onError = jest.fn(); + renderWithRedux( + + + + + , + { admin: { resources: { comments: { data: {} } } } } + ); + await waitFor(() => { + expect(onError.mock.calls[0][0].message).toBe( + 'You must declare a in order to use a ' + ); + }); + }); + describe('ReferenceFieldView', () => { it('should render a link to specified resourceLinkPath', () => { const { container } = render( diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx index fb98669b97b..db5625cdf4d 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx @@ -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, @@ -15,6 +16,7 @@ import { RecordContextProvider, Record, useRecordContext, + ReduxState, } from 'ra-core'; import LinearProgress from '../layout/LinearProgress'; @@ -70,6 +72,16 @@ import { ClassesOverride } from '../types'; const ReferenceField: FC = props => { const { source, emptyText, ...rest } = props; const record = useRecordContext(props); + const isReferenceDeclared = useSelector( + state => typeof state.admin.resources[props.reference] !== 'undefined' + ); + + if (!isReferenceDeclared) { + throw new Error( + `You must declare a in order to use a ` + ); + } + return get(record, source) == null ? ( emptyText ? ( diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx index dad58468ed4..cbf318132f6 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx @@ -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'; @@ -112,4 +117,63 @@ describe('', () => { 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

Something went wrong.

; + } + + return this.props.children; + } + } + const onError = jest.fn(); + renderWithRedux( + + + + + + + , + { admin: { resources: { comments: { data: {} } } } } + ); + await waitFor(() => { + expect(onError.mock.calls[0][0].message).toBe( + 'You must declare a in order to use a ' + ); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx index 3fa136b0613..e5ddda8b55b 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx @@ -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'; @@ -80,6 +82,16 @@ export const ReferenceManyField: FC = props => { ); } + const isReferenceDeclared = useSelector( + state => typeof state.admin.resources[props.reference] !== 'undefined' + ); + + if (!isReferenceDeclared) { + throw new Error( + `You must declare a in order to use a ` + ); + } + const controllerProps = useReferenceManyFieldController({ basePath, filter,