From 6b85500bf80337b6ec1ad11771a1eaddf35eceeb Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Mon, 16 Sep 2019 10:30:43 +0200 Subject: [PATCH 1/4] Introduce useGetManyReference hook --- examples/simple/src/posts/PostEdit.js | 1 + .../field/useReferenceManyFieldController.ts | 93 +++--------------- packages/ra-core/src/dataProvider/index.ts | 2 + .../src/dataProvider/useGetManyReference.ts | 95 +++++++++++++++++++ 4 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 packages/ra-core/src/dataProvider/useGetManyReference.ts diff --git a/examples/simple/src/posts/PostEdit.js b/examples/simple/src/posts/PostEdit.js index 0be71db7946..d5825bbb0a0 100644 --- a/examples/simple/src/posts/PostEdit.js +++ b/examples/simple/src/posts/PostEdit.js @@ -155,6 +155,7 @@ const PostEdit = ({ permissions, ...props }) => ( reference="comments" target="post_id" addLabel={false} + fullWidth > diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index a7f321c786c..77cb10bdfcc 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -1,20 +1,15 @@ -import { useEffect, useMemo } from 'react'; -import { useSelector, useDispatch, shallowEqual } from 'react-redux'; +import { useMemo } from 'react'; import get from 'lodash/get'; -import { crudGetManyReference } from '../../actions'; -import { - getIds, - getReferences, - getTotal, - nameRelatedTo, -} from '../../reducer/admin/references/oneToMany'; +import { nameRelatedTo } from '../../reducer/admin/references/oneToMany'; import { Record, Sort, RecordMap, Identifier } from '../../types'; +import { useGetManyReference } from '../../dataProvider'; interface ReferenceManyProps { data: RecordMap; ids: Identifier[]; loaded: boolean; + loading: boolean; referenceBasePath: string; total: number; } @@ -95,42 +90,15 @@ const useReferenceManyFieldController = ({ sort = { field: 'id', order: 'DESC' }, }: Options): ReferenceManyProps => { const referenceId = get(record, source); - const relatedTo = useMemo( - () => nameRelatedTo(reference, referenceId, resource, target, filter), - [filter, reference, referenceId, resource, target] - ); - const ids = useSelector(selectIds(relatedTo), shallowEqual); - const data = useSelector(selectData(reference, relatedTo), shallowEqual); - const total = useSelector(selectTotal(relatedTo)); - - const dispatch = useDispatch(); - - useEffect( - fetchReferences({ - reference, - referenceId, - target, - filter, - source, - page, - perPage, - sort, - dispatch, - relatedTo, - }), - [ - reference, - referenceId, - resource, - target, - filter, - source, - crudGetManyReference, - page, - perPage, - sort.field, - sort.order, - ] + const { data, ids, total, loading, loaded } = useGetManyReference( + resource, + reference, + target, + referenceId, + { page, perPage }, + sort, + filter, + source ); const referenceBasePath = basePath.replace(resource, reference); @@ -138,42 +106,11 @@ const useReferenceManyFieldController = ({ return { data, ids, - loaded: typeof ids !== 'undefined', + loaded, + loading, referenceBasePath, total, }; }; -const fetchReferences = ({ - reference, - referenceId, - target, - filter, - source, - dispatch, - page, - perPage, - sort, - relatedTo, -}) => () => { - 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 useReferenceManyFieldController; diff --git a/packages/ra-core/src/dataProvider/index.ts b/packages/ra-core/src/dataProvider/index.ts index 3a393b146bc..2f2df64dc2f 100644 --- a/packages/ra-core/src/dataProvider/index.ts +++ b/packages/ra-core/src/dataProvider/index.ts @@ -12,6 +12,7 @@ import withDataProvider from './withDataProvider'; import useGetOne from './useGetOne'; import useGetList from './useGetList'; import useGetMany from './useGetMany'; +import useGetManyReference from './useGetManyReference'; import useUpdate from './useUpdate'; import useUpdateMany from './useUpdateMany'; import useCreate from './useCreate'; @@ -31,6 +32,7 @@ export { useGetOne, useGetList, useGetMany, + useGetManyReference, useUpdate, useUpdateMany, useCreate, diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts new file mode 100644 index 00000000000..a791521742a --- /dev/null +++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts @@ -0,0 +1,95 @@ +import { useSelector, shallowEqual } from 'react-redux'; +import { CRUD_GET_MANY_REFERENCE } from '../actions/dataActions/crudGetManyReference'; +import { GET_MANY_REFERENCE } from '../dataFetchActions'; +import { Pagination, Sort, Identifier } from '../types'; +import useQueryWithStore from './useQueryWithStore'; +import { + getReferences, + getIds, + getTotal, + nameRelatedTo, +} from '../reducer/admin/references/oneToMany'; +import { useMemo } from 'react'; + +/** + * Call the dataProvider with a GET_MANY_REFERENCE verb and return the result as well as the loading state. + * + * The return value updates according to the request state: + * + * - start: { loading: true, loaded: false } + * - success: { data: [data from store], ids: [ids from response], total: [total from response], loading: false, loaded: true } + * - error: { error: [error from response], loading: false, loaded: true } + * + * This hook will return the cached result when called a second time + * with the same parameters, until the response arrives. + * + * @param {string} resource The resource name, e.g. 'posts' + * @param {string} reference The referenced resource name, e.g. 'comments' + * @param {string} target The target resource key, e.g. 'post_id' + * @param {Object} id The identifier of the record to look for in 'target' + * @param {Object} pagination The request pagination { page, perPage }, e.g. { page: 1, perPage: 10 } + * @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' } + * @param {Object} filters The request filters, e.g. { body: 'hello, world' } + * @param {Object} options Options object to pass to the dataProvider. May include side effects to be executed upon success of failure, e.g. { onSuccess: { refresh: true } } + * + * @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }. + * + * @example + * + * import { useGetManyReference } from 'react-admin'; + * + * const PostComments = ({ post_id }) => { + * const { data, ids, loading, error } = useGetManyReference( + * 'posts', + * 'comments', + * 'post_id', + * post_id, + * { page: 1, perPage: 10 }, + * { field: 'published_at', order: 'DESC' } + * ); + * if (loading) { return ; } + * if (error) { return

ERROR

; } + * return
    {ids.map(id => + *
  • {data[id].body}
  • + * )}
; + * }; + */ +const useGetManyReference = ( + resource: string, + reference: string, + target: string, + id: Identifier, + pagination: Pagination, + sort: Sort, + filter: object, + source: string, + options?: any +) => { + const relatedTo = useMemo( + () => nameRelatedTo(reference, id, resource, target, filter), + [filter, reference, id, resource, target] + ); + + const { data: ids, total, error, loading, loaded } = useQueryWithStore( + { + type: GET_MANY_REFERENCE, + resource: reference, + payload: { target, id, pagination, sort, filter, source }, + }, + { ...options, relatedTo, action: CRUD_GET_MANY_REFERENCE }, + selectIds(relatedTo), + selectTotal(relatedTo) + ); + const data = useSelector(selectData(reference, relatedTo), shallowEqual); + + return { data, ids, total, error, loading, loaded }; +}; + +export default useGetManyReference; + +const selectData = (reference, relatedTo) => state => + getReferences(state, reference, relatedTo); + +const selectIds = relatedTo => state => getIds(state, relatedTo); + +const selectTotal = relatedTo => state => getTotal(state, relatedTo); From 62b09a981820becad8088f70b0c0c7dbdd224f7c Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Mon, 16 Sep 2019 10:39:57 +0200 Subject: [PATCH 2/4] Cleanup --- .../src/controller/field/useReferenceManyFieldController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 77cb10bdfcc..003c3bea95e 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -1,7 +1,5 @@ -import { useMemo } from 'react'; import get from 'lodash/get'; -import { nameRelatedTo } from '../../reducer/admin/references/oneToMany'; import { Record, Sort, RecordMap, Identifier } from '../../types'; import { useGetManyReference } from '../../dataProvider'; From 429bd433f90a50a3d963c123bb9ab717b693fc2e Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Mon, 16 Sep 2019 11:50:38 +0200 Subject: [PATCH 3/4] Review and tests --- .../ReferenceManyFieldController.spec.tsx | 35 +++---------------- .../field/useReferenceManyFieldController.ts | 13 ++++++- .../src/dataProvider/useGetManyReference.ts | 13 ++++--- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 234820066e4..1ce36cf79cb 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -40,13 +40,6 @@ describe('', () => { 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', }, @@ -169,13 +162,6 @@ describe('', () => { 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', }, @@ -208,20 +194,14 @@ describe('', () => { ); const { rerender, dispatch } = renderWithRedux(); - rerender(); - expect(dispatch).toBeCalledTimes(2); + expect(dispatch).toBeCalledTimes(3); // CRUD_GET_MANY_REFERENCE, CRUD_GET_MANY_REFERENCE_LOADING, FETCH_START + rerender(); + expect(dispatch).toBeCalledTimes(6); 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', }, @@ -237,16 +217,9 @@ describe('', () => { }, ]); - assert.deepEqual(dispatch.mock.calls[1], [ + assert.deepEqual(dispatch.mock.calls[3], [ { meta: { - fetch: 'GET_MANY_REFERENCE', - onFailure: { - notification: { - body: 'ra.notification.http_error', - level: 'warning', - }, - }, relatedTo: 'foo_bar@foo_id_1', resource: 'bar', }, diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 003c3bea95e..25f4f7247c4 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -2,6 +2,7 @@ import get from 'lodash/get'; import { Record, Sort, RecordMap, Identifier } from '../../types'; import { useGetManyReference } from '../../dataProvider'; +import { useNotify } from '../../sideEffect'; interface ReferenceManyProps { data: RecordMap; @@ -88,6 +89,7 @@ const useReferenceManyFieldController = ({ sort = { field: 'id', order: 'DESC' }, }: Options): ReferenceManyProps => { const referenceId = get(record, source); + const notify = useNotify(); const { data, ids, total, loading, loaded } = useGetManyReference( resource, reference, @@ -96,7 +98,16 @@ const useReferenceManyFieldController = ({ { page, perPage }, sort, filter, - source + source, + { + onFailure: error => + notify( + typeof error === 'string' + ? error + : error.message || 'ra.notification.http_error', + 'warning' + ), + } ); const referenceBasePath = basePath.replace(resource, reference); diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts index a791521742a..c6a35aa7186 100644 --- a/packages/ra-core/src/dataProvider/useGetManyReference.ts +++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts @@ -1,7 +1,7 @@ import { useSelector, shallowEqual } from 'react-redux'; import { CRUD_GET_MANY_REFERENCE } from '../actions/dataActions/crudGetManyReference'; import { GET_MANY_REFERENCE } from '../dataFetchActions'; -import { Pagination, Sort, Identifier } from '../types'; +import { Pagination, Sort, Identifier, ReduxState } from '../types'; import useQueryWithStore from './useQueryWithStore'; import { getReferences, @@ -87,9 +87,12 @@ const useGetManyReference = ( export default useGetManyReference; -const selectData = (reference, relatedTo) => state => - getReferences(state, reference, relatedTo); +const selectData = (reference: string, relatedTo: string) => ( + state: ReduxState +) => getReferences(state, reference, relatedTo); -const selectIds = relatedTo => state => getIds(state, relatedTo); +const selectIds = (relatedTo: string) => (state: ReduxState) => + getIds(state, relatedTo); -const selectTotal = relatedTo => state => getTotal(state, relatedTo); +const selectTotal = (relatedTo: string) => (state: ReduxState) => + getTotal(state, relatedTo); From be69d0ef9e3b5c685c376442a182e0af33c87ee1 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Mon, 16 Sep 2019 14:13:33 +0200 Subject: [PATCH 4/4] Review --- .../ReferenceManyFieldController.spec.tsx | 42 ++++++++++++++++--- .../field/useReferenceManyFieldController.ts | 3 +- .../src/dataProvider/useGetManyReference.ts | 20 ++++----- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 1ce36cf79cb..32ffc9081bc 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -3,6 +3,7 @@ import assert from 'assert'; import ReferenceManyFieldController from './ReferenceManyFieldController'; import renderWithRedux from '../../util/renderWithRedux'; +import { waitForDomChange } from '@testing-library/react'; describe('', () => { it('should set loaded to false when related records are not yet fetched', () => { @@ -48,7 +49,6 @@ describe('', () => { id: undefined, pagination: { page: 1, perPage: 25 }, sort: { field: 'id', order: 'DESC' }, - source: 'items', target: 'foo_id', }, type: 'RA/CRUD_GET_MANY_REFERENCE', @@ -144,7 +144,9 @@ describe('', () => { }); it('should support custom source', () => { - const children = jest.fn().mockReturnValue('children'); + const children = jest.fn(({ data }) => + data && data.length > 0 ? data.length : null + ); const { dispatch } = renderWithRedux( ', () => { source="customId" > {children} - + , + { + admin: { + references: { + oneToMany: { + 'posts_comments@post_id_1': { + ids: [1], + total: 1, + }, + }, + }, + resources: { + comments: { + data: { + 1: { + post_id: 1, + id: 1, + body: 'Hello!', + }, + }, + }, + }, + }, + } ); assert.deepEqual(dispatch.mock.calls[0], [ @@ -170,12 +195,19 @@ describe('', () => { id: 1, pagination: { page: 1, perPage: 25 }, sort: { field: 'id', order: 'DESC' }, - source: 'customId', target: 'post_id', }, type: 'RA/CRUD_GET_MANY_REFERENCE', }, ]); + + expect(children.mock.calls[0][0].data).toEqual({ + 1: { + post_id: 1, + id: 1, + body: 'Hello!', + }, + }); }); it('should call crudGetManyReference when its props changes', () => { @@ -210,7 +242,6 @@ describe('', () => { id: 1, pagination: { page: 1, perPage: 25 }, sort: { field: 'id', order: 'DESC' }, - source: 'id', target: 'foo_id', }, type: 'RA/CRUD_GET_MANY_REFERENCE', @@ -228,7 +259,6 @@ describe('', () => { 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/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 25f4f7247c4..da771070f72 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -91,14 +91,13 @@ const useReferenceManyFieldController = ({ const referenceId = get(record, source); const notify = useNotify(); const { data, ids, total, loading, loaded } = useGetManyReference( - resource, reference, target, referenceId, { page, perPage }, sort, filter, - source, + resource, { onFailure: error => notify( diff --git a/packages/ra-core/src/dataProvider/useGetManyReference.ts b/packages/ra-core/src/dataProvider/useGetManyReference.ts index c6a35aa7186..0da57e9a0ca 100644 --- a/packages/ra-core/src/dataProvider/useGetManyReference.ts +++ b/packages/ra-core/src/dataProvider/useGetManyReference.ts @@ -23,13 +23,13 @@ import { useMemo } from 'react'; * This hook will return the cached result when called a second time * with the same parameters, until the response arrives. * - * @param {string} resource The resource name, e.g. 'posts' - * @param {string} reference The referenced resource name, e.g. 'comments' + * @param {string} resource The referenced resource name, e.g. 'comments' * @param {string} target The target resource key, e.g. 'post_id' * @param {Object} id The identifier of the record to look for in 'target' * @param {Object} pagination The request pagination { page, perPage }, e.g. { page: 1, perPage: 10 } * @param {Object} sort The request sort { field, order }, e.g. { field: 'id', order: 'DESC' } * @param {Object} filters The request filters, e.g. { body: 'hello, world' } + * @param {string} referencingResource The resource name, e.g. 'posts'. Used to generate a cache key * @param {Object} options Options object to pass to the dataProvider. May include side effects to be executed upon success of failure, e.g. { onSuccess: { refresh: true } } * * @returns The current request state. Destructure as { data, total, ids, error, loading, loaded }. @@ -40,12 +40,13 @@ import { useMemo } from 'react'; * * const PostComments = ({ post_id }) => { * const { data, ids, loading, error } = useGetManyReference( - * 'posts', * 'comments', * 'post_id', * post_id, * { page: 1, perPage: 10 }, * { field: 'published_at', order: 'DESC' } + * {}, + * 'posts', * ); * if (loading) { return ; } * if (error) { return

ERROR

; } @@ -56,31 +57,30 @@ import { useMemo } from 'react'; */ const useGetManyReference = ( resource: string, - reference: string, target: string, id: Identifier, pagination: Pagination, sort: Sort, filter: object, - source: string, + referencingResource: string, options?: any ) => { const relatedTo = useMemo( - () => nameRelatedTo(reference, id, resource, target, filter), - [filter, reference, id, resource, target] + () => nameRelatedTo(resource, id, referencingResource, target, filter), + [filter, resource, id, referencingResource, target] ); const { data: ids, total, error, loading, loaded } = useQueryWithStore( { type: GET_MANY_REFERENCE, - resource: reference, - payload: { target, id, pagination, sort, filter, source }, + resource: resource, + payload: { target, id, pagination, sort, filter }, }, { ...options, relatedTo, action: CRUD_GET_MANY_REFERENCE }, selectIds(relatedTo), selectTotal(relatedTo) ); - const data = useSelector(selectData(reference, relatedTo), shallowEqual); + const data = useSelector(selectData(resource, relatedTo), shallowEqual); return { data, ids, total, error, loading, loaded }; };