From e14f814b9c9b78c04cde3ecc58d0cc4452596cd2 Mon Sep 17 00:00:00 2001 From: thiery Date: Wed, 15 May 2019 16:39:27 +0200 Subject: [PATCH 1/8] refactor ReferenceManyFieldController to functional component with hooks --- .../ReferenceManyFieldController.spec.tsx | 49 ++-- .../field/ReferenceManyFieldController.tsx | 212 +++++++++--------- 2 files changed, 140 insertions(+), 121 deletions(-) diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 9e178a90685..4bd21889fa8 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -1,14 +1,13 @@ import React from 'react'; import assert from 'assert'; -import { shallow } from 'enzyme'; import { render } from 'react-testing-library'; import { UnconnectedReferenceManyFieldController as ReferenceManyFieldController } from './ReferenceManyFieldController'; describe('', () => { it('should set loadedOnce to false when related records are not yet fetched', () => { - const children = jest.fn(); + const children = jest.fn().mockReturnValue('children');; const crudGetManyReference = jest.fn(); - shallow( + render( ', () => { crudGetManyReference={crudGetManyReference} > {children} - , - { disableLifecycleMethods: true } + ); assert.equal(children.mock.calls[0][0].loadedOnce, false); }); it('should pass data and ids to children function', () => { - const children = jest.fn(); + const children = jest.fn().mockReturnValue('children');; const crudGetManyReference = jest.fn(); const data = { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - shallow( + render( ', () => { crudGetManyReference={crudGetManyReference} > {children} - , - { disableLifecycleMethods: true } + ); 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(); + 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' }, }; - shallow( + render( ', () => { crudGetManyReference={crudGetManyReference} > {children} - , - { disableLifecycleMethods: true } + ); assert.deepEqual(children.mock.calls[0][0].data, data); assert.deepEqual(children.mock.calls[0][0].ids, ['abc-1', 'abc-2']); }); it('should support record with number identifier', () => { - const children = jest.fn(); + const children = jest.fn().mockReturnValue('children');; const crudGetManyReference = jest.fn(); const data = { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, }; - shallow( + render( ', () => { crudGetManyReference={crudGetManyReference} > {children} - , - { disableLifecycleMethods: true } + ); 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(); + const children = jest.fn().mockReturnValue('children'); const crudGetManyReference = jest.fn(); - shallow( + render( ', () => { 2: { id: 2, title: 'world' }, }} ids={[1, 2]} + source="id" crudGetManyReference={crudGetManyReference} {...props} > - {() => null} + {() => 'null'} ); const { rerender } = render(); rerender(); + expect(crudGetManyReference).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(crudGetManyReference.mock.calls[1], [ 'bar', 'foo_id', diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index cf291bc16b0..56a833e0884 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,7 +1,6 @@ -import { Component, ReactNode } from 'react'; +import { ReactNode, useState, useReducer, useEffect } from 'react'; import { connect } from 'react-redux'; import get from 'lodash/get'; -import isEqual from 'lodash/isEqual'; import { crudGetManyReference as crudGetManyReferenceAction } from '../../actions'; import { @@ -48,11 +47,18 @@ interface Props { total?: number; } -interface State { - sort: Sort; - page: number; - perPage: number; -} +const sortReducer = (state: Sort, field: string | Sort): Sort => { + if (typeof field !== 'string') { + return field; + } + const order = + state.field === field && state.order === SORT_ASC + ? SORT_DESC + : SORT_ASC; + return { field, order }; +}; + +const defaultFilter = {}; /** * Render related records to the current one. @@ -100,108 +106,112 @@ interface State { * ... * */ -export class UnconnectedReferenceManyFieldController extends Component< - Props, - State -> { - public static defaultProps: Partial = { - filter: {}, - perPage: 25, - sort: { field: 'id', order: 'DESC' }, - source: 'id', - }; - - public state: State = { - sort: this.props.sort, - page: 1, - perPage: this.props.perPage, - }; - - componentDidMount() { - this.fetchReferences(); - } - - componentWillReceiveProps(nextProps: Props) { - if ( - this.props.record.id !== nextProps.record.id || - !isEqual(this.props.filter, nextProps.filter) - ) { - this.fetchReferences(nextProps); - } - - if (!isEqual(this.props.sort, nextProps.sort)) { - this.setState({ sort: nextProps.sort }, this.fetchReferences); - } - } - - setSort = (field: string) => { - const order = - this.state.sort.field === field && - this.state.sort.order === SORT_ASC - ? SORT_DESC - : SORT_ASC; - this.setState({ sort: { field, order } }, this.fetchReferences); - }; - - setPage = (page: number) => this.setState({ page }, this.fetchReferences); - - setPerPage = (perPage: number) => - this.setState({ perPage }, this.fetchReferences); - - fetchReferences( - { reference, record, resource, target, filter, source } = this.props - ) { - const { crudGetManyReference } = this.props; - const { page, perPage, sort } = this.state; - const relatedTo = nameRelatedTo( +export const UnconnectedReferenceManyFieldController = ({ + 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 [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]); + + useEffect( + fetchReferences({ reference, - get(record, source), + referenceId, resource, target, - filter - ); - - crudGetManyReference( + filter, + source, + crudGetManyReference, + page, + perPage: currentPerPage, + sort: currentSort, + }), + [ reference, + referenceId, + resource, target, - get(record, source), - relatedTo, - { page, perPage }, - sort, filter, - source - ); - } - - render() { - const { - resource, - reference, - data, - ids, - children, - basePath, - total, - } = this.props; - const { page, perPage } = this.state; + source, + crudGetManyReference, + page, + currentPerPage, + currentSort.field, + currentSort.order, + ] + ); - const referenceBasePath = basePath.replace(resource, reference); + const referenceBasePath = basePath.replace(resource, reference); + + return children({ + reference, + record, + resource, + target, + filter, + source, + sort, + currentSort, + data, + ids, + loadedOnce: typeof ids !== 'undefined', + page, + perPage: currentPerPage, + referenceBasePath, + setPage, + setPerPage, + setSort, + total, + }); +}; + +const fetchReferences = ({ + reference, + referenceId, + resource, + target, + filter, + source, + crudGetManyReference, + page, + perPage, + sort, +}) => () => { + const relatedTo = nameRelatedTo( + reference, + referenceId, + resource, + target, + filter + ); - return children({ - currentSort: this.state.sort, - data, - ids, - loadedOnce: typeof ids !== 'undefined', - page, - perPage, - referenceBasePath, - setPage: this.setPage, - setPerPage: this.setPerPage, - setSort: this.setSort, - total, - }); - } -} + crudGetManyReference( + reference, + target, + referenceId, + relatedTo, + { page, perPage }, + sort, + filter, + source + ); +}; function mapStateToProps(state, props) { const relatedTo = nameRelatedTo( From b166d03c8d1851525967e61c44c65cce39682b88 Mon Sep 17 00:00:00 2001 From: thiery Date: Thu, 16 May 2019 21:53:00 +0200 Subject: [PATCH 2/8] allow renderWithRedux to support rerender --- packages/ra-core/src/util/renderWithRedux.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ra-core/src/util/renderWithRedux.tsx b/packages/ra-core/src/util/renderWithRedux.tsx index a5e84a8947c..b62582c5979 100644 --- a/packages/ra-core/src/util/renderWithRedux.tsx +++ b/packages/ra-core/src/util/renderWithRedux.tsx @@ -33,6 +33,17 @@ export default (component, initialState = {}) => { return { ...renderResult, + rerender: newComponent => { + return renderResult.rerender( + + {({ store }) => { + dispatch = jest.spyOn(store, 'dispatch'); + reduxStore = store; + return newComponent; + }} + + ); + }, dispatch, reduxStore, }; From c0e6773b1acec498f6b98e2a021e6aabd8dd9f3c Mon Sep 17 00:00:00 2001 From: thiery Date: Thu, 16 May 2019 22:06:09 +0200 Subject: [PATCH 3/8] use react-redux hooks --- .../ReferenceManyFieldController.spec.tsx | 263 ++++++++++++------ .../field/ReferenceManyFieldController.tsx | 74 +++-- 2 files changed, 214 insertions(+), 123 deletions(-) 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; From 4c73aa0f589552e2c9e9c42394fc112e6dc34c0d Mon Sep 17 00:00:00 2001 From: thiery Date: Mon, 20 May 2019 09:36:49 +0200 Subject: [PATCH 4/8] add useReferenceMany Hooks --- .../field/ReferenceManyFieldController.tsx | 142 +++----------- .../src/controller/field/useReferenceMany.ts | 177 ++++++++++++++++++ 2 files changed, 205 insertions(+), 114 deletions(-) create mode 100644 packages/ra-core/src/controller/field/useReferenceMany.ts diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index 4f70b49be02..f6b8adc9a3b 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,20 +1,11 @@ -import { ReactNode, useState, useReducer, useEffect, useMemo } from 'react'; -// @ts-ignore -import { useSelector, useDispatch } from 'react-redux'; -import get from 'lodash/get'; +import { ReactElement, FunctionComponent } from 'react'; -import { crudGetManyReference } from '../../actions'; import { SORT_ASC, SORT_DESC, } from '../../reducer/admin/resource/list/queryReducer'; -import { - getIds, - getReferences, - getTotal, - nameRelatedTo, -} from '../../reducer/admin/references/oneToMany'; import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; +import useReferenceMany from './useReferenceMany'; interface ChildrenFuncParams { currentSort: Sort; @@ -32,11 +23,8 @@ interface ChildrenFuncParams { interface Props { basePath: string; - children: (params: ChildrenFuncParams) => ReactNode; - data?: RecordMap; + children: (params: ChildrenFuncParams) => ReactElement; filter?: any; - ids?: any[]; - loadedOnce?: boolean; perPage?: number; record?: Record; reference: string; @@ -58,8 +46,6 @@ const sortReducer = (state: Sort, field: string | Sort): Sort => { return { field, order }; }; -const defaultFilter = {}; - /** * Render related records to the current one. * @@ -106,79 +92,47 @@ const defaultFilter = {}; * ... * */ -export const ReferenceManyFieldController = ({ +export const ReferenceManyFieldController: FunctionComponent = ({ resource, reference, record, target, - filter = defaultFilter, + filter, source, - children, basePath, - perPage = 25, - sort = { field: 'id', order: 'DESC' }, + perPage, + sort, + children, }) => { - 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, - referenceId, - resource, - target, - filter, - source, - page, - perPage: currentPerPage, - sort: currentSort, - dispatch, - }), - [ - reference, - referenceId, - resource, - target, - filter, - source, - crudGetManyReference, - page, - currentPerPage, - currentSort.field, - currentSort.order, - ] - ); - - const referenceBasePath = basePath.replace(resource, reference); - - return children({ + const { + currentSort, + data, + ids, + loadedOnce, + page, + currentPerPage, + referenceBasePath, + setPage, + setPerPage, + setSort, + total, + } = useReferenceMany({ + resource, reference, record, - resource, target, filter, source, + basePath, + perPage, sort, + }); + + return children({ currentSort, data, ids, - loadedOnce: typeof ids !== 'undefined', + loadedOnce, page, perPage: currentPerPage, referenceBasePath, @@ -189,44 +143,4 @@ export const ReferenceManyFieldController = ({ }); }; -const fetchReferences = ({ - reference, - referenceId, - resource, - target, - filter, - source, - dispatch, - page, - perPage, - sort, -}) => () => { - const relatedTo = nameRelatedTo( - reference, - referenceId, - resource, - target, - filter - ); - - dispatch( - crudGetManyReference( - reference, - target, - referenceId, - relatedTo, - { page, perPage }, - sort, - filter, - source - ) - ); -}; - -const selectData = (reference, relatedTo) => state => - getReferences(state, reference, relatedTo); - -const selectIds = relatedTo => state => getIds(state, relatedTo); -const selectTotal = relatedTo => state => getTotal(state, relatedTo); - export default ReferenceManyFieldController; diff --git a/packages/ra-core/src/controller/field/useReferenceMany.ts b/packages/ra-core/src/controller/field/useReferenceMany.ts new file mode 100644 index 00000000000..87f50f5e3bd --- /dev/null +++ b/packages/ra-core/src/controller/field/useReferenceMany.ts @@ -0,0 +1,177 @@ +import { ReactNode, useState, useReducer, useEffect, useMemo } from 'react'; +// @ts-ignore +import { useSelector, useDispatch } from 'react-redux'; +import get from 'lodash/get'; + +import { crudGetManyReference } from '../../actions'; +import { + SORT_ASC, + SORT_DESC, +} from '../../reducer/admin/resource/list/queryReducer'; +import { + getIds, + getReferences, + getTotal, + nameRelatedTo, +} from '../../reducer/admin/references/oneToMany'; +import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; + +interface ChildrenFuncParams { + currentSort: Sort; + data: RecordMap; + ids: Identifier[]; + loadedOnce: boolean; + page: number; + currentPerPage: number; + referenceBasePath: string; + setPage: (page: number) => void; + setPerPage: (perPage: number) => void; + setSort: (field: string) => void; + total: number; +} + +interface Options { + basePath: string; + data?: RecordMap; + filter?: any; + ids?: any[]; + loadedOnce?: boolean; + perPage?: number; + record?: Record; + reference: string; + resource: string; + sort?: Sort; + source: string; + target: string; + total?: number; +} + +const sortReducer = (state: Sort, field: string | Sort): Sort => { + if (typeof field !== 'string') { + return field; + } + const order = + state.field === field && state.order === SORT_ASC + ? SORT_DESC + : SORT_ASC; + return { field, order }; +}; + +const defaultFilter = {}; + +const useReferenceMany = ({ + resource, + reference, + record, + target, + filter = defaultFilter, + source, + basePath, + perPage = 25, + sort = { field: 'id', order: 'DESC' }, +}: Options): ChildrenFuncParams => { + 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, + referenceId, + resource, + target, + filter, + source, + page, + perPage: currentPerPage, + sort: currentSort, + dispatch, + }), + [ + reference, + referenceId, + resource, + target, + filter, + source, + crudGetManyReference, + page, + currentPerPage, + currentSort.field, + currentSort.order, + ] + ); + + const referenceBasePath = basePath.replace(resource, reference); + + return { + currentSort, + data, + ids, + loadedOnce: typeof ids !== 'undefined', + page, + currentPerPage, + referenceBasePath, + setPage, + setPerPage, + setSort, + total, + }; +}; + +const fetchReferences = ({ + reference, + referenceId, + resource, + target, + filter, + source, + dispatch, + page, + perPage, + sort, +}) => () => { + const relatedTo = nameRelatedTo( + reference, + referenceId, + resource, + target, + filter + ); + + dispatch( + crudGetManyReference( + reference, + target, + referenceId, + relatedTo, + { page, perPage }, + sort, + filter, + source + ) + ); +}; + +const selectData = (reference, relatedTo) => state => + getReferences(state, reference, relatedTo); + +const selectIds = relatedTo => state => getIds(state, relatedTo); +const selectTotal = relatedTo => state => getTotal(state, relatedTo); + +export default useReferenceMany; From f526de93fa26a7651aedb7a7aa21304bedb2ce9b Mon Sep 17 00:00:00 2001 From: thiery Date: Tue, 21 May 2019 13:52:32 +0200 Subject: [PATCH 5/8] extract usePagination and useSort from useReferenceMany --- .../field/ReferenceManyFieldController.tsx | 40 +++++---------- .../src/controller/field/useReferenceMany.ts | 50 ++++--------------- .../ra-core/src/controller/usePagination.ts | 21 ++++++++ packages/ra-core/src/controller/useSort.ts | 31 ++++++++++++ 4 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 packages/ra-core/src/controller/usePagination.ts create mode 100644 packages/ra-core/src/controller/useSort.ts diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index f6b8adc9a3b..f627d7b962a 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,11 +1,9 @@ import { ReactElement, FunctionComponent } from 'react'; -import { - SORT_ASC, - SORT_DESC, -} from '../../reducer/admin/resource/list/queryReducer'; -import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; +import { Record, Sort, RecordMap, Identifier } from '../../types'; import useReferenceMany from './useReferenceMany'; +import useSort from '../useSort'; +import usePagination from '../usePagination'; interface ChildrenFuncParams { currentSort: Sort; @@ -35,17 +33,6 @@ interface Props { total?: number; } -const sortReducer = (state: Sort, field: string | Sort): Sort => { - if (typeof field !== 'string') { - return field; - } - const order = - state.field === field && state.order === SORT_ASC - ? SORT_DESC - : SORT_ASC; - return { field, order }; -}; - /** * Render related records to the current one. * @@ -100,21 +87,19 @@ export const ReferenceManyFieldController: FunctionComponent = ({ filter, source, basePath, - perPage, - sort, + perPage: initialPerPage, + sort: initialSort, children, }) => { + const { sort, sortBy } = useSort(initialSort); + const { page, perPage, setPage, setPerPage } = usePagination( + initialPerPage + ); const { - currentSort, data, ids, loadedOnce, - page, - currentPerPage, referenceBasePath, - setPage, - setPerPage, - setSort, total, } = useReferenceMany({ resource, @@ -125,20 +110,21 @@ export const ReferenceManyFieldController: FunctionComponent = ({ source, basePath, perPage, + page, sort, }); return children({ - currentSort, + currentSort: sort, data, ids, loadedOnce, page, - perPage: currentPerPage, + perPage, referenceBasePath, setPage, setPerPage, - setSort, + setSort: sortBy, total, }); }; diff --git a/packages/ra-core/src/controller/field/useReferenceMany.ts b/packages/ra-core/src/controller/field/useReferenceMany.ts index 87f50f5e3bd..7744e7bf046 100644 --- a/packages/ra-core/src/controller/field/useReferenceMany.ts +++ b/packages/ra-core/src/controller/field/useReferenceMany.ts @@ -1,13 +1,9 @@ -import { ReactNode, useState, useReducer, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; // @ts-ignore import { useSelector, useDispatch } from 'react-redux'; import get from 'lodash/get'; import { crudGetManyReference } from '../../actions'; -import { - SORT_ASC, - SORT_DESC, -} from '../../reducer/admin/resource/list/queryReducer'; import { getIds, getReferences, @@ -17,16 +13,10 @@ import { import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; interface ChildrenFuncParams { - currentSort: Sort; data: RecordMap; ids: Identifier[]; loadedOnce: boolean; - page: number; - currentPerPage: number; referenceBasePath: string; - setPage: (page: number) => void; - setPerPage: (perPage: number) => void; - setSort: (field: string) => void; total: number; } @@ -36,7 +26,8 @@ interface Options { filter?: any; ids?: any[]; loadedOnce?: boolean; - perPage?: number; + page: number; + perPage: number; record?: Record; reference: string; resource: string; @@ -46,17 +37,6 @@ interface Options { total?: number; } -const sortReducer = (state: Sort, field: string | Sort): Sort => { - if (typeof field !== 'string') { - return field; - } - const order = - state.field === field && state.order === SORT_ASC - ? SORT_DESC - : SORT_ASC; - return { field, order }; -}; - const defaultFilter = {}; const useReferenceMany = ({ @@ -67,7 +47,8 @@ const useReferenceMany = ({ filter = defaultFilter, source, basePath, - perPage = 25, + page, + perPage, sort = { field: 'id', order: 'DESC' }, }: Options): ChildrenFuncParams => { const referenceId = get(record, source); @@ -81,11 +62,6 @@ const useReferenceMany = ({ 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(); @@ -98,8 +74,8 @@ const useReferenceMany = ({ filter, source, page, - perPage: currentPerPage, - sort: currentSort, + perPage, + sort, dispatch, }), [ @@ -111,25 +87,19 @@ const useReferenceMany = ({ source, crudGetManyReference, page, - currentPerPage, - currentSort.field, - currentSort.order, + perPage, + sort.field, + sort.order, ] ); const referenceBasePath = basePath.replace(resource, reference); return { - currentSort, data, ids, loadedOnce: typeof ids !== 'undefined', - page, - currentPerPage, referenceBasePath, - setPage, - setPerPage, - setSort, total, }; }; diff --git a/packages/ra-core/src/controller/usePagination.ts b/packages/ra-core/src/controller/usePagination.ts new file mode 100644 index 00000000000..5808f152e8e --- /dev/null +++ b/packages/ra-core/src/controller/usePagination.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; + +interface PaginationProps { + page: number; + perPage: number; + setPage: (page: number) => void; + setPerPage: (perPage: number) => void; +} + +export default (initialPerPage: number = 25): PaginationProps => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(initialPerPage); + useEffect(() => setPerPage(initialPerPage), [initialPerPage]); + + return { + page, + perPage, + setPage, + setPerPage, + }; +}; diff --git a/packages/ra-core/src/controller/useSort.ts b/packages/ra-core/src/controller/useSort.ts new file mode 100644 index 00000000000..03acbc5499b --- /dev/null +++ b/packages/ra-core/src/controller/useSort.ts @@ -0,0 +1,31 @@ +import { useReducer, useEffect } from 'react'; + +import { + SORT_ASC, + SORT_DESC, +} from '../reducer/admin/resource/list/queryReducer'; +import { Sort } from '../types'; + +const sortReducer = (state: Sort, field: string | Sort): Sort => { + if (typeof field !== 'string') { + return field; + } + const order = + state.field === field && state.order === SORT_ASC + ? SORT_DESC + : SORT_ASC; + return { field, order }; +}; + +export default (initialSort: Sort = { field: 'id', order: 'DESC' }) => { + const [sort, dispatch] = useReducer(sortReducer, initialSort); + useEffect(() => dispatch(initialSort), [ + initialSort.field, + initialSort.order, + ]); + + return { + sortBy: (field: string) => dispatch(field), + sort, + }; +}; From 6ecc5b21ec9ee9254a1eb92cc440cb5c7b7daa32 Mon Sep 17 00:00:00 2001 From: thiery Date: Tue, 21 May 2019 19:00:34 +0200 Subject: [PATCH 6/8] use new Hooks in ReferenceManyFieldCOntroller --- .../field/ReferenceManyFieldController.tsx | 4 +- .../ra-core/src/controller/field/index.ts | 2 + packages/ra-core/src/controller/index.ts | 4 ++ packages/ra-core/src/controller/useSort.ts | 11 +++- .../src/field/ReferenceManyField.js | 63 ++++++++++++++++--- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index f627d7b962a..f6ce242ac25 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -91,7 +91,7 @@ export const ReferenceManyFieldController: FunctionComponent = ({ sort: initialSort, children, }) => { - const { sort, sortBy } = useSort(initialSort); + const { sort, setSort } = useSort(initialSort); const { page, perPage, setPage, setPerPage } = usePagination( initialPerPage ); @@ -124,7 +124,7 @@ export const ReferenceManyFieldController: FunctionComponent = ({ referenceBasePath, setPage, setPerPage, - setSort: sortBy, + setSort, total, }); }; diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts index 9022a0d35e0..50d5c159967 100644 --- a/packages/ra-core/src/controller/field/index.ts +++ b/packages/ra-core/src/controller/field/index.ts @@ -2,10 +2,12 @@ import ReferenceArrayFieldController from './ReferenceArrayFieldController'; import ReferenceFieldController from './ReferenceFieldController'; import ReferenceManyFieldController from './ReferenceManyFieldController'; import useReference from './useReference'; +import useReferenceMany from './useReferenceMany'; export { ReferenceArrayFieldController, ReferenceFieldController, useReference, + useReferenceMany, ReferenceManyFieldController, }; diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts index 424c6954cad..af47d10505f 100644 --- a/packages/ra-core/src/controller/index.ts +++ b/packages/ra-core/src/controller/index.ts @@ -8,6 +8,8 @@ import ListController from './ListController'; import ShowController from './ShowController'; import useRecordSelection from './useRecordSelection'; import useVersion from './useVersion'; +import useSort from './useSort'; +import usePagination from './usePagination'; export { getListControllerProps, sanitizeListRestProps, @@ -17,6 +19,8 @@ export { ShowController, useRecordSelection, useVersion, + useSort, + usePagination, }; export * from './field'; diff --git a/packages/ra-core/src/controller/useSort.ts b/packages/ra-core/src/controller/useSort.ts index 03acbc5499b..e6911e791c6 100644 --- a/packages/ra-core/src/controller/useSort.ts +++ b/packages/ra-core/src/controller/useSort.ts @@ -6,6 +6,11 @@ import { } from '../reducer/admin/resource/list/queryReducer'; import { Sort } from '../types'; +interface SortProps { + setSort: (field: string) => void; + sort: Sort; +} + const sortReducer = (state: Sort, field: string | Sort): Sort => { if (typeof field !== 'string') { return field; @@ -17,7 +22,9 @@ const sortReducer = (state: Sort, field: string | Sort): Sort => { return { field, order }; }; -export default (initialSort: Sort = { field: 'id', order: 'DESC' }) => { +export default ( + initialSort: Sort = { field: 'id', order: 'DESC' } +): SortProps => { const [sort, dispatch] = useReducer(sortReducer, initialSort); useEffect(() => dispatch(initialSort), [ initialSort.field, @@ -25,7 +32,7 @@ export default (initialSort: Sort = { field: 'id', order: 'DESC' }) => { ]); return { - sortBy: (field: string) => dispatch(field), + setSort: (field: string) => dispatch(field), sort, }; }; diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.js b/packages/ra-ui-materialui/src/field/ReferenceManyField.js index b31728376e1..5601378cdf4 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.js @@ -1,6 +1,6 @@ import React, { Fragment, cloneElement, Children } from 'react'; import PropTypes from 'prop-types'; -import { ReferenceManyFieldController, ComponentPropType } from 'ra-core'; +import { useSort, usePagination, useReferenceMany, ComponentPropType } from 'ra-core'; export const ReferenceManyFieldView = ({ children, @@ -106,22 +106,65 @@ ReferenceManyFieldView.propTypes = { * ... * */ -export const ReferenceManyField = ({ children, ...props }) => { +export const ReferenceManyField = ({ + children, + sort: initialSort, + perPage: initialPerPage, + resource, + reference, + record, + target, + filter, + source, + basePath, + ...props +}) => { if (React.Children.count(children) !== 1) { throw new Error( ' only accepts a single child (like )' ); } + const { sort, setSort } = useSort(initialSort); + const { page, perPage, setPage, setPerPage } = usePagination(initialPerPage); + + const { + data, + ids, + loadedOnce, + referenceBasePath, + total, + } = useReferenceMany({ + resource, + reference, + record, + target, + filter, + source, + basePath, + page, + perPage, + sort, + }); return ( - - {controllerProps => ( - - )} - + ); }; From 87002d155f5471732ee957b9f04cdfae0816ec86 Mon Sep 17 00:00:00 2001 From: thiery Date: Thu, 23 May 2019 16:45:18 +0200 Subject: [PATCH 7/8] add comment for the new hooks --- .../src/controller/field/useReferenceMany.ts | 62 +++++++++++++++---- .../ra-core/src/controller/usePagination.ts | 26 ++++++++ packages/ra-core/src/controller/useSort.ts | 28 +++++++++ 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/packages/ra-core/src/controller/field/useReferenceMany.ts b/packages/ra-core/src/controller/field/useReferenceMany.ts index 7744e7bf046..0528a052522 100644 --- a/packages/ra-core/src/controller/field/useReferenceMany.ts +++ b/packages/ra-core/src/controller/field/useReferenceMany.ts @@ -10,9 +10,9 @@ import { getTotal, nameRelatedTo, } from '../../reducer/admin/references/oneToMany'; -import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; +import { Record, Sort, RecordMap, Identifier } from '../../types'; -interface ChildrenFuncParams { +interface ReferenceManyProps { data: RecordMap; ids: Identifier[]; loadedOnce: boolean; @@ -39,6 +39,50 @@ interface Options { const defaultFilter = {}; +/** + * @typedef ReferenceManyProps + * @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 {string | false} referenceBasePath base path of the related record + */ + +/** + * Fetch reference records, and return them when avaliable + * + * The reference prop sould be the name of one of the components + * added as child. + * + * @example + * + * const { isLoading, referenceRecord, resourceLinkPath } = useReferenceMany({ + * resource + * reference: 'users', + * record: { + * userId: 7 + * } + * target: 'comments', + * source: 'userId', + * basePath: '/comments', + * page: 1, + * perPage: 25, + * }); + * + * @param {Object} option + * @param {string} option.resource The current resource name + * @param {string} option.reference The linked resource name + * @param {Object} option.record The current resource record + * @param {string} option.target The target resource key + * @param {Object} option.filter The filter applied on the recorded records list + * @param {string} option.source The key of the linked resource identifier + * @param {string} option.basePath basepath to current resource + * @param {number} option.page the page number + * @param {number} option.perPage the number of item per page + * @param {object} option.sort the sort to apply to the referenced records + * + * @returns {ReferenceManyProps} The reference many props + */ const useReferenceMany = ({ resource, reference, @@ -50,7 +94,7 @@ const useReferenceMany = ({ page, perPage, sort = { field: 'id', order: 'DESC' }, -}: Options): ChildrenFuncParams => { +}: Options): ReferenceManyProps => { const referenceId = get(record, source); const relatedTo = useMemo( () => nameRelatedTo(reference, referenceId, resource, target, filter), @@ -69,7 +113,6 @@ const useReferenceMany = ({ fetchReferences({ reference, referenceId, - resource, target, filter, source, @@ -77,6 +120,7 @@ const useReferenceMany = ({ perPage, sort, dispatch, + relatedTo, }), [ reference, @@ -107,7 +151,6 @@ const useReferenceMany = ({ const fetchReferences = ({ reference, referenceId, - resource, target, filter, source, @@ -115,15 +158,8 @@ const fetchReferences = ({ page, perPage, sort, + relatedTo, }) => () => { - const relatedTo = nameRelatedTo( - reference, - referenceId, - resource, - target, - filter - ); - dispatch( crudGetManyReference( reference, diff --git a/packages/ra-core/src/controller/usePagination.ts b/packages/ra-core/src/controller/usePagination.ts index 5808f152e8e..588c6057baa 100644 --- a/packages/ra-core/src/controller/usePagination.ts +++ b/packages/ra-core/src/controller/usePagination.ts @@ -7,6 +7,32 @@ interface PaginationProps { setPerPage: (perPage: number) => void; } +/** + * set the sort to the given field, swap the order if the field is the same + * @name setNumber + * @function + * @param {number} state the state value + */ + +/** + * @typedef PaginationProps + * @type {Object} + * @property {number} page: The page number. + * @property {number} perPage: The number of item per page. + * @property {setNumber} setPage: Set the page number + * @property {setNumber} setPerPage: Set the per page number + */ + +/** + * Hooks to provide pagination state (apge and perPage) + * + * @example + * + * const { page, setpage, perPage, setPerPage } = usePagination(initialPerPage); + * + * @param {numper} initialPerPage the initial value per page + * @returns {PaginationProps} The pagination props + */ export default (initialPerPage: number = 25): PaginationProps => { const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(initialPerPage); diff --git a/packages/ra-core/src/controller/useSort.ts b/packages/ra-core/src/controller/useSort.ts index e6911e791c6..f3c3708d619 100644 --- a/packages/ra-core/src/controller/useSort.ts +++ b/packages/ra-core/src/controller/useSort.ts @@ -22,6 +22,34 @@ const sortReducer = (state: Sort, field: string | Sort): Sort => { return { field, order }; }; +/** + * set the sort to the given field, swap the order if the field is the same + * @name setSort + * @function + * @param {string} field the name of the field to sort + */ + +/** + * @typedef SortProps + * @type {Object} + * @property {Object} sort: the sort object. + * @property {String} sort.field: the sort object. + * @property {'ASC' | 'DESC'} sort.order: the sort object. + * @property {setSort} setSort + */ + +/** + * Hooks to provide sort state + * + * @example + * + * const { sort, setSort } = useSort({ field: 'name',order: 'ASC' }); + * + * @param {Object} initialSort + * @param {string} initialSort.resource The current resource name + * @param {string} initialSort.reference The linked resource name + * @returns {SortProps} The sort props + */ export default ( initialSort: Sort = { field: 'id', order: 'DESC' } ): SortProps => { From c88778dae7f00b1d242a8dc51c4aecd9603b4040 Mon Sep 17 00:00:00 2001 From: thiery Date: Mon, 27 May 2019 09:43:41 +0200 Subject: [PATCH 8/8] code review --- .../field/ReferenceManyFieldController.tsx | 8 +-- packages/ra-core/src/controller/index.ts | 8 +-- ...usePagination.ts => usePaginationState.ts} | 0 .../{useSort.ts => useSortState.ts} | 14 ++++-- .../src/field/ReferenceManyField.js | 50 +++++++------------ 5 files changed, 36 insertions(+), 44 deletions(-) rename packages/ra-core/src/controller/{usePagination.ts => usePaginationState.ts} (100%) rename packages/ra-core/src/controller/{useSort.ts => useSortState.ts} (82%) diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index f6ce242ac25..e040daa14cb 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -2,8 +2,8 @@ import { ReactElement, FunctionComponent } from 'react'; import { Record, Sort, RecordMap, Identifier } from '../../types'; import useReferenceMany from './useReferenceMany'; -import useSort from '../useSort'; -import usePagination from '../usePagination'; +import useSortState from '../useSortState'; +import usePaginationState from '../usePaginationState'; interface ChildrenFuncParams { currentSort: Sort; @@ -91,8 +91,8 @@ export const ReferenceManyFieldController: FunctionComponent = ({ sort: initialSort, children, }) => { - const { sort, setSort } = useSort(initialSort); - const { page, perPage, setPage, setPerPage } = usePagination( + const { sort, setSort } = useSortState(initialSort); + const { page, perPage, setPage, setPerPage } = usePaginationState( initialPerPage ); const { diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts index af47d10505f..2684a51ba08 100644 --- a/packages/ra-core/src/controller/index.ts +++ b/packages/ra-core/src/controller/index.ts @@ -8,8 +8,8 @@ import ListController from './ListController'; import ShowController from './ShowController'; import useRecordSelection from './useRecordSelection'; import useVersion from './useVersion'; -import useSort from './useSort'; -import usePagination from './usePagination'; +import useSortState from './useSortState'; +import usePaginationState from './usePaginationState'; export { getListControllerProps, sanitizeListRestProps, @@ -19,8 +19,8 @@ export { ShowController, useRecordSelection, useVersion, - useSort, - usePagination, + useSortState, + usePaginationState, }; export * from './field'; diff --git a/packages/ra-core/src/controller/usePagination.ts b/packages/ra-core/src/controller/usePaginationState.ts similarity index 100% rename from packages/ra-core/src/controller/usePagination.ts rename to packages/ra-core/src/controller/usePaginationState.ts diff --git a/packages/ra-core/src/controller/useSort.ts b/packages/ra-core/src/controller/useSortState.ts similarity index 82% rename from packages/ra-core/src/controller/useSort.ts rename to packages/ra-core/src/controller/useSortState.ts index f3c3708d619..54a90e14549 100644 --- a/packages/ra-core/src/controller/useSort.ts +++ b/packages/ra-core/src/controller/useSortState.ts @@ -1,4 +1,4 @@ -import { useReducer, useEffect } from 'react'; +import { useReducer, useEffect, useRef } from 'react'; import { SORT_ASC, @@ -54,10 +54,14 @@ export default ( initialSort: Sort = { field: 'id', order: 'DESC' } ): SortProps => { const [sort, dispatch] = useReducer(sortReducer, initialSort); - useEffect(() => dispatch(initialSort), [ - initialSort.field, - initialSort.order, - ]); + const isFirstRender = useRef(true); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + dispatch(initialSort); + }, [initialSort.field, initialSort.order]); return { setSort: (field: string) => dispatch(field), diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.js b/packages/ra-ui-materialui/src/field/ReferenceManyField.js index 5601378cdf4..24480add69a 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.js +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.js @@ -1,6 +1,6 @@ import React, { Fragment, cloneElement, Children } from 'react'; import PropTypes from 'prop-types'; -import { useSort, usePagination, useReferenceMany, ComponentPropType } from 'ra-core'; +import { useSortState, usePaginationState, useReferenceMany, ComponentPropType } from 'ra-core'; export const ReferenceManyFieldView = ({ children, @@ -106,34 +106,28 @@ ReferenceManyFieldView.propTypes = { * ... * */ -export const ReferenceManyField = ({ - children, - sort: initialSort, - perPage: initialPerPage, - resource, - reference, - record, - target, - filter, - source, - basePath, - ...props -}) => { +export const ReferenceManyField = props => { + const { + children, + sort: initialSort, + perPage: initialPerPage, + resource, + reference, + record, + target, + filter, + source, + basePath, + } = props; if (React.Children.count(children) !== 1) { throw new Error( ' only accepts a single child (like )' ); } - const { sort, setSort } = useSort(initialSort); - const { page, perPage, setPage, setPerPage } = usePagination(initialPerPage); + const { sort, setSort } = useSortState(initialSort); + const { page, perPage, setPage, setPerPage } = usePaginationState(initialPerPage); - const { - data, - ids, - loadedOnce, - referenceBasePath, - total, - } = useReferenceMany({ + const useReferenceManyProps = useReferenceMany({ resource, reference, record, @@ -150,19 +144,13 @@ export const ReferenceManyField = ({ );