diff --git a/examples/simple/src/customRouteLayout.js b/examples/simple/src/customRouteLayout.js index de10cb7da71..b18666178d9 100644 --- a/examples/simple/src/customRouteLayout.js +++ b/examples/simple/src/customRouteLayout.js @@ -1,12 +1,21 @@ import * as React from 'react'; -import { useGetList, useAuthenticated, Title } from 'react-admin'; +import { + useGetList, + useAuthenticated, + Datagrid, + TextField, + Title, +} from 'react-admin'; + +const currentSort = { field: 'published_at', order: 'DESC' }; const CustomRouteLayout = () => { useAuthenticated(); + const { ids, data, total, loaded } = useGetList( 'posts', { page: 1, perPage: 10 }, - { field: 'published_at', order: 'DESC' } + currentSort ); return loaded ? ( @@ -16,11 +25,18 @@ const CustomRouteLayout = () => {

Found {total} posts !

- + + + + ) : null; }; diff --git a/packages/ra-core/src/controller/details/useCreateContext.tsx b/packages/ra-core/src/controller/details/useCreateContext.tsx index afef3680630..e8299188d8f 100644 --- a/packages/ra-core/src/controller/details/useCreateContext.tsx +++ b/packages/ra-core/src/controller/details/useCreateContext.tsx @@ -1,8 +1,27 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import merge from 'lodash/merge'; + import { Record } from '../../types'; import { CreateContext } from './CreateContext'; import { CreateControllerProps } from './useCreateController'; +/** + * Hook to read the create controller props from the CreateContext. + * + * Mostly used within a (e.g. as a descendent of ). + * + * But you can also use it without a . In this case, it is up to you + * to pass all the necessary props. + * + * The given props will take precedence over context values. + * + * @typedef {Object} CreateControllerProps + * + * @returns {CreateControllerProps} create controller props + * + * @see useCreateController for how it is filled + * + */ export const useCreateContext = < RecordType extends Omit = Omit >( @@ -13,22 +32,59 @@ export const useCreateContext = < // @ts-ignore CreateContext ); - - if (!context.resource) { - /** - * The element isn't inside a - * To avoid breakage in that case, fallback to props - * - * @deprecated - to be removed in 4.0 - */ - if (process.env.NODE_ENV !== 'production') { - console.log( - "Create components must be used inside a . Relying on props rather than context to get Create data and callbacks is deprecated and won't be supported in the next major version of react-admin." - ); - } - - return props; - } - - return context; + // Props take precedence over the context + return useMemo( + () => + merge( + {}, + context, + props != null ? extractCreateContextProps(props) : {} + ), + [context, props] + ); }; + +/** + * Extract only the create controller props + * + * @param {Object} props props passed to the useCreateContext hook + * + * @returns {CreateControllerProps} create controller props + */ +const extractCreateContextProps = ({ + basePath, + record, + defaultTitle, + onFailureRef, + onSuccessRef, + transformRef, + loaded, + loading, + redirect, + setOnFailure, + setOnSuccess, + setTransform, + resource, + save, + saving, + successMessage, + version, +}: any) => ({ + basePath, + record, + defaultTitle, + onFailureRef, + onSuccessRef, + transformRef, + loaded, + loading, + redirect, + setOnFailure, + setOnSuccess, + setTransform, + resource, + save, + saving, + successMessage, + version, +}); diff --git a/packages/ra-core/src/controller/details/useEditContext.tsx b/packages/ra-core/src/controller/details/useEditContext.tsx index 73493c7c440..b811e7927b3 100644 --- a/packages/ra-core/src/controller/details/useEditContext.tsx +++ b/packages/ra-core/src/controller/details/useEditContext.tsx @@ -1,35 +1,91 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import merge from 'lodash/merge'; + import { Record } from '../../types'; import { EditContext } from './EditContext'; import { EditControllerProps } from './useEditController'; +/** + * Hook to read the edit controller props from the CreateContext. + * + * Mostly used within a (e.g. as a descendent of ). + * + * But you can also use it without a . In this case, it is up to you + * to pass all the necessary props. + * + * The given props will take precedence over context values. + * + * @typedef {Object} EditControllerProps + * + * @returns {EditControllerProps} edit controller props + * + * @see useEditController for how it is filled + * + */ export const useEditContext = ( props?: Partial> ): Partial> => { - // Can't find a way to specify the RecordType when CreateContext is declared + // Can't find a way to specify the RecordType when EditContext is declared // @ts-ignore const context = useContext>(EditContext); - if (!context.resource) { - /** - * The element isn't inside a - * To avoid breakage in that case, fallback to props - * - * @deprecated - to be removed in 4.0 - */ - if (process.env.NODE_ENV !== 'production') { - console.log( - "Edit components must be used inside a . Relying on props rather than context to get Edit data and callbacks is deprecated and won't be supported in the next major version of react-admin." - ); - } - // Necessary for actions (EditActions) which expect a data prop containing the record - // @deprecated - to be removed in 4.0d - return { - ...props, - record: props.record || props.data, - data: props.record || props.data, - }; - } - - return context; + // Props take precedence over the context + return useMemo( + () => + merge( + {}, + context, + props != null ? extractEditContextProps(props) : {} + ), + [context, props] + ); }; + +/** + * Extract only the edit controller props + * + * @param {Object} props props passed to the useEditContext hook + * + * @returns {EditControllerProps} edit controller props + */ +const extractEditContextProps = ({ + basePath, + data, + record, + defaultTitle, + onFailureRef, + onSuccessRef, + transformRef, + loaded, + loading, + redirect, + setOnFailure, + setOnSuccess, + setTransform, + resource, + save, + saving, + successMessage, + version, +}: any) => ({ + basePath, + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + data: record || data, + record: record || data, + defaultTitle, + onFailureRef, + onSuccessRef, + transformRef, + loaded, + loading, + redirect, + setOnFailure, + setOnSuccess, + setTransform, + resource, + save, + saving, + successMessage, + version, +}); diff --git a/packages/ra-core/src/controller/details/useShowContext.tsx b/packages/ra-core/src/controller/details/useShowContext.tsx index cbd5badfaef..a74e3e46365 100644 --- a/packages/ra-core/src/controller/details/useShowContext.tsx +++ b/packages/ra-core/src/controller/details/useShowContext.tsx @@ -1,35 +1,71 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import merge from 'lodash/merge'; + import { Record } from '../../types'; import { ShowContext } from './ShowContext'; import { ShowControllerProps } from './useShowController'; +/** + * Hook to read the show controller props from the ShowContext. + * + * Mostly used within a (e.g. as a descendent of ). + * + * But you can also use it without a . In this case, it is up to you + * to pass all the necessary props. + * + * The given props will take precedence over context values. + * + * @typedef {Object} ShowControllerProps + * + * @returns {ShowControllerProps} create controller props + * + * @see useShowController for how it is filled + * + */ export const useShowContext = ( props?: Partial> ): Partial> => { - // Can't find a way to specify the RecordType when CreateContext is declared + // Can't find a way to specify the RecordType when ShowContext is declared // @ts-ignore const context = useContext>(ShowContext); - if (!context.resource) { - /** - * The element isn't inside a - * To avoid breakage in that case, fallback to props - * - * @deprecated - to be removed in 4.0 - */ - if (process.env.NODE_ENV !== 'production') { - console.log( - "Show components must be used inside a . Relying on props rather than context to get Show data and callbacks is deprecated and won't be supported in the next major version of react-admin." - ); - } - // Necessary for actions (EditActions) which expect a data prop containing the record - // @deprecated - to be removed in 4.0d - return { - ...props, - record: props.record || props.data, - data: props.record || props.data, - }; - } - - return context; + // Props take precedence over the context + return useMemo( + () => + merge( + {}, + context, + props != null ? extractShowContextProps(props) : {} + ), + [context, props] + ); }; + +/** + * Extract only the show controller props + * + * @param {Object} props props passed to the useShowContext hook + * + * @returns {ShowControllerProps} show controller props + */ +const extractShowContextProps = ({ + basePath, + record, + data, + defaultTitle, + loaded, + loading, + resource, + version, +}: any) => ({ + basePath, + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + record: record || data, + data: record || data, + defaultTitle, + loaded, + loading, + resource, + version, +}); diff --git a/packages/ra-core/src/controller/useListContext.ts b/packages/ra-core/src/controller/useListContext.ts index 3d059246ff4..4e7b8601ecc 100644 --- a/packages/ra-core/src/controller/useListContext.ts +++ b/packages/ra-core/src/controller/useListContext.ts @@ -1,4 +1,5 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import merge from 'lodash/merge'; import ListContext from './ListContext'; import { ListControllerProps } from './useListController'; @@ -7,9 +8,14 @@ import { Record } from '../types'; /** * Hook to read the list controller props from the ListContext. * - * Must be used within a (e.g. as a descendent of + * Mostly used within a (e.g. as a descendent of * or ). * + * But you can also use it without a . In this case, it is up to you + * to pass all the necessary props (see the list below). + * + * The given props will take precedence over context values. + * * @typedef {Object} ListControllerProps * @prop {Object} data an id-based dictionary of the list data, e.g. { 123: { id: 123, title: 'hello world' }, 456: { ... } } * @prop {Array} ids an array listing the ids of the records in the list, e.g. [123, 456, ...] @@ -91,26 +97,75 @@ const useListContext = ( props?: any ): ListControllerProps => { const context = useContext(ListContext); - if (!context.resource) { - /** - * The element isn't inside a - * - * This may only happen when using Datagrid / SimpleList / SingleFieldList components - * outside of a List / ReferenceManyField / ReferenceArrayField - - * which isn't documented but tolerated. - * To avoid breakage in that case, fallback to props - * - * @deprecated - to be removed in 4.0 - */ - if (process.env.NODE_ENV !== 'production') { - console.log( - "List components must be used inside a . Relying on props rather than context to get List data and callbacks is deprecated and won't be supported in the next major version of react-admin." - ); - } - return props; - } - // @ts-ignore - return context; + // Props take precedence over the context + return useMemo( + () => + merge( + {}, + context, + props != null ? extractListContextProps(props) : {} + ), + [context, props] + ); }; export default useListContext; + +/** + * Extract only the list controller props + * + * @param {Object} props Props passed to the useListContext hook + * + * @returns {ListControllerProps} List controller props + */ +const extractListContextProps = ({ + basePath, + currentSort, + data, + defaultTitle, + displayedFilters, + filterValues, + hasCreate, + hideFilter, + ids, + loaded, + loading, + onSelect, + onToggleItem, + onUnselectItems, + page, + perPage, + resource, + selectedIds, + setFilters, + setPage, + setPerPage, + setSort, + showFilter, + total, +}) => ({ + basePath, + currentSort, + data, + defaultTitle, + displayedFilters, + filterValues, + hasCreate, + hideFilter, + ids, + loaded, + loading, + onSelect, + onToggleItem, + onUnselectItems, + page, + perPage, + resource, + selectedIds, + setFilters, + setPage, + setPerPage, + setSort, + showFilter, + total, +}); diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index be0ac217b5a..05f506a4765 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -62,6 +62,43 @@ import { ClassesOverride } from '../../types'; * * * + * + * + * @example Usage it outside of a or a . + * + * const currentSort = { field: 'published_at', order: 'DESC' }; + * + * export const MyCustomList = (props) => { + * const { ids, data, total, loaded } = useGetList( + * 'posts', + * { page: 1, perPage: 10 }, + * currentSort + * ); + * + * return ( + * { + * console.log('set sort'); + * }} + * onSelect={() => { + * console.log('on select'); + * }} + * onToggleItem={() => { + * console.log('on toggle item'); + * }} + * > + * + * + * + * ); + * } */ const Datagrid: FC = React.forwardRef((props, ref) => { const classes = useDatagridStyles(props);