diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx index 8f668082ba..5de569df05 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx @@ -1,124 +1,183 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; -import { UnconnectedReferenceArrayFieldController as ReferenceArrayFieldController } from './ReferenceArrayFieldController'; +import { cleanup } from 'react-testing-library'; -describe('', () => { - const crudGetManyAccumulate = jest.fn(); +import ReferenceArrayFieldController from './ReferenceArrayFieldController'; +import renderWithRedux from '../../util/renderWithRedux'; +import { crudGetManyAccumulate } from '../../actions'; +describe('', () => { + afterEach(cleanup); it('should set the loadedOnce prop to false when related records are not yet fetched', () => { - const children = jest.fn(); + const children = jest.fn().mockReturnValue('child'); - shallow( + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: {}, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, false); + expect(children.mock.calls[0][0]).toEqual({ + currentSort: { field: 'id', order: 'ASC' }, + loadedOnce: false, + referenceBasePath: '', + data: null, + ids: [1, 2], + }); }); it('should set the loadedOnce prop to true when at least one related record is found', () => { - const children = jest.fn(); + const children = jest.fn().mockReturnValue('child'); - shallow( + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 2: { + id: 2, + title: 'hello', + }, + }, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, true); + expect(children.mock.calls[0][0]).toEqual({ + currentSort: { field: 'id', order: 'ASC' }, + loadedOnce: true, + referenceBasePath: '', + data: { + 2: { + id: 2, + title: 'hello', + }, + }, + ids: [1, 2], + }); }); it('should set the data prop to the loaded data when it has been fetched', () => { - const children = jest.fn(); - const data = { - 1: { id: 1, title: 'hello' }, - 2: { id: 2, title: 'world' }, - }; - shallow( + const children = jest.fn().mockReturnValue('child'); + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 1: { id: 1, title: 'hello' }, + 2: { id: 2, title: 'world' }, + }, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, true); - assert.deepEqual(children.mock.calls[0][0].data, data); - assert.deepEqual(children.mock.calls[0][0].ids, [1, 2]); + expect(children.mock.calls[0][0]).toEqual({ + currentSort: { field: 'id', order: 'ASC' }, + loadedOnce: true, + referenceBasePath: '', + data: { + 1: { id: 1, title: 'hello' }, + 2: { id: 2, title: 'world' }, + }, + ids: [1, 2], + }); }); it('should support record with string identifier', () => { - const children = jest.fn(); - const data = { - 'abc-1': { id: 'abc-1', title: 'hello' }, - 'abc-2': { id: 'abc-2', title: 'world' }, - }; - shallow( + const children = jest.fn().mockReturnValue('child'); + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 'abc-1': { id: 'abc-1', title: 'hello' }, + 'abc-2': { id: 'abc-2', title: 'world' }, + }, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, true); - assert.deepEqual(children.mock.calls[0][0].data, data); - assert.deepEqual(children.mock.calls[0][0].ids, ['abc-1', 'abc-2']); + expect(children.mock.calls[0][0]).toEqual({ + currentSort: { field: 'id', order: 'ASC' }, + loadedOnce: true, + referenceBasePath: '', + data: { + 'abc-1': { id: 'abc-1', title: 'hello' }, + 'abc-2': { id: 'abc-2', title: 'world' }, + }, + ids: ['abc-1', 'abc-2'], + }); }); - it('should support record with number identifier', () => { - const children = jest.fn(); - const data = { - 1: { id: 1, title: 'hello' }, - 2: { id: 2, title: 'world' }, - }; - shallow( + it('should dispatch crudGetManyAccumulate', () => { + const children = jest.fn().mockReturnValue('child'); + const { dispatch } = renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: {}, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, true); - assert.deepEqual(children.mock.calls[0][0].data, data); - assert.deepEqual(children.mock.calls[0][0].ids, [1, 2]); + expect(dispatch).toBeCalledWith(crudGetManyAccumulate('bar', [1, 2])); }); }); diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx index a591e8e24c..14decb7a28 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx @@ -1,17 +1,7 @@ -import { Component, ReactNode } from 'react'; -import { connect } from 'react-redux'; -import get from 'lodash/get'; +import { FunctionComponent, ReactNode, ReactElement } from 'react'; -import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; -import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; -import { - ReduxState, - Record, - RecordMap, - Dispatch, - Sort, - Identifier, -} from '../../types'; +import useReferenceArray from './useReferenceArray'; +import { Identifier, RecordMap, Record, Sort } from '../..'; interface ChildrenFuncParams { loadedOnce: boolean; @@ -24,9 +14,6 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetManyAccumulate: Dispatch; - data?: RecordMap; - ids: Identifier[]; record?: Record; reference: string; resource: string; @@ -65,64 +52,27 @@ interface Props { * * */ -export class UnconnectedReferenceArrayFieldController extends Component { - componentDidMount() { - this.fetchReferences(); - } - - componentWillReceiveProps(nextProps) { - if ( - (this.props.record || { id: undefined }).id !== - (nextProps.record || {}).id - ) { - this.fetchReferences(nextProps); - } - } - - fetchReferences({ crudGetManyAccumulate, reference, ids } = this.props) { - crudGetManyAccumulate(reference, ids); - } - - render() { - const { +const ReferenceArrayFieldController: FunctionComponent = ({ + resource, + reference, + basePath, + record, + source, + children, +}) => { + return children({ + currentSort: { + field: 'id', + order: 'ASC', + }, + ...useReferenceArray({ resource, reference, - data, - ids, - children, basePath, - } = this.props; - - const referenceBasePath = basePath.replace(resource, reference); // FIXME obviously very weak - - return children({ - // tslint:disable-next-line:triple-equals - loadedOnce: data != undefined, - ids, - data, - referenceBasePath, - currentSort: { - field: 'id', - order: 'ASC', - }, - }); - } -} - -const mapStateToProps = (state: ReduxState, props: Props) => { - const { record, source, reference } = props; - const ids = get(record, source) || []; - return { - data: getReferencesByIds(state, reference, ids), - ids, - }; + record, + source, + }), + }) as ReactElement; }; -const ReferenceArrayFieldController = connect( - mapStateToProps, - { - crudGetManyAccumulate: crudGetManyAccumulateAction, - } -)(UnconnectedReferenceArrayFieldController); - export default ReferenceArrayFieldController; diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts index 50d5c15996..82efc59319 100644 --- a/packages/ra-core/src/controller/field/index.ts +++ b/packages/ra-core/src/controller/field/index.ts @@ -2,9 +2,11 @@ import ReferenceArrayFieldController from './ReferenceArrayFieldController'; import ReferenceFieldController from './ReferenceFieldController'; import ReferenceManyFieldController from './ReferenceManyFieldController'; import useReference from './useReference'; +import useReferenceArray from './useReferenceArray'; import useReferenceMany from './useReferenceMany'; export { + useReferenceArray, ReferenceArrayFieldController, ReferenceFieldController, useReference, diff --git a/packages/ra-core/src/controller/field/useReferenceArray.ts b/packages/ra-core/src/controller/field/useReferenceArray.ts new file mode 100644 index 0000000000..4065939b78 --- /dev/null +++ b/packages/ra-core/src/controller/field/useReferenceArray.ts @@ -0,0 +1,96 @@ +import { FunctionComponent, ReactNode, useEffect, ReactElement } from 'react'; +// @ts-ignore +import { useDispatch, useSelector } from 'react-redux'; +import get from 'lodash/get'; + +import { crudGetManyAccumulate } from '../../actions'; +import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; +import { ReduxState, Record, RecordMap, Sort, Identifier } from '../../types'; + +interface ReferenceArrayProps { + loadedOnce: boolean; + ids: Identifier[]; + data: RecordMap; + referenceBasePath: string; +} + +interface Option { + basePath: string; + record?: Record; + reference: string; + resource: string; + source: string; +} + +/** + * @typedef ReferenceArrayProps + * @type {Object} + * @property {boolean} loadedOnce: 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 + */ + +/** + * Hook that fetches records from another resource specified + * by an array of *ids* in current record. + * + * @example + * + * const { loadedOnce, data, ids, referenceBasePath, currentSort } = useReferenceArray({ + * basePath: 'resource'; + * record: { referenceIds: ['id1', 'id2']}; + * reference: 'reference'; + * resource: 'resource'; + * source: 'referenceIds'; + * }); + * + * @param {Object} option + * @param {boolean} option.allowEmpty do we allow for no referenced record (default to false) + * @param {string} option.basePath basepath to current resource + * @param {string | false} option.linkType The type of the link toward the referenced record. edit, show of false for no link (default to edit) + * @param {Object} option.record The The current resource record + * @param {string} option.reference The linked resource name + * @param {string} option.resource The current resource name + * @param {string} option.source The key of the linked resource identifier + * + * @returns {ReferenceProps} The reference props + */ +const useReferenceArray = ({ + resource, + reference, + basePath, + record, + source, +}: Option): ReferenceArrayProps => { + const dispatch = useDispatch(); + const { data, ids } = useSelector( + getReferenceArray({ record, source, reference }), + [record, source, reference] + ); + useEffect(() => { + dispatch(crudGetManyAccumulate(reference, ids)); + }, [reference, ids, record.id]); + + const referenceBasePath = basePath.replace(resource, reference); // FIXME obviously very weak + + return { + // tslint:disable-next-line:triple-equals + loadedOnce: data != undefined, + ids, + data, + referenceBasePath, + }; +}; + +const getReferenceArray = ({ record, source, reference }) => ( + state: ReduxState +) => { + const ids = get(record, source) || []; + return { + data: getReferencesByIds(state, reference, ids), + ids, + }; +}; + +export default useReferenceArray; diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.js b/packages/ra-ui-materialui/src/field/ReferenceArrayField.js index 24255b44d1..0fe5376803 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.js @@ -2,7 +2,7 @@ import React, { Children } from 'react'; import PropTypes from 'prop-types'; import LinearProgress from '@material-ui/core/LinearProgress'; import { withStyles, createStyles } from '@material-ui/core/styles'; -import { ReferenceArrayFieldController } from 'ra-core'; +import { useReferenceArray } from 'ra-core'; import { fieldPropTypes } from './types'; const styles = createStyles({ @@ -85,14 +85,11 @@ export const ReferenceArrayField = ({ children, ...props }) => { } return ( - - {controllerProps => ( - - )} - + ); };