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 => (
-
- )}
-
+
);
};