diff --git a/UPGRADE.md b/UPGRADE.md index 5cb2bd40b8c..e76a2689507 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -331,4 +331,27 @@ When using custom component with ReferenceInputController, you should rename the + /> + )} + -``` \ No newline at end of file +``` + +## `loadedOnce` prop renamed as `loaded` + +The `List`, `ReferenceArrayfield` and `ReferenceManyField` used to inject an `loadedOnce` prop to their child. This prop has been renamed to `loaded`. + +As a consequence, the components usually used as children of these 3 components now accept a `loaded` prop instead of `loadedOnce`. This concerns `Datagrid`, `SingleFieldList`, and `GridList`. + +This change is transparent unless you use a custom view component inside a `List`, `ReferenceArrayfield` or `ReferenceManyField`. + +```diff +const PostList = props => ( + + + +) + +-const MyListView = ({ loadedOnce, ...props }) => ( ++const MyListView = ({ loaded, ...props }) => ( +- if (!loadedOnce) return null; ++ if (!loaded) return null; + // rest of the view +); +``` diff --git a/examples/demo/src/products/GridList.js b/examples/demo/src/products/GridList.js index 1a8642ced48..a003f90b946 100644 --- a/examples/demo/src/products/GridList.js +++ b/examples/demo/src/products/GridList.js @@ -106,7 +106,7 @@ const LoadedGridList = ({ ids, data, basePath, width }) => { ); }; -const GridList = ({ loadedOnce, ...props }) => - loadedOnce ? : ; +const GridList = ({ loaded, ...props }) => + loaded ? : ; export default withWidth()(GridList); diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx index 5de569df050..c871a0d33cd 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx @@ -7,7 +7,7 @@ import { crudGetManyAccumulate } from '../../actions'; describe('', () => { afterEach(cleanup); - it('should set the loadedOnce prop to false when related records are not yet fetched', () => { + it('should set the loaded prop to false when related records are not yet fetched', () => { const children = jest.fn().mockReturnValue('child'); renderWithRedux( @@ -32,14 +32,14 @@ describe('', () => { ); expect(children.mock.calls[0][0]).toEqual({ currentSort: { field: 'id', order: 'ASC' }, - loadedOnce: false, + loaded: false, referenceBasePath: '', data: null, ids: [1, 2], }); }); - it('should set the loadedOnce prop to true when at least one related record is found', () => { + it('should set the loaded prop to true when at least one related record is found', () => { const children = jest.fn().mockReturnValue('child'); renderWithRedux( @@ -70,7 +70,7 @@ describe('', () => { expect(children.mock.calls[0][0]).toEqual({ currentSort: { field: 'id', order: 'ASC' }, - loadedOnce: true, + loaded: true, referenceBasePath: '', data: { 2: { @@ -109,7 +109,7 @@ describe('', () => { ); expect(children.mock.calls[0][0]).toEqual({ currentSort: { field: 'id', order: 'ASC' }, - loadedOnce: true, + loaded: true, referenceBasePath: '', data: { 1: { id: 1, title: 'hello' }, @@ -146,7 +146,7 @@ describe('', () => { ); expect(children.mock.calls[0][0]).toEqual({ currentSort: { field: 'id', order: 'ASC' }, - loadedOnce: true, + loaded: true, referenceBasePath: '', data: { 'abc-1': { id: 'abc-1', title: 'hello' }, diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx index 14decb7a283..441b2abbec6 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx @@ -1,10 +1,10 @@ import { FunctionComponent, ReactNode, ReactElement } from 'react'; -import useReferenceArray from './useReferenceArray'; +import useReferenceArrayFieldController from './useReferenceArrayFieldController'; import { Identifier, RecordMap, Record, Sort } from '../..'; interface ChildrenFuncParams { - loadedOnce: boolean; + loaded: boolean; ids: Identifier[]; data: RecordMap; referenceBasePath: string; @@ -21,36 +21,9 @@ interface Props { } /** - * A container component that fetches records from another resource specified - * by an array of *ids* in current record. - * - * You must define the fields to be passed to the iterator component as children. - * - * @example Display all the products of the current order as datagrid - * // order = { - * // id: 123, - * // product_ids: [456, 457, 458], - * // } - * - * - * - * - * - * - * - * - * - * @example Display all the categories of the current product as a list of chips - * // product = { - * // id: 456, - * // category_ids: [11, 22, 33], - * // } - * - * - * - * - * + * Render prop version of the useReferenceArrayFieldController hook. * + * @see useReferenceArrayFieldController */ const ReferenceArrayFieldController: FunctionComponent = ({ resource, @@ -65,7 +38,7 @@ const ReferenceArrayFieldController: FunctionComponent = ({ field: 'id', order: 'ASC', }, - ...useReferenceArray({ + ...useReferenceArrayFieldController({ resource, reference, basePath, diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 1f22d0fa9dc..d1eaad7148a 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -5,7 +5,7 @@ import ReferenceManyFieldController from './ReferenceManyFieldController'; import renderWithRedux from '../../util/renderWithRedux'; describe('', () => { - it('should set loadedOnce to false when related records are not yet fetched', () => { + it('should set loaded to false when related records are not yet fetched', () => { const children = jest.fn().mockReturnValue('children'); const { dispatch } = renderWithRedux( - * - * - * - * - * - * - * - * - * @example Display all the books by the current author, only the title - * - * - * - * - * - * - * By default, restricts the possible values to 25. You can extend this limit - * by setting the `perPage` prop. - * - * @example - * - * ... - * - * - * By default, orders the possible values by id desc. You can change this order - * by setting the `sort` prop (an object with `field` and `order` properties). - * - * @example - * - * ... - * - * - * Also, you can filter the query used to populate the possible values. Use the - * `filter` prop for that. - * - * @example - * - * ... - * + * @see useReferenceManyFieldController */ export const ReferenceManyFieldController: FunctionComponent = ({ resource, @@ -100,10 +59,10 @@ export const ReferenceManyFieldController: FunctionComponent = ({ const { data, ids, - loadedOnce, + loaded, referenceBasePath, total, - } = useReferenceMany({ + } = useReferenceManyFieldController({ resource, reference, record, @@ -120,7 +79,7 @@ export const ReferenceManyFieldController: FunctionComponent = ({ currentSort: sort, data, ids, - loadedOnce, + loaded, page, perPage, referenceBasePath, diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts index 341d655ad65..1c5adcfc803 100644 --- a/packages/ra-core/src/controller/field/index.ts +++ b/packages/ra-core/src/controller/field/index.ts @@ -2,14 +2,14 @@ import ReferenceArrayFieldController from './ReferenceArrayFieldController'; import ReferenceFieldController from './ReferenceFieldController'; import ReferenceManyFieldController from './ReferenceManyFieldController'; import getResourceLinkPath from './getResourceLinkPath'; -import useReferenceArray from './useReferenceArray'; -import useReferenceMany from './useReferenceMany'; +import useReferenceArrayFieldController from './useReferenceArrayFieldController'; +import useReferenceManyFieldController from './useReferenceManyFieldController'; export { - useReferenceArray, + useReferenceArrayFieldController, ReferenceArrayFieldController, ReferenceFieldController, getResourceLinkPath, - useReferenceMany, + useReferenceManyFieldController, ReferenceManyFieldController, }; diff --git a/packages/ra-core/src/controller/field/useReferenceArray.ts b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts similarity index 88% rename from packages/ra-core/src/controller/field/useReferenceArray.ts rename to packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts index b70bb159683..4188845f190 100644 --- a/packages/ra-core/src/controller/field/useReferenceArray.ts +++ b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts @@ -8,7 +8,7 @@ import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; import { ReduxState, Record, RecordMap, Identifier } from '../../types'; interface ReferenceArrayProps { - loadedOnce: boolean; + loaded: boolean; ids: Identifier[]; data: RecordMap; referenceBasePath: string; @@ -25,7 +25,7 @@ interface Option { /** * @typedef ReferenceArrayProps * @type {Object} - * @property {boolean} loadedOnce: boolean indicating if the reference has already beeen loaded + * @property {boolean} loaded: boolean indicating if the reference has already beeen loaded * @property {Array} ids: the list of ids. * @property {Object} data: Object holding the reference data by their ids * @property {string} referenceBasePath basePath of the reference @@ -37,7 +37,7 @@ interface Option { * * @example * - * const { loadedOnce, data, ids, referenceBasePath, currentSort } = useReferenceArray({ + * const { loaded, data, ids, referenceBasePath, currentSort } = useReferenceArrayFieldController({ * basePath: 'resource'; * record: { referenceIds: ['id1', 'id2']}; * reference: 'reference'; @@ -56,7 +56,7 @@ interface Option { * * @returns {ReferenceProps} The reference props */ -const useReferenceArray = ({ +const useReferenceArrayFieldController = ({ resource, reference, basePath, @@ -76,7 +76,7 @@ const useReferenceArray = ({ return { // eslint-disable-next-line eqeqeq - loadedOnce: data != undefined, + loaded: data != undefined, ids, data, referenceBasePath, @@ -93,4 +93,4 @@ const getReferenceArray = ({ record, source, reference }) => ( }; }; -export default useReferenceArray; +export default useReferenceArrayFieldController; diff --git a/packages/ra-core/src/controller/field/useReferenceMany.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts similarity index 93% rename from packages/ra-core/src/controller/field/useReferenceMany.ts rename to packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index f09fc1aa50d..27eb67a1dca 100644 --- a/packages/ra-core/src/controller/field/useReferenceMany.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -15,7 +15,7 @@ import { Record, Sort, RecordMap, Identifier } from '../../types'; interface ReferenceManyProps { data: RecordMap; ids: Identifier[]; - loadedOnce: boolean; + loaded: boolean; referenceBasePath: string; total: number; } @@ -25,7 +25,7 @@ interface Options { data?: RecordMap; filter?: any; ids?: any[]; - loadedOnce?: boolean; + loaded?: boolean; page: number; perPage: number; record?: Record; @@ -44,7 +44,7 @@ const defaultFilter = {}; * @type {Object} * @property {Object} data: the referenced records dictionary by their ids. * @property {Object} ids: the list of referenced records ids. - * @property {boolean} loadedOnce: boolean indicating if the references has already be loaded loaded + * @property {boolean} loaded: boolean indicating if the references has already be loaded loaded * @property {string | false} referenceBasePath base path of the related record */ @@ -56,7 +56,7 @@ const defaultFilter = {}; * * @example * - * const { isLoading, referenceRecord, resourceLinkPath } = useReferenceMany({ + * const { isLoading, referenceRecord, resourceLinkPath } = useReferenceManyFieldController({ * resource * reference: 'users', * record: { @@ -83,7 +83,7 @@ const defaultFilter = {}; * * @returns {ReferenceManyProps} The reference many props */ -const useReferenceMany = ({ +const useReferenceManyFieldController = ({ resource, reference, record, @@ -139,7 +139,7 @@ const useReferenceMany = ({ return { data, ids, - loadedOnce: typeof ids !== 'undefined', + loaded: typeof ids !== 'undefined', referenceBasePath, total, }; @@ -177,4 +177,4 @@ const selectData = (reference, relatedTo) => state => const selectIds = relatedTo => state => getIds(state, relatedTo); const selectTotal = relatedTo => state => getTotal(state, relatedTo); -export default useReferenceMany; +export default useReferenceManyFieldController; diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.tsx index e343b3fb034..04042c2cca6 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -7,11 +7,9 @@ import { import { WrappedFieldInputProps } from 'redux-form'; import { Sort, Record } from '../../types'; -import useReferenceInput, { ReferenceInputValue } from './useReferenceInput'; -import { filter } from 'async'; - -const defaultReferenceSource = (resource: string, source: string) => - `${resource}@${source}`; +import useReferenceInputController, { + ReferenceInputValue, +} from './useReferenceInputController'; interface Props { allowEmpty?: boolean; @@ -23,7 +21,7 @@ interface Props { perPage?: number; record?: Record; reference: string; - referenceSource?: typeof defaultReferenceSource; + referenceSource?: (resource: string, source: string) => string; resource: string; sort?: Sort; source: string; @@ -31,107 +29,15 @@ interface Props { } /** - * An Input component for choosing a reference record. Useful for foreign keys. - * - * This component fetches the possible values in the reference resource - * (using the `CRUD_GET_MATCHING` REST method), then delegates rendering - * to a subcomponent, to which it passes the possible choices - * as the `choices` attribute. - * - * Use it with a selector component as child, like ``, - * ``, or ``. - * - * @example - * export const CommentEdit = (props) => ( - * - * - * - * - * - * - * - * ); - * - * @example - * export const CommentEdit = (props) => ( - * - * - * - * - * - * - * - * ); - * - * By default, restricts the possible values to 25. You can extend this limit - * by setting the `perPage` prop. - * - * @example - * - * - * - * - * By default, orders the possible values by id desc. You can change this order - * by setting the `sort` prop (an object with `field` and `order` properties). - * - * @example - * - * - * - * - * Also, you can filter the query used to populate the possible values. Use the - * `filter` prop for that. - * - * @example - * - * - * - * - * The enclosed component may filter results. ReferenceInput passes a `setFilter` - * function as prop to its child component. It uses the value to create a filter - * for the query - by default { q: [searchText] }. You can customize the mapping - * searchText => searchQuery by setting a custom `filterToQuery` function prop: + * Render prop version of the useReferenceInputController hook. * - * @example - * ({ title: searchText })}> - * - * + * @see useReferenceInputController */ export const ReferenceInputController: FunctionComponent = ({ - input, children, - perPage = 25, - filter: permanentFilter = {}, - reference, - filterToQuery, - referenceSource = defaultReferenceSource, - resource, - source, + ...props }) => { - return children( - useReferenceInput({ - input, - perPage, - permanentFilter: filter, - reference, - filterToQuery, - referenceSource, - resource, - source, - }) - ) as ReactElement; + return children(useReferenceInputController(props)) as ReactElement; }; export default ReferenceInputController as ComponentType; diff --git a/packages/ra-core/src/controller/input/index.ts b/packages/ra-core/src/controller/input/index.ts index 1f2dabd3ab0..2d84189d5da 100644 --- a/packages/ra-core/src/controller/input/index.ts +++ b/packages/ra-core/src/controller/input/index.ts @@ -1,4 +1,9 @@ import ReferenceArrayInputController from './ReferenceArrayInputController'; import ReferenceInputController from './ReferenceInputController'; +import useReferenceInputController from './useReferenceInputController'; -export { ReferenceArrayInputController, ReferenceInputController }; +export { + ReferenceArrayInputController, + ReferenceInputController, + useReferenceInputController, +}; diff --git a/packages/ra-core/src/controller/input/useReferenceInput.ts b/packages/ra-core/src/controller/input/useReferenceInputController.ts similarity index 90% rename from packages/ra-core/src/controller/input/useReferenceInput.ts rename to packages/ra-core/src/controller/input/useReferenceInputController.ts index 7071737d9c5..09e79f2daf7 100644 --- a/packages/ra-core/src/controller/input/useReferenceInput.ts +++ b/packages/ra-core/src/controller/input/useReferenceInputController.ts @@ -11,6 +11,7 @@ import useFilterState from '../useFilterState'; const defaultReferenceSource = (resource: string, source: string) => `${resource}@${source}`; +const defaultFilter = {}; export interface ReferenceInputValue { choices: Record[]; @@ -27,7 +28,7 @@ export interface ReferenceInputValue { interface Option { allowEmpty?: boolean; - permanentFilter?: any; + filter?: any; filterToQuery?: (filter: string) => any; input?: WrappedFieldInputProps; perPage?: number; @@ -49,7 +50,7 @@ interface Option { * @example * const { * choices, // the available reference resource - * } = useReferenceInput({ + * } = useReferenceInputController({ * input, // the input props * resource: 'comments', * reference: 'posts', @@ -66,7 +67,7 @@ interface Option { * const { * choices, // the available reference resource * setFilter, - * } = useReferenceInput({ + * } = useReferenceInputController({ * input, // the input props * resource: 'comments', * reference: 'posts', @@ -77,10 +78,10 @@ interface Option { * filterToQuery: searchText => ({ title: searchText }) * }); */ -export default ({ +const useReferenceInputController = ({ input, perPage = 25, - permanentFilter = {}, + filter = defaultFilter, reference, filterToQuery, referenceSource = defaultReferenceSource, @@ -91,15 +92,15 @@ export default ({ const { pagination, setPagination } = usePaginationState({ perPage }); const { sort, setSort } = useSortState(); - const { filter, setFilter } = useFilterState({ - permanentFilter, + const { filter: filterValue, setFilter } = useFilterState({ + permanentFilter: filter, filterToQuery, }); const { matchingReferences } = useGetMatchingReferences({ reference, referenceSource, - filter, + filter: filterValue, pagination, sort, resource, @@ -124,7 +125,7 @@ export default ({ choices: dataStatus.choices, error: dataStatus.error, loading: dataStatus.waiting, - filter, + filter: filterValue, setFilter, pagination, setPagination, @@ -133,3 +134,5 @@ export default ({ warning: dataStatus.warning, }; }; + +export default useReferenceInputController; diff --git a/packages/ra-core/src/controller/useListController.ts b/packages/ra-core/src/controller/useListController.ts index 431ce4cac98..cd9063dea22 100644 --- a/packages/ra-core/src/controller/useListController.ts +++ b/packages/ra-core/src/controller/useListController.ts @@ -51,7 +51,7 @@ export interface ListControllerProps { hideFilter: (filterName: string) => void; ids: Identifier[]; isLoading: boolean; - loadedOnce: boolean; + loaded: boolean; onSelect: (ids: Identifier[]) => void; onToggleItem: (id: Identifier) => void; onUnselectItems: () => void; @@ -175,7 +175,7 @@ const useListController = (props: ListProps): ListControllerProps => { hasCreate, ids, isLoading: loading, - loadedOnce: loaded, + loaded, onSelect: selectionModifiers.select, onToggleItem: selectionModifiers.toggle, onUnselectItems: selectionModifiers.clearSelection, @@ -205,7 +205,7 @@ export const injectedProps = [ 'hideFilter', 'ids', 'isLoading', - 'loadedOnce', + 'loaded', 'onSelect', 'onToggleItem', 'onUnselectItems', diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.js b/packages/ra-ui-materialui/src/field/ReferenceArrayField.js index 0fe53768033..df38144f4eb 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.js @@ -1,50 +1,10 @@ -import React, { Children } from 'react'; +import React, { Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; import LinearProgress from '@material-ui/core/LinearProgress'; -import { withStyles, createStyles } from '@material-ui/core/styles'; -import { useReferenceArray } from 'ra-core'; +import { makeStyles } from '@material-ui/core/styles'; +import { useReferenceArrayFieldController } from 'ra-core'; import { fieldPropTypes } from './types'; -const styles = createStyles({ - progress: { marginTop: '1em' }, -}); - -export const ReferenceArrayFieldView = ({ - children, - className, - classes = {}, - data, - ids, - loadedOnce, - reference, - referenceBasePath, -}) => { - if (loadedOnce === false) { - return ; - } - - return React.cloneElement(Children.only(children), { - className, - resource: reference, - ids, - data, - loadedOnce, - basePath: referenceBasePath, - currentSort: {}, - }); -}; - -ReferenceArrayFieldView.propTypes = { - classes: PropTypes.object, - className: PropTypes.string, - data: PropTypes.object, - ids: PropTypes.array, - loadedOnce: PropTypes.bool, - children: PropTypes.element.isRequired, - reference: PropTypes.string.isRequired, - referenceBasePath: PropTypes.string, -}; - /** * A container component that fetches records from another resource specified * by an array of *ids* in current record. @@ -87,38 +47,70 @@ export const ReferenceArrayField = ({ children, ...props }) => { return ( ); }; ReferenceArrayField.propTypes = { + ...fieldPropTypes, addLabel: PropTypes.bool, - basePath: PropTypes.string.isRequired, + basePath: PropTypes.string, classes: PropTypes.object, className: PropTypes.string, children: PropTypes.element.isRequired, label: PropTypes.string, - record: PropTypes.object.isRequired, + record: PropTypes.object, reference: PropTypes.string.isRequired, - resource: PropTypes.string.isRequired, + resource: PropTypes.string, sortBy: PropTypes.string, source: PropTypes.string.isRequired, }; -const EnhancedReferenceArrayField = withStyles(styles)(ReferenceArrayField); - -EnhancedReferenceArrayField.defaultProps = { +ReferenceArrayField.defaultProps = { addLabel: true, }; -EnhancedReferenceArrayField.propTypes = { - ...fieldPropTypes, - reference: PropTypes.string, - children: PropTypes.element.isRequired, +const useStyles = makeStyles(theme => ({ + progress: { marginTop: theme.spacing(2) }, +})); + +export const ReferenceArrayFieldView = ({ + children, + className, + classes: classesOverride, + data, + ids, + loaded, + reference, + referenceBasePath, +}) => { + const classes = useStyles({ classes: classesOverride }); + if (loaded === false) { + return ; + } + + return cloneElement(Children.only(children), { + className, + resource: reference, + ids, + data, + loaded, + basePath: referenceBasePath, + currentSort: {}, + }); }; -EnhancedReferenceArrayField.displayName = 'EnhancedReferenceArrayField'; +ReferenceArrayFieldView.propTypes = { + classes: PropTypes.object, + className: PropTypes.string, + data: PropTypes.object, + ids: PropTypes.array, + loaded: PropTypes.bool, + children: PropTypes.element.isRequired, + reference: PropTypes.string.isRequired, + referenceBasePath: PropTypes.string, +}; -export default EnhancedReferenceArrayField; +export default ReferenceArrayField; diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.js b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.js index d2fc261073c..9edae9b85e8 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.js +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.spec.js @@ -1,13 +1,16 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; +import { MemoryRouter } from 'react-router'; + import { ReferenceArrayFieldView } from './ReferenceArrayField'; import TextField from './TextField'; import SingleFieldList from '../list/SingleFieldList'; describe('', () => { + afterEach(cleanup); it('should render a loading indicator when related records are not yet fetched', () => { - const wrapper = shallow( + const { queryAllByRole } = render( ', () => { basePath="" data={null} ids={[1, 2]} - loadedOnce={false} + loaded={false} > ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 1); - const SingleFieldListElement = wrapper.find('SingleFieldList'); - assert.equal(SingleFieldListElement.length, 0); + expect(queryAllByRole('progressbar')).toHaveLength(1); }); it('should render a list of the child component', () => { @@ -36,36 +34,32 @@ describe('', () => { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - const wrapper = shallow( - - - - - - ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 0); - const SingleFieldListElement = wrapper.find( - 'WithStyles(SingleFieldList)' - ); - assert.equal(SingleFieldListElement.length, 1); - assert.equal(SingleFieldListElement.at(0).prop('resource'), 'bar'); - assert.deepEqual(SingleFieldListElement.at(0).prop('data'), data); - assert.deepEqual(SingleFieldListElement.at(0).prop('ids'), [1, 2]); + const { queryAllByRole, container, getByText } = render( + + + + + + + + ); + expect(queryAllByRole('progressbar')).toHaveLength(0); + expect(container.firstChild.textContent).not.toBeUndefined(); + expect(getByText('hello')).toBeDefined(); + expect(getByText('world')).toBeDefined(); }); it('should render nothing when there are no related records', () => { - const wrapper = shallow( + const { queryAllByRole, container } = render( ', () => { basePath="" data={{}} ids={[]} + loaded={true} > ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 0); - const SingleFieldListElement = wrapper.find( - 'WithStyles(SingleFieldList)' - ); - assert.equal(SingleFieldListElement.length, 1); - assert.equal(SingleFieldListElement.at(0).prop('resource'), 'bar'); - assert.deepEqual(SingleFieldListElement.at(0).prop('data'), {}); - assert.deepEqual(SingleFieldListElement.at(0).prop('ids'), []); + expect(queryAllByRole('progressbar')).toHaveLength(0); + expect(container.firstChild.textContent).toBe(''); }); it('should support record with string identifier', () => { @@ -98,35 +84,27 @@ describe('', () => { 'abc-1': { id: 'abc-1', title: 'hello' }, 'abc-2': { id: 'abc-2', title: 'world' }, }; - const wrapper = shallow( - - - - - - ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 0); - const SingleFieldListElement = wrapper.find( - 'WithStyles(SingleFieldList)' - ); - assert.equal(SingleFieldListElement.length, 1); - assert.equal(SingleFieldListElement.at(0).prop('resource'), 'bar'); - assert.deepEqual(SingleFieldListElement.at(0).prop('data'), data); - assert.deepEqual(SingleFieldListElement.at(0).prop('ids'), [ - 'abc-1', - 'abc-2', - ]); + const { queryAllByRole, container, getByText } = render( + + + + + + + + ); + expect(queryAllByRole('progressbar')).toHaveLength(0); + expect(container.firstChild.textContent).not.toBeUndefined(); + expect(getByText('hello')).toBeDefined(); + expect(getByText('world')).toBeDefined(); }); it('should support record with number identifier', () => { @@ -134,32 +112,27 @@ describe('', () => { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - const wrapper = shallow( - - - - - - ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 0); - const SingleFieldListElement = wrapper.find( - 'WithStyles(SingleFieldList)' - ); - assert.equal(SingleFieldListElement.length, 1); - assert.equal(SingleFieldListElement.at(0).prop('resource'), 'bar'); - assert.deepEqual(SingleFieldListElement.at(0).prop('data'), data); - assert.deepEqual(SingleFieldListElement.at(0).prop('ids'), [1, 2]); + const { queryAllByRole, container, getByText } = render( + + + + + + + + ); + expect(queryAllByRole('progressbar')).toHaveLength(0); + expect(container.firstChild.textContent).not.toBeUndefined(); + expect(getByText('hello')).toBeDefined(); + expect(getByText('world')).toBeDefined(); }); it('should use custom className', () => { @@ -167,29 +140,24 @@ describe('', () => { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - const wrapper = shallow( - - - - - - ); - const ProgressElements = wrapper.find( - 'WithStyles(ForwardRef(LinearProgress))' - ); - assert.equal(ProgressElements.length, 0); - const SingleFieldListElement = wrapper.find( - 'WithStyles(SingleFieldList)' - ); - assert.equal(SingleFieldListElement.at(0).prop('className'), 'myClass'); + const { container } = render( + + + + + + + + ); + expect(container.getElementsByClassName('myClass')).toHaveLength(1); }); }); diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.js b/packages/ra-ui-materialui/src/field/ReferenceField.js index 2c4b3ec1097..cf10e1329f8 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceField.js @@ -1,92 +1,14 @@ -import React, { Children } from 'react'; +import React, { Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { withStyles, createStyles } from '@material-ui/core/styles'; +import get from 'lodash/get'; +import { makeStyles } from '@material-ui/core/styles'; import { useReference, getResourceLinkPath } from 'ra-core'; import LinearProgress from '../layout/LinearProgress'; import Link from '../Link'; import sanitizeRestProps from './sanitizeRestProps'; -const styles = theme => - createStyles({ - link: { - color: theme.palette.primary.main, - }, - }); - -// useful to prevent click bubbling in a datagrid with rowClick -const stopPropagation = e => e.stopPropagation(); - -export const ReferenceFieldView = ({ - allowEmpty, - basePath, - children, - className, - classes = {}, - isLoading, - record, - reference, - referenceRecord, - resource, - resourceLinkPath, - source, - translateChoice = false, - ...rest -}) => { - if (isLoading) { - return ; - } - - if (resourceLinkPath) { - return ( - - {React.cloneElement(Children.only(children), { - className: classnames( - children.props.className, - classes.link // force color override for Typography components - ), - record: referenceRecord, - resource: reference, - allowEmpty, - basePath, - translateChoice, - ...sanitizeRestProps(rest), - })} - - ); - } - - return React.cloneElement(Children.only(children), { - record: referenceRecord, - resource: reference, - allowEmpty, - basePath, - translateChoice, - ...sanitizeRestProps(rest), - }); -}; - -ReferenceFieldView.propTypes = { - allowEmpty: PropTypes.bool, - basePath: PropTypes.string, - children: PropTypes.element, - className: PropTypes.string, - classes: PropTypes.object, - isLoading: PropTypes.bool, - record: PropTypes.object, - reference: PropTypes.string, - referenceRecord: PropTypes.object, - resource: PropTypes.string, - resourceLinkPath: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - source: PropTypes.string, - translateChoice: PropTypes.bool, -}; - /** * Fetch reference record, and delegate rendering to child component. * @@ -132,19 +54,22 @@ ReferenceFieldView.propTypes = { * backward-compatibility is still kept */ -const ReferenceField = ({ children, ...props }) => { +const ReferenceField = ({ children, record, source, ...props }) => { if (React.Children.count(children) !== 1) { throw new Error(' only accepts a single child'); } - - const { isLoading, referenceRecord } = useReference(props); + const id = get(record, source); + const { loading, referenceRecord } = useReference({ + id, + ...props, + }); const resourceLinkPath = getResourceLinkPath(props); return ( @@ -180,18 +105,90 @@ ReferenceField.propTypes = { }; ReferenceField.defaultProps = { + addLabel: true, allowEmpty: false, classes: {}, link: 'edit', record: {}, }; -const EnhancedReferenceField = withStyles(styles)(ReferenceField); +const useStyles = makeStyles(theme => ({ + link: { + color: theme.palette.primary.main, + }, +})); -EnhancedReferenceField.defaultProps = { - addLabel: true, +// useful to prevent click bubbling in a datagrid with rowClick +const stopPropagation = e => e.stopPropagation(); + +export const ReferenceFieldView = ({ + allowEmpty, + basePath, + children, + className, + classes: classesOverride, + loading, + record, + reference, + referenceRecord, + resource, + resourceLinkPath, + source, + translateChoice = false, + ...rest +}) => { + const classes = useStyles({ classes: classesOverride }); + if (loading) { + return ; + } + + if (resourceLinkPath) { + return ( + + {cloneElement(Children.only(children), { + className: classnames( + children.props.className, + classes.link // force color override for Typography components + ), + record: referenceRecord, + resource: reference, + allowEmpty, + basePath, + translateChoice, + ...sanitizeRestProps(rest), + })} + + ); + } + + return cloneElement(Children.only(children), { + record: referenceRecord, + resource: reference, + allowEmpty, + basePath, + translateChoice, + ...sanitizeRestProps(rest), + }); }; -EnhancedReferenceField.displayName = 'EnhancedReferenceField'; +ReferenceFieldView.propTypes = { + allowEmpty: PropTypes.bool, + basePath: PropTypes.string, + children: PropTypes.element, + className: PropTypes.string, + classes: PropTypes.object, + loading: PropTypes.bool, + record: PropTypes.object, + reference: PropTypes.string, + referenceRecord: PropTypes.object, + resource: PropTypes.string, + resourceLinkPath: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + source: PropTypes.string, + translateChoice: PropTypes.bool, +}; -export default EnhancedReferenceField; +export default ReferenceField; diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.js b/packages/ra-ui-materialui/src/field/ReferenceField.spec.js index 2af289e8170..21b9cce0477 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.js +++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.js @@ -1,30 +1,35 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; +import { render } from 'react-testing-library'; +import { MemoryRouter } from 'react-router'; import { ReferenceFieldView } from './ReferenceField'; import TextField from './TextField'; describe('', () => { it('should render a link to specified resourceLinkPath', () => { - const wrapper = shallow( - - - + const { container } = render( + + + + + ); - const linkElement = wrapper.find('WithStyles(Link)'); - assert.equal(linkElement.prop('to'), '/posts/123'); + const links = container.getElementsByTagName('a'); + expect(links).toHaveLength(1); + expect(links.item(0).href).toBe('http://localhost/posts/123'); }); + it('should render no link when resourceLinkPath is not specified', () => { - const wrapper = shallow( + const { container } = render( ', () => { ); - const linkElement = wrapper.find('WithStyles(Link)'); - assert.equal(linkElement.length, 0); + const links = container.getElementsByTagName('a'); + expect(links).toHaveLength(0); }); }); diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.js b/packages/ra-ui-materialui/src/field/ReferenceManyField.js index 1a02b542c0c..ff08ea0b87e 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.js @@ -1,63 +1,10 @@ import React, { Fragment, cloneElement, Children } from 'react'; import PropTypes from 'prop-types'; -import { useSortState, usePaginationState, useReferenceMany } from 'ra-core'; - -export const ReferenceManyFieldView = ({ - children, - className, - currentSort, - data, - ids, - loadedOnce, - page, - pagination, - perPage, - reference, - referenceBasePath, - setPage, - setPerPage, - setSort, - total, -}) => ( - - {cloneElement(Children.only(children), { - className, - resource: reference, - ids, - loadedOnce, - data, - basePath: referenceBasePath, - currentSort, - setSort, - total, - })} - {pagination && - total !== undefined && - cloneElement(pagination, { - page, - perPage, - setPage, - setPerPage, - total, - })} - -); - -ReferenceManyFieldView.propTypes = { - children: PropTypes.element, - className: PropTypes.string, - currentSort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.string, - }), - data: PropTypes.object, - ids: PropTypes.array, - loadedOnce: PropTypes.bool, - pagination: PropTypes.element, - reference: PropTypes.string, - referenceBasePath: PropTypes.string, - setSort: PropTypes.func, -}; +import { + useSortState, + usePaginationState, + useReferenceManyFieldController, +} from 'ra-core'; /** * Render related records to the current one. @@ -128,7 +75,7 @@ export const ReferenceManyField = props => { perPage: initialPerPage, }); - const useReferenceManyProps = useReferenceMany({ + const controllerProps = useReferenceManyFieldController({ resource, reference, record, @@ -151,7 +98,7 @@ export const ReferenceManyField = props => { setPage, setPerPage, setSort, - ...useReferenceManyProps, + ...controllerProps, }} /> ); @@ -186,4 +133,61 @@ ReferenceManyField.defaultProps = { addLabel: true, }; +export const ReferenceManyFieldView = ({ + children, + className, + currentSort, + data, + ids, + loaded, + page, + pagination, + perPage, + reference, + referenceBasePath, + setPage, + setPerPage, + setSort, + total, +}) => ( + + {cloneElement(Children.only(children), { + className, + resource: reference, + ids, + loaded, + data, + basePath: referenceBasePath, + currentSort, + setSort, + total, + })} + {pagination && + total !== undefined && + cloneElement(pagination, { + page, + perPage, + setPage, + setPerPage, + total, + })} + +); + +ReferenceManyFieldView.propTypes = { + children: PropTypes.element, + className: PropTypes.string, + currentSort: PropTypes.shape({ + field: PropTypes.string, + order: PropTypes.string, + }), + data: PropTypes.object, + ids: PropTypes.array, + loaded: PropTypes.bool, + pagination: PropTypes.element, + reference: PropTypes.string, + referenceBasePath: PropTypes.string, + setSort: PropTypes.func, +}; + export default ReferenceManyField; diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.js b/packages/ra-ui-materialui/src/input/ReferenceInput.js index 44e5d9aca19..b46bd68bb9c 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.js +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.js @@ -1,12 +1,127 @@ -import React from 'react'; +import React, { Children, cloneElement } from 'react'; import PropTypes from 'prop-types'; -import compose from 'recompose/compose'; -import { addField, translate, ReferenceInputController } from 'ra-core'; +import { addField, useReferenceInputController } from 'ra-core'; import LinearProgress from '../layout/LinearProgress'; import Labeled from './Labeled'; import ReferenceError from './ReferenceError'; +/** + * An Input component for choosing a reference record. Useful for foreign keys. + * + * This component fetches the possible values in the reference resource + * (using the `CRUD_GET_MATCHING` REST method), then delegates rendering + * to a subcomponent, to which it passes the possible choices + * as the `choices` attribute. + * + * Use it with a selector component as child, like ``, + * ``, or ``. + * + * @example + * export const CommentEdit = (props) => ( + * + * + * + * + * + * + * + * ); + * + * @example + * export const CommentEdit = (props) => ( + * + * + * + * + * + * + * + * ); + * + * By default, restricts the possible values to 25. You can extend this limit + * by setting the `perPage` prop. + * + * @example + * + * + * + * + * By default, orders the possible values by id desc. You can change this order + * by setting the `sort` prop (an object with `field` and `order` properties). + * + * @example + * + * + * + * + * Also, you can filter the query used to populate the possible values. Use the + * `filter` prop for that. + * + * @example + * + * + * + * + * The enclosed component may filter results. ReferenceInput passes a `setFilter` + * function as prop to its child component. It uses the value to create a filter + * for the query - by default { q: [searchText] }. You can customize the mapping + * searchText => searchQuery by setting a custom `filterToQuery` function prop: + * + * @example + * ({ title: searchText })}> + * + * + */ +export const ReferenceInput = props => ( + +); + +ReferenceInput.propTypes = { + allowEmpty: PropTypes.bool.isRequired, + basePath: PropTypes.string, + children: PropTypes.element.isRequired, + className: PropTypes.string, + classes: PropTypes.object, + filter: PropTypes.object, + filterToQuery: PropTypes.func.isRequired, + input: PropTypes.object.isRequired, + label: PropTypes.string, + meta: PropTypes.object, + onChange: PropTypes.func, + perPage: PropTypes.number, + record: PropTypes.object, + reference: PropTypes.string.isRequired, + resource: PropTypes.string.isRequired, + sort: PropTypes.shape({ + field: PropTypes.string, + order: PropTypes.oneOf(['ASC', 'DESC']), + }), + source: PropTypes.string, +}; + +ReferenceInput.defaultProps = { + allowEmpty: false, + filter: {}, + filterToQuery: searchText => ({ q: searchText }), + perPage: 25, + sort: { field: 'id', order: 'DESC' }, +}; + +const EnhancedReferenceInput = addField(ReferenceInput); + const sanitizeRestProps = ({ allowEmpty, basePath, @@ -39,7 +154,6 @@ const sanitizeRestProps = ({ sort, source, textAlign, - translate, translateChoice, validation, ...rest @@ -64,10 +178,13 @@ export const ReferenceInputView = ({ setPagination, setSort, source, - translate, warning, ...rest }) => { + if (Children.count(children) !== 1) { + throw new Error(' only accepts a single child'); + } + if (loading) { return ( ; } - return React.cloneElement(children, { + return cloneElement(children, { allowEmpty, classes, className, @@ -128,141 +245,7 @@ ReferenceInputView.propTypes = { setPagination: PropTypes.func, setSort: PropTypes.func, source: PropTypes.string, - translate: PropTypes.func.isRequired, warning: PropTypes.string, }; -/** - * An Input component for choosing a reference record. Useful for foreign keys. - * - * This component fetches the possible values in the reference resource - * (using the `CRUD_GET_MATCHING` REST method), then delegates rendering - * to a subcomponent, to which it passes the possible choices - * as the `choices` attribute. - * - * Use it with a selector component as child, like ``, - * ``, or ``. - * - * @example - * export const CommentEdit = (props) => ( - * - * - * - * - * - * - * - * ); - * - * @example - * export const CommentEdit = (props) => ( - * - * - * - * - * - * - * - * ); - * - * By default, restricts the possible values to 25. You can extend this limit - * by setting the `perPage` prop. - * - * @example - * - * - * - * - * By default, orders the possible values by id desc. You can change this order - * by setting the `sort` prop (an object with `field` and `order` properties). - * - * @example - * - * - * - * - * Also, you can filter the query used to populate the possible values. Use the - * `filter` prop for that. - * - * @example - * - * - * - * - * The enclosed component may filter results. ReferenceInput passes a `setFilter` - * function as prop to its child component. It uses the value to create a filter - * for the query - by default { q: [searchText] }. You can customize the mapping - * searchText => searchQuery by setting a custom `filterToQuery` function prop: - * - * @example - * ({ title: searchText })}> - * - * - */ -export const ReferenceInput = ({ children, ...props }) => { - if (React.Children.count(children) !== 1) { - throw new Error(' only accepts a single child'); - } - - return ( - - {controllerProps => ( - - )} - - ); -}; - -ReferenceInput.propTypes = { - allowEmpty: PropTypes.bool.isRequired, - basePath: PropTypes.string, - children: PropTypes.element.isRequired, - className: PropTypes.string, - classes: PropTypes.object, - filter: PropTypes.object, - filterToQuery: PropTypes.func.isRequired, - input: PropTypes.object.isRequired, - label: PropTypes.string, - meta: PropTypes.object, - onChange: PropTypes.func, - perPage: PropTypes.number, - record: PropTypes.object, - reference: PropTypes.string.isRequired, - resource: PropTypes.string.isRequired, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(['ASC', 'DESC']), - }), - source: PropTypes.string, - translate: PropTypes.func.isRequired, -}; - -ReferenceInput.defaultProps = { - allowEmpty: false, - filter: {}, - filterToQuery: searchText => ({ q: searchText }), - perPage: 25, - sort: { field: 'id', order: 'DESC' }, -}; - -const EnhancedReferenceInput = compose( - addField, - translate -)(ReferenceInput); - export default EnhancedReferenceInput; diff --git a/packages/ra-ui-materialui/src/input/ReferenceInput.spec.js b/packages/ra-ui-materialui/src/input/ReferenceInput.spec.js index 1c155367959..d79c59911e8 100644 --- a/packages/ra-ui-materialui/src/input/ReferenceInput.spec.js +++ b/packages/ra-ui-materialui/src/input/ReferenceInput.spec.js @@ -12,7 +12,6 @@ describe('', () => { reference: 'posts', resource: 'comments', source: 'post_id', - translate: x => `*${x}*`, }; const MyComponent = () => ; diff --git a/packages/ra-ui-materialui/src/list/Datagrid.js b/packages/ra-ui-materialui/src/list/Datagrid.js index 4fc93797cfc..9e92efa306b 100644 --- a/packages/ra-ui-materialui/src/list/Datagrid.js +++ b/packages/ra-ui-materialui/src/list/Datagrid.js @@ -133,7 +133,7 @@ class Datagrid extends Component { hover, ids, isLoading, - loadedOnce, + loaded, onSelect, onToggleItem, resource, @@ -147,11 +147,11 @@ class Datagrid extends Component { } = this.props; /** - * if loadedOnce is false, the list displays for the first time, and the dataProvider hasn't answered yet - * if loadedOnce is true, the data for the list has at least been returned once by the dataProvider - * if loadedOnce is undefined, the Datagrid parent doesn't track loading state (e.g. ReferenceArrayField) + * if loaded is false, the list displays for the first time, and the dataProvider hasn't answered yet + * if loaded is true, the data for the list has at least been returned once by the dataProvider + * if loaded is undefined, the Datagrid parent doesn't track loading state (e.g. ReferenceArrayField) */ - if (loadedOnce === false) { + if (loaded === false) { return ( props; @@ -65,7 +65,7 @@ export class SingleFieldList extends Component { className, ids, data, - loadedOnce, + loaded, resource, basePath, children, @@ -73,7 +73,7 @@ export class SingleFieldList extends Component { ...rest } = this.props; - if (loadedOnce === false) { + if (loaded === false) { return ; } @@ -90,7 +90,7 @@ export class SingleFieldList extends Component { if (resourceLinkPath) { return (