diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 4bd21889fa8..cc401f4839a 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -1,103 +1,157 @@ import React from 'react'; import assert from 'assert'; import { render } from 'react-testing-library'; -import { UnconnectedReferenceManyFieldController as ReferenceManyFieldController } from './ReferenceManyFieldController'; + +import ReferenceManyFieldController from './ReferenceManyFieldController'; +import renderWithRedux from '../../util/renderWithRedux'; describe('', () => { it('should set loadedOnce to false when related records are not yet fetched', () => { - const children = jest.fn().mockReturnValue('children');; - const crudGetManyReference = jest.fn(); - render( + const children = jest.fn().mockReturnValue('children'); + const { dispatch } = renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 1: { id: 1, title: 'hello' }, + 2: { id: 2, title: 'world' }, + }, + }, + }, + references: { + oneToMany: { + 'foo_bar@fooId_barId': { + ids: [1, 2], + }, + }, + }, + }, + } ); - assert.equal(children.mock.calls[0][0].loadedOnce, false); + assert.deepEqual(dispatch.mock.calls[0], [ + { + meta: { + fetch: 'GET_MANY_REFERENCE', + onFailure: { + notification: { + body: 'ra.notification.http_error', + level: 'warning', + }, + }, + relatedTo: 'foo_bar@foo_id_undefined', + resource: 'bar', + }, + payload: { + filter: {}, + id: undefined, + pagination: { page: 1, perPage: 25 }, + sort: { field: 'id', order: 'DESC' }, + source: undefined, + target: 'foo_id', + }, + type: 'RA/CRUD_GET_MANY_REFERENCE', + }, + ]); }); it('should pass data and ids to children function', () => { - const children = jest.fn().mockReturnValue('children');; - const crudGetManyReference = jest.fn(); + const children = jest.fn().mockReturnValue('children'); const data = { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - render( + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 1: { id: 1, title: 'hello' }, + 2: { id: 2, title: 'world' }, + }, + }, + }, + references: { + oneToMany: { + 'foo_bar@fooId_barId': { + ids: [1, 2], + }, + }, + }, + }, + } ); assert.deepEqual(children.mock.calls[0][0].data, data); assert.deepEqual(children.mock.calls[0][0].ids, [1, 2]); }); it('should support record with string identifier', () => { - const children = jest.fn().mockReturnValue('children');; - const crudGetManyReference = jest.fn(); - const data = { - 'abc-1': { id: 'abc-1', title: 'hello' }, - 'abc-2': { id: 'abc-2', title: 'world' }, - }; - render( + const children = jest.fn().mockReturnValue('children'); + renderWithRedux( {children} - + , + { + admin: { + resources: { + bar: { + data: { + 'abc-1': { id: 'abc-1', title: 'hello' }, + 'abc-2': { id: 'abc-2', title: 'world' }, + }, + }, + }, + references: { + oneToMany: { + 'foo_bar@fooId_barId': { + ids: ['abc-1', 'abc-2'], + }, + }, + }, + }, + } ); - assert.deepEqual(children.mock.calls[0][0].data, data); + assert.deepEqual(children.mock.calls[0][0].data, { + 'abc-1': { id: 'abc-1', title: 'hello' }, + 'abc-2': { id: 'abc-2', title: 'world' }, + }); assert.deepEqual(children.mock.calls[0][0].ids, ['abc-1', 'abc-2']); }); - it('should support record with number identifier', () => { - const children = jest.fn().mockReturnValue('children');; - const crudGetManyReference = jest.fn(); - const data = { - 1: { id: 1, title: 'hello' }, - 2: { id: 2, title: 'world' }, - }; - render( - - {children} - - ); - assert.deepEqual(children.mock.calls[0][0].data, data); - assert.deepEqual(children.mock.calls[0][0].ids, [1, 2]); - }); - it('should support custom source', () => { const children = jest.fn().mockReturnValue('children'); - const crudGetManyReference = jest.fn(); - render( + const { dispatch } = renderWithRedux( ', () => { basePath="" record={{ id: 'not me', customId: 1 }} source="customId" - crudGetManyReference={crudGetManyReference} > {children} ); - assert.equal(crudGetManyReference.mock.calls[0][2], 1); + assert.deepEqual(dispatch.mock.calls[0], [ + { + meta: { + fetch: 'GET_MANY_REFERENCE', + onFailure: { + notification: { + body: 'ra.notification.http_error', + level: 'warning', + }, + }, + relatedTo: 'posts_comments@post_id_1', + resource: 'comments', + }, + payload: { + filter: {}, + id: 1, + pagination: { page: 1, perPage: 25 }, + sort: { field: 'id', order: 'DESC' }, + source: 'customId', + target: 'post_id', + }, + type: 'RA/CRUD_GET_MANY_REFERENCE', + }, + ]); }); it('should call crudGetManyReference when its props changes', () => { - const crudGetManyReference = jest.fn(); const ControllerWrapper = props => ( ', () => { reference="bar" target="foo_id" basePath="" - data={{ - 1: { id: 1, title: 'hello' }, - 2: { id: 2, title: 'world' }, - }} - ids={[1, 2]} source="id" - crudGetManyReference={crudGetManyReference} {...props} > {() => 'null'} ); - const { rerender } = render(); + const { rerender, dispatch } = renderWithRedux(); rerender(); - expect(crudGetManyReference).toBeCalledTimes(2); + expect(dispatch).toBeCalledTimes(2); - assert.deepEqual(crudGetManyReference.mock.calls[0], [ - 'bar', - 'foo_id', - 1, - 'foo_bar@foo_id_1', - { page: 1, perPage: 25 }, - { field: 'id', order: 'DESC' }, - {}, - 'id', + assert.deepEqual(dispatch.mock.calls[0], [ + { + meta: { + fetch: 'GET_MANY_REFERENCE', + onFailure: { + notification: { + body: 'ra.notification.http_error', + level: 'warning', + }, + }, + relatedTo: 'foo_bar@foo_id_1', + resource: 'bar', + }, + payload: { + filter: {}, + id: 1, + pagination: { page: 1, perPage: 25 }, + sort: { field: 'id', order: 'DESC' }, + source: 'id', + target: 'foo_id', + }, + type: 'RA/CRUD_GET_MANY_REFERENCE', + }, ]); - assert.deepEqual(crudGetManyReference.mock.calls[1], [ - 'bar', - 'foo_id', - 1, - 'foo_bar@foo_id_1', - { page: 1, perPage: 25 }, - { field: 'id', order: 'ASC' }, - {}, - 'id', + assert.deepEqual(dispatch.mock.calls[1], [ + { + meta: { + fetch: 'GET_MANY_REFERENCE', + onFailure: { + notification: { + body: 'ra.notification.http_error', + level: 'warning', + }, + }, + relatedTo: 'foo_bar@foo_id_1', + resource: 'bar', + }, + payload: { + filter: {}, + id: 1, + pagination: { page: 1, perPage: 25 }, + sort: { field: 'id', order: 'ASC' }, + source: 'id', + target: 'foo_id', + }, + type: 'RA/CRUD_GET_MANY_REFERENCE', + }, ]); }); }); diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index 56a833e0884..4f70b49be02 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,8 +1,9 @@ -import { ReactNode, useState, useReducer, useEffect } from 'react'; -import { connect } from 'react-redux'; +import { ReactNode, useState, useReducer, useEffect, useMemo } from 'react'; +// @ts-ignore +import { useSelector, useDispatch } from 'react-redux'; import get from 'lodash/get'; -import { crudGetManyReference as crudGetManyReferenceAction } from '../../actions'; +import { crudGetManyReference } from '../../actions'; import { SORT_ASC, SORT_DESC, @@ -32,7 +33,6 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetManyReference: Dispatch; data?: RecordMap; filter?: any; ids?: any[]; @@ -106,29 +106,37 @@ const defaultFilter = {}; * ... * */ -export const UnconnectedReferenceManyFieldController = ({ +export const ReferenceManyFieldController = ({ resource, reference, record, target, filter = defaultFilter, source, - crudGetManyReference, - data, - ids, children, basePath, - total, perPage = 25, sort = { field: 'id', order: 'DESC' }, }) => { const referenceId = get(record, source); + const relatedTo = useMemo( + () => nameRelatedTo(reference, referenceId, resource, target, filter), + [filter, reference, referenceId, resource, target] + ); + const ids = useSelector(selectIds(relatedTo), [relatedTo]); + const data = useSelector(selectData(reference, relatedTo), [ + reference, + relatedTo, + ]); + const total = useSelector(selectTotal(relatedTo), [relatedTo]); const [page, setPage] = useState(1); const [currentPerPage, setPerPage] = useState(perPage); useEffect(() => setPerPage(perPage), [perPage]); const [currentSort, setSort] = useReducer(sortReducer, sort); useEffect(() => setSort(sort), [sort.field, sort.order]); + const dispatch = useDispatch(); + useEffect( fetchReferences({ reference, @@ -137,10 +145,10 @@ export const UnconnectedReferenceManyFieldController = ({ target, filter, source, - crudGetManyReference, page, perPage: currentPerPage, sort: currentSort, + dispatch, }), [ reference, @@ -188,7 +196,7 @@ const fetchReferences = ({ target, filter, source, - crudGetManyReference, + dispatch, page, perPage, sort, @@ -201,38 +209,24 @@ const fetchReferences = ({ filter ); - crudGetManyReference( - reference, - target, - referenceId, - relatedTo, - { page, perPage }, - sort, - filter, - source + dispatch( + crudGetManyReference( + reference, + target, + referenceId, + relatedTo, + { page, perPage }, + sort, + filter, + source + ) ); }; -function mapStateToProps(state, props) { - const relatedTo = nameRelatedTo( - props.reference, - get(props.record, props.source), - props.resource, - props.target, - props.filter - ); - return { - data: getReferences(state, props.reference, relatedTo), - ids: getIds(state, relatedTo), - total: getTotal(state, relatedTo), - }; -} +const selectData = (reference, relatedTo) => state => + getReferences(state, reference, relatedTo); -const ReferenceManyFieldController = connect( - mapStateToProps, - { - crudGetManyReference: crudGetManyReferenceAction, - } -)(UnconnectedReferenceManyFieldController); +const selectIds = relatedTo => state => getIds(state, relatedTo); +const selectTotal = relatedTo => state => getTotal(state, relatedTo); export default ReferenceManyFieldController;