From 21003a1c555c2ddd1ea903d38ce2b0e8f51eddff Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 25 Mar 2024 18:55:31 +0100 Subject: [PATCH 01/19] [TypeScript] Make types more strict in ra-core, part II --- .../src/controller/create/CreateContext.tsx | 17 ++--- .../controller/create/useCreateController.ts | 25 +++++-- .../src/controller/edit/EditContext.tsx | 20 ++---- .../src/controller/edit/useEditController.ts | 22 ++++-- .../list/InfinitePaginationContext.ts | 12 ++-- .../src/controller/list/ListContext.tsx | 68 ++++++++++++------- .../src/controller/list/ListFilterContext.tsx | 14 ++-- .../controller/list/ListPaginationContext.tsx | 23 ++++--- .../src/controller/list/ListSortContext.tsx | 12 +++- .../ra-core/src/controller/list/useList.ts | 6 +- .../src/controller/list/useListController.ts | 12 ++-- .../src/controller/record/RecordContext.tsx | 6 +- .../src/controller/saveContext/SaveContext.ts | 2 +- .../src/controller/show/ShowContext.tsx | 12 ++-- .../src/controller/show/useShowController.ts | 14 +++- .../ra-core/src/dataProvider/HttpError.ts | 2 +- .../src/form/choices/ChoicesContext.ts | 14 ++-- .../src/form/choices/useChoicesContext.ts | 2 +- .../src/button/ExportButton.spec.tsx | 5 +- 19 files changed, 177 insertions(+), 111 deletions(-) diff --git a/packages/ra-core/src/controller/create/CreateContext.tsx b/packages/ra-core/src/controller/create/CreateContext.tsx index ce670de62aa..b07ea491726 100644 --- a/packages/ra-core/src/controller/create/CreateContext.tsx +++ b/packages/ra-core/src/controller/create/CreateContext.tsx @@ -20,17 +20,12 @@ import { CreateControllerResult } from './useCreateController'; * }; */ export const CreateContext = createContext({ - record: null, - defaultTitle: null, - isFetching: null, - isLoading: null, - isPending: null, - redirect: null, - resource: null, - save: null, - saving: null, - registerMutationMiddleware: null, - unregisterMutationMiddleware: null, + isFetching: false, + isLoading: false, + isPending: false, + redirect: false, + resource: '', + saving: false, }); CreateContext.displayName = 'CreateContext'; diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts index dc093eae6a4..982eb253145 100644 --- a/packages/ra-core/src/controller/create/useCreateController.ts +++ b/packages/ra-core/src/controller/create/useCreateController.ts @@ -63,6 +63,11 @@ export const useCreateController = < useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); + if (!resource) { + throw new Error( + 'useCreateController requires a non-empty resource prop or context' + ); + } const { hasEdit, hasShow } = useResourceDefinition(props); const finalRedirectTo = redirectTo ?? getDefaultRedirectRoute(hasShow, hasEdit); @@ -113,8 +118,12 @@ export const useCreateController = < _: typeof error === 'string' ? error - : error && (error as Error).message - ? (error as Error).message + : error instanceof Error || + (typeof error === 'object' && + error !== null && + error.hasOwnProperty('message')) + ? // @ts-ignore + error.message : undefined, }, } @@ -148,8 +157,14 @@ export const useCreateController = < callTimeOptions ); } catch (error) { - if ((error as HttpError).body?.errors != null) { - return (error as HttpError).body.errors; + if ( + (error instanceof HttpError || + (typeof error === 'object' && + error !== null && + error.hasOwnProperty('body'))) && + error.body?.errors != null + ) { + return error.body.errors; } } }), @@ -201,7 +216,7 @@ export interface CreateControllerResult< // Necessary for actions (EditActions) which expect a data prop containing the record // @deprecated - to be removed in 4.0d data?: RecordType; - defaultTitle: string; + defaultTitle?: string; isFetching: boolean; isPending: boolean; isLoading: boolean; diff --git a/packages/ra-core/src/controller/edit/EditContext.tsx b/packages/ra-core/src/controller/edit/EditContext.tsx index 817f8876a1b..f6c89ac28f4 100644 --- a/packages/ra-core/src/controller/edit/EditContext.tsx +++ b/packages/ra-core/src/controller/edit/EditContext.tsx @@ -20,19 +20,13 @@ import { EditControllerResult } from './useEditController'; * }; */ export const EditContext = createContext({ - record: null, - defaultTitle: null, - isFetching: null, - isLoading: null, - isPending: null, - mutationMode: null, - redirect: null, - refetch: null, - resource: null, - save: null, - saving: null, - registerMutationMiddleware: null, - unregisterMutationMiddleware: null, + isFetching: false, + isLoading: false, + isPending: false, + redirect: false, + refetch: () => Promise.reject('not implemented'), + resource: '', + saving: false, }); EditContext.displayName = 'EditContext'; diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts index 66183c941cb..83baf1d7dcf 100644 --- a/packages/ra-core/src/controller/edit/useEditController.ts +++ b/packages/ra-core/src/controller/edit/useEditController.ts @@ -65,13 +65,23 @@ export const useEditController = < } = props; useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); + if (!resource) { + throw new Error( + 'useEditController requires a non-empty resource prop or context' + ); + } const getRecordRepresentation = useGetRecordRepresentation(resource); const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); const refresh = useRefresh(); const { id: routeId } = useParams<'id'>(); - const id = propsId != null ? propsId : decodeURIComponent(routeId); + if (!routeId && !propsId) { + throw new Error( + 'useEditController requires an id prop or a route with an /:id? parameter.' + ); + } + const id = propsId ?? decodeURIComponent(routeId!); const { meta: queryMeta, ...otherQueryOptions } = queryOptions; const { meta: mutationMeta, @@ -160,8 +170,12 @@ export const useEditController = < _: typeof error === 'string' ? error - : error && (error as Error).message - ? (error as Error).message + : error instanceof Error || + (typeof error === 'object' && + error !== null && + error.hasOwnProperty('message')) + ? // @ts-ignore + error.message : undefined, }, } @@ -265,7 +279,7 @@ export interface EditControllerResult // @deprecated - to be removed in 4.0d data?: RecordType; error?: any; - defaultTitle: string; + defaultTitle?: string; isFetching: boolean; isLoading: boolean; isPending: boolean; diff --git a/packages/ra-core/src/controller/list/InfinitePaginationContext.ts b/packages/ra-core/src/controller/list/InfinitePaginationContext.ts index 4b8e1f945e4..e26d83e4d00 100644 --- a/packages/ra-core/src/controller/list/InfinitePaginationContext.ts +++ b/packages/ra-core/src/controller/list/InfinitePaginationContext.ts @@ -26,12 +26,12 @@ import { InfiniteListControllerResult } from './useInfiniteListController'; export const InfinitePaginationContext = createContext< InfinitePaginationContextValue >({ - hasNextPage: null, - fetchNextPage: null, - isFetchingNextPage: null, - hasPreviousPage: null, - fetchPreviousPage: null, - isFetchingPreviousPage: null, + hasNextPage: false, + fetchNextPage: () => Promise.reject('not implemented'), + isFetchingNextPage: false, + hasPreviousPage: false, + fetchPreviousPage: () => Promise.reject('not implemented'), + isFetchingPreviousPage: false, }); InfinitePaginationContext.displayName = 'InfinitePaginationContext'; diff --git a/packages/ra-core/src/controller/list/ListContext.tsx b/packages/ra-core/src/controller/list/ListContext.tsx index e187ede90c7..b2235d288e4 100644 --- a/packages/ra-core/src/controller/list/ListContext.tsx +++ b/packages/ra-core/src/controller/list/ListContext.tsx @@ -1,5 +1,7 @@ import { createContext } from 'react'; import { ListControllerResult } from './useListController'; +import { th } from 'date-fns/locale'; +import { SORT_ASC } from './queryReducer'; /** * Context to store the result of the useListController() hook. @@ -53,32 +55,50 @@ import { ListControllerResult } from './useListController'; * }; */ export const ListContext = createContext({ - sort: null, - data: null, - defaultTitle: null, + sort: { + field: 'id', + order: SORT_ASC, + }, displayedFilters: null, - exporter: null, filterValues: null, - hasNextPage: null, - hasPreviousPage: null, - hideFilter: null, - isFetching: null, - isLoading: null, - isPending: null, - onSelect: null, - onToggleItem: null, - onUnselectItems: null, - page: null, - perPage: null, - refetch: null, - resource: null, - selectedIds: undefined, - setFilters: null, - setPage: null, - setPerPage: null, - setSort: null, - showFilter: null, - total: null, + hasNextPage: false, + hasPreviousPage: false, + hideFilter: () => { + throw new Error('not implemented'); + }, + isFetching: false, + isLoading: false, + isPending: false, + onSelect: () => { + throw new Error('not implemented'); + }, + onToggleItem: () => { + throw new Error('not implemented'); + }, + onUnselectItems: () => { + throw new Error('not implemented'); + }, + page: 1, + perPage: 25, + refetch: () => Promise.reject('not implemented'), + resource: '', + selectedIds: [], + setFilters: () => { + throw new Error('not implemented'); + }, + setPage: () => { + throw new Error('not implemented'); + }, + setPerPage: () => { + throw new Error('not implemented'); + }, + setSort: () => { + throw new Error('not implemented'); + }, + showFilter: () => { + throw new Error('not implemented'); + }, + total: undefined, }); ListContext.displayName = 'ListContext'; diff --git a/packages/ra-core/src/controller/list/ListFilterContext.tsx b/packages/ra-core/src/controller/list/ListFilterContext.tsx index a2d8c0e1c54..fb59667a793 100644 --- a/packages/ra-core/src/controller/list/ListFilterContext.tsx +++ b/packages/ra-core/src/controller/list/ListFilterContext.tsx @@ -40,10 +40,16 @@ import { ListControllerResult } from './useListController'; export const ListFilterContext = createContext({ displayedFilters: null, filterValues: null, - hideFilter: null, - setFilters: null, - showFilter: null, - resource: null, + hideFilter: () => { + throw new Error('not implemented'); + }, + setFilters: () => { + throw new Error('not implemented'); + }, + showFilter: () => { + throw new Error('not implemented'); + }, + resource: '', }); export type ListFilterContextValue = Pick< diff --git a/packages/ra-core/src/controller/list/ListPaginationContext.tsx b/packages/ra-core/src/controller/list/ListPaginationContext.tsx index df142bbde49..9823341aaac 100644 --- a/packages/ra-core/src/controller/list/ListPaginationContext.tsx +++ b/packages/ra-core/src/controller/list/ListPaginationContext.tsx @@ -42,16 +42,19 @@ import { ListControllerResult } from './useListController'; * }; */ export const ListPaginationContext = createContext({ - isLoading: null, - isPending: null, - page: null, - perPage: null, - setPage: null, - setPerPage: null, - hasPreviousPage: null, - hasNextPage: null, - total: undefined, - resource: null, + isLoading: false, + isPending: false, + page: 1, + perPage: 25, + setPage: () => { + throw new Error('not implemented'); + }, + setPerPage: () => { + throw new Error('not implemented'); + }, + hasPreviousPage: false, + hasNextPage: false, + resource: '', }); ListPaginationContext.displayName = 'ListPaginationContext'; diff --git a/packages/ra-core/src/controller/list/ListSortContext.tsx b/packages/ra-core/src/controller/list/ListSortContext.tsx index 04668304ffe..ddf3df0df53 100644 --- a/packages/ra-core/src/controller/list/ListSortContext.tsx +++ b/packages/ra-core/src/controller/list/ListSortContext.tsx @@ -1,6 +1,7 @@ import { createContext, useMemo } from 'react'; import pick from 'lodash/pick'; import { ListControllerResult } from './useListController'; +import { SORT_ASC } from './queryReducer'; /** * Context to store the sort part of the useListController() result. @@ -35,9 +36,14 @@ import { ListControllerResult } from './useListController'; * }; */ export const ListSortContext = createContext({ - sort: null, - setSort: null, - resource: null, + sort: { + field: 'id', + order: SORT_ASC, + }, + setSort: () => { + throw new Error('not implemented'); + }, + resource: '', }); export type ListSortContextValue = Pick< diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts index 4e64ff493b3..c61a7e6d3b6 100644 --- a/packages/ra-core/src/controller/list/useList.ts +++ b/packages/ra-core/src/controller/list/useList.ts @@ -105,7 +105,9 @@ export const useList = ( ); // selection logic - const [selectedIds, selectionModifiers] = useRecordSelection(resource); + const [selectedIds, selectionModifiers] = useRecordSelection( + resource || '' // FIXME use false as default value when https://github.com/marmelab/react-admin/pull/9742 is merged + ); // filter logic const filterRef = useRef(filter); @@ -275,7 +277,7 @@ export const useList = ( onUnselectItems: selectionModifiers.clearSelection, page, perPage, - resource: undefined, + resource: '', refetch, selectedIds, setFilters, diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 3b8efd6f4a2..95d3d529e6f 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -53,12 +53,12 @@ export const useListController = ( if (!resource) { throw new Error( - ` was called outside of a ResourceContext and without a resource prop. You must set the resource prop.` + `useListController requires a non-empty resource prop or context` ); } if (filter && isValidElement(filter)) { throw new Error( - ' received a React element as `filter` props. If you intended to set the list filter elements, use the `filters` (with an s) prop instead. The `filter` prop is internal and should not be set by the developer.' + 'useListController received a React element as `filter` props. If you intended to set the list filter elements, use the `filters` (with an s) prop instead. The `filter` prop is internal and should not be set by the developer.' ); } @@ -396,7 +396,7 @@ const defaultSort = { export interface ListControllerResult { sort: SortPayload; - data: RecordType[]; + data?: RecordType[]; defaultTitle?: string; displayedFilters: any; error?: any; @@ -424,9 +424,9 @@ export interface ListControllerResult { setPerPage: (page: number) => void; setSort: (sort: SortPayload) => void; showFilter: (filterName: string, defaultValue: any) => void; - total: number; - hasNextPage: boolean; - hasPreviousPage: boolean; + total?: number; + hasNextPage?: boolean; + hasPreviousPage?: boolean; } export const injectedProps = [ diff --git a/packages/ra-core/src/controller/record/RecordContext.tsx b/packages/ra-core/src/controller/record/RecordContext.tsx index b923d7b473a..d713ce04f3f 100644 --- a/packages/ra-core/src/controller/record/RecordContext.tsx +++ b/packages/ra-core/src/controller/record/RecordContext.tsx @@ -8,9 +8,9 @@ import { RaRecord } from '../../types'; * @see RecordContextProvider * @see useRecordContext */ -export const RecordContext = createContext>( - undefined -); +export const RecordContext = createContext< + RaRecord | Omit | undefined +>(undefined); RecordContext.displayName = 'RecordContext'; diff --git a/packages/ra-core/src/controller/saveContext/SaveContext.ts b/packages/ra-core/src/controller/saveContext/SaveContext.ts index 8c90e6c0795..222fcea137d 100644 --- a/packages/ra-core/src/controller/saveContext/SaveContext.ts +++ b/packages/ra-core/src/controller/saveContext/SaveContext.ts @@ -33,4 +33,4 @@ export type SaveHandlerCallbacks = { transform?: TransformData; meta?: any; }; -export const SaveContext = createContext(undefined); +export const SaveContext = createContext({}); diff --git a/packages/ra-core/src/controller/show/ShowContext.tsx b/packages/ra-core/src/controller/show/ShowContext.tsx index ba13ee1ff46..2c242ccd73f 100644 --- a/packages/ra-core/src/controller/show/ShowContext.tsx +++ b/packages/ra-core/src/controller/show/ShowContext.tsx @@ -20,13 +20,11 @@ import { ShowControllerResult } from './useShowController'; * }; */ export const ShowContext = createContext({ - record: null, - defaultTitle: null, - isFetching: null, - isLoading: null, - isPending: null, - refetch: null, - resource: null, + isFetching: false, + isLoading: false, + isPending: false, + refetch: () => Promise.reject('not implemented'), + resource: '', }); ShowContext.displayName = 'ShowContext'; diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts index ee454eeb11f..c36339602b8 100644 --- a/packages/ra-core/src/controller/show/useShowController.ts +++ b/packages/ra-core/src/controller/show/useShowController.ts @@ -55,13 +55,23 @@ export const useShowController = ( const { disableAuthentication, id: propsId, queryOptions = {} } = props; useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); + if (!resource) { + throw new Error( + `useShowController requires a non-empty resource prop or context` + ); + } const getRecordRepresentation = useGetRecordRepresentation(resource); const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); const refresh = useRefresh(); const { id: routeId } = useParams<'id'>(); - const id = propsId != null ? propsId : decodeURIComponent(routeId); + if (!routeId && !propsId) { + throw new Error( + 'useShowController requires an id prop or a route with an /:id? parameter.' + ); + } + const id = propsId != null ? propsId : decodeURIComponent(routeId!); const { meta, ...otherQueryOptions } = queryOptions; const { @@ -126,7 +136,7 @@ export interface ShowControllerProps { } export interface ShowControllerResult { - defaultTitle: string; + defaultTitle?: string; // Necessary for actions (EditActions) which expect a data prop containing the record // @deprecated - to be removed in 4.0d data?: RecordType; diff --git a/packages/ra-core/src/dataProvider/HttpError.ts b/packages/ra-core/src/dataProvider/HttpError.ts index fa01d3c79b8..a1c81c5cecf 100644 --- a/packages/ra-core/src/dataProvider/HttpError.ts +++ b/packages/ra-core/src/dataProvider/HttpError.ts @@ -2,7 +2,7 @@ class HttpError extends Error { constructor( public readonly message, public readonly status, - public readonly body = null + public readonly body: any = null ) { super(message); Object.setPrototypeOf(this, HttpError.prototype); diff --git a/packages/ra-core/src/form/choices/ChoicesContext.ts b/packages/ra-core/src/form/choices/ChoicesContext.ts index 9ccf88e5063..18957c1013c 100644 --- a/packages/ra-core/src/form/choices/ChoicesContext.ts +++ b/packages/ra-core/src/form/choices/ChoicesContext.ts @@ -12,14 +12,14 @@ export const ChoicesContext = createContext( ); export type ChoicesContextValue = { - allChoices: RecordType[]; - availableChoices: RecordType[]; + allChoices?: RecordType[]; + availableChoices?: RecordType[]; displayedFilters: any; error?: any; filter?: FilterPayload; filterValues: any; - hasNextPage: boolean; - hasPreviousPage: boolean; + hasNextPage?: boolean; + hasPreviousPage?: boolean; hideFilter: (filterName: string) => void; isFetching: boolean; isLoading: boolean; @@ -28,7 +28,7 @@ export type ChoicesContextValue = { perPage: number; refetch: (() => void) | UseGetListHookValue['refetch']; resource: string; - selectedChoices: RecordType[]; + selectedChoices?: RecordType[]; setFilters: ( filters: any, displayedFilters?: any, @@ -39,7 +39,7 @@ export type ChoicesContextValue = { setSort: (sort: SortPayload) => void; showFilter: (filterName: string, defaultValue: any) => void; sort: SortPayload; - source: string; - total: number; + source?: string; + total?: number; isFromReference: boolean; }; diff --git a/packages/ra-core/src/form/choices/useChoicesContext.ts b/packages/ra-core/src/form/choices/useChoicesContext.ts index 3151b2eb061..517fef7250d 100644 --- a/packages/ra-core/src/form/choices/useChoicesContext.ts +++ b/packages/ra-core/src/form/choices/useChoicesContext.ts @@ -5,7 +5,7 @@ import { ChoicesContext, ChoicesContextValue } from './ChoicesContext'; export const useChoicesContext = ( options: Partial & { choices?: ChoicesType[] } = {} -): ChoicesContextValue => { +): ChoicesContextValue => { const context = useContext(ChoicesContext) as ChoicesContextValue< ChoicesType >; diff --git a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx index cbb2a1ad296..011dd94616c 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx @@ -32,7 +32,10 @@ describe('', () => { await waitFor(() => { expect(dataProvider.getList).toHaveBeenCalledWith('test', { - sort: null, + sort: { + field: 'id', + order: 'ASC', + }, filter: { filters: 'override' }, pagination: { page: 1, perPage: 1000 }, meta: { pass: 'meta' }, From e3749af930fa7d4d6acc7db900374f1e26ba4389 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 25 Mar 2024 19:28:15 +0100 Subject: [PATCH 02/19] Make useListController return type smarter --- examples/demo/src/categories/CategoryList.tsx | 5 +- examples/demo/src/orders/MobileGrid.tsx | 4 +- .../demo/src/reviews/ReviewListMobile.tsx | 4 +- examples/demo/src/visitors/MobileGrid.tsx | 4 +- .../field/useReferenceManyFieldController.ts | 1 + .../src/controller/list/ListContext.tsx | 4 +- .../controller/list/ListPaginationContext.tsx | 1 + .../list/useInfiniteListController.ts | 8 +- .../ra-core/src/controller/list/useList.ts | 1 + .../src/controller/list/useListController.ts | 106 ++++++++++++------ 10 files changed, 92 insertions(+), 46 deletions(-) diff --git a/examples/demo/src/categories/CategoryList.tsx b/examples/demo/src/categories/CategoryList.tsx index b03e1b1208c..549d2db56eb 100644 --- a/examples/demo/src/categories/CategoryList.tsx +++ b/examples/demo/src/categories/CategoryList.tsx @@ -31,10 +31,13 @@ const CategoryList = () => ( ); const CategoryGrid = () => { - const { data, isPending } = useListContext(); + const { data, error, isPending } = useListContext(); if (isPending) { return null; } + if (error) { + return null; + } return ( {data.map(record => ( diff --git a/examples/demo/src/orders/MobileGrid.tsx b/examples/demo/src/orders/MobileGrid.tsx index a09128f5618..a75588f77bc 100644 --- a/examples/demo/src/orders/MobileGrid.tsx +++ b/examples/demo/src/orders/MobileGrid.tsx @@ -16,9 +16,9 @@ import CustomerReferenceField from '../visitors/CustomerReferenceField'; import { Order } from '../types'; const MobileGrid = () => { - const { data, isPending } = useListContext(); + const { data, error, isPending } = useListContext(); const translate = useTranslate(); - if (isPending || data.length === 0) { + if (isPending || (data && data.length === 0) || error) { return null; } return ( diff --git a/examples/demo/src/reviews/ReviewListMobile.tsx b/examples/demo/src/reviews/ReviewListMobile.tsx index badc8069f62..771018eceb6 100644 --- a/examples/demo/src/reviews/ReviewListMobile.tsx +++ b/examples/demo/src/reviews/ReviewListMobile.tsx @@ -6,8 +6,8 @@ import { ReviewItem } from './ReviewItem'; import { Review } from './../types'; const ReviewListMobile = () => { - const { data, isPending, total } = useListContext(); - if (isPending || Number(total) === 0) { + const { data, error, isPending, total } = useListContext(); + if (isPending || error || Number(total) === 0) { return null; } return ( diff --git a/examples/demo/src/visitors/MobileGrid.tsx b/examples/demo/src/visitors/MobileGrid.tsx index 31088ee81ca..b60ad10bba4 100644 --- a/examples/demo/src/visitors/MobileGrid.tsx +++ b/examples/demo/src/visitors/MobileGrid.tsx @@ -17,9 +17,9 @@ import { Customer } from '../types'; const MobileGrid = () => { const translate = useTranslate(); - const { data, isPending } = useListContext(); + const { data, error, isPending } = useListContext(); - if (isPending || data.length === 0) { + if (isPending || error || (data && data.length === 0)) { return null; } diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 07f24f7fa14..eb2e5a46971 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -203,6 +203,7 @@ export const useReferenceManyFieldController = < } ); + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort, data, diff --git a/packages/ra-core/src/controller/list/ListContext.tsx b/packages/ra-core/src/controller/list/ListContext.tsx index b2235d288e4..f97638f5f89 100644 --- a/packages/ra-core/src/controller/list/ListContext.tsx +++ b/packages/ra-core/src/controller/list/ListContext.tsx @@ -55,6 +55,9 @@ import { SORT_ASC } from './queryReducer'; * }; */ export const ListContext = createContext({ + data: [], + total: 0, + error: null, sort: { field: 'id', order: SORT_ASC, @@ -98,7 +101,6 @@ export const ListContext = createContext({ showFilter: () => { throw new Error('not implemented'); }, - total: undefined, }); ListContext.displayName = 'ListContext'; diff --git a/packages/ra-core/src/controller/list/ListPaginationContext.tsx b/packages/ra-core/src/controller/list/ListPaginationContext.tsx index 9823341aaac..ed615d17003 100644 --- a/packages/ra-core/src/controller/list/ListPaginationContext.tsx +++ b/packages/ra-core/src/controller/list/ListPaginationContext.tsx @@ -44,6 +44,7 @@ import { ListControllerResult } from './useListController'; export const ListPaginationContext = createContext({ isLoading: false, isPending: false, + total: 0, page: 1, perPage: 25, setPage: () => { diff --git a/packages/ra-core/src/controller/list/useInfiniteListController.ts b/packages/ra-core/src/controller/list/useInfiniteListController.ts index 57dbb2ae416..705ac6e20b2 100644 --- a/packages/ra-core/src/controller/list/useInfiniteListController.ts +++ b/packages/ra-core/src/controller/list/useInfiniteListController.ts @@ -167,6 +167,7 @@ export const useInfiniteListController = ( [data] ); + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort: currentSort, data: unwrappedData, @@ -222,8 +223,9 @@ export interface InfiniteListControllerProps< storeKey?: string | false; } -export interface InfiniteListControllerResult - extends ListControllerResult { +export type InfiniteListControllerResult< + RecordType extends RaRecord = any +> = ListControllerResult & { fetchNextPage: InfiniteQueryObserverBaseResult< InfiniteData> >['fetchNextPage']; @@ -236,4 +238,4 @@ export interface InfiniteListControllerResult isFetchingPreviousPage: InfiniteQueryObserverBaseResult< InfiniteData> >['isFetchingPreviousPage']; -} +}; diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts index c61a7e6d3b6..50ac63d947e 100644 --- a/packages/ra-core/src/controller/list/useList.ts +++ b/packages/ra-core/src/controller/list/useList.ts @@ -256,6 +256,7 @@ export const useList = ( } }, [isPending, pendingState, setPendingState]); + // @ts-ignore FIXME cannot find another way to fox this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort, data: finalItems?.data, diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 95d3d529e6f..a0eb1e8c63e 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -147,6 +147,7 @@ export const useListController = ( name: getResourceLabel(resource, 2), }); + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort: currentSort, data, @@ -394,41 +395,6 @@ const defaultSort = { order: SORT_ASC, } as const; -export interface ListControllerResult { - sort: SortPayload; - data?: RecordType[]; - defaultTitle?: string; - displayedFilters: any; - error?: any; - exporter?: Exporter | false; - filter?: FilterPayload; - filterValues: any; - hideFilter: (filterName: string) => void; - isFetching: boolean; - isLoading: boolean; - isPending: boolean; - onSelect: (ids: RecordType['id'][]) => void; - onToggleItem: (id: RecordType['id']) => void; - onUnselectItems: () => void; - page: number; - perPage: number; - refetch: (() => void) | UseGetListHookValue['refetch']; - resource: string; - selectedIds: RecordType['id'][]; - setFilters: ( - filters: any, - displayedFilters?: any, - debounce?: boolean - ) => void; - setPage: (page: number) => void; - setPerPage: (page: number) => void; - setSort: (sort: SortPayload) => void; - showFilter: (filterName: string, defaultValue: any) => void; - total?: number; - hasNextPage?: boolean; - hasPreviousPage?: boolean; -} - export const injectedProps = [ 'sort', 'data', @@ -478,3 +444,73 @@ export const sanitizeListRestProps = props => Object.keys(props) .filter(propName => !injectedProps.includes(propName)) .reduce((acc, key) => ({ ...acc, [key]: props[key] }), {}); + +interface ListControllerBaseResult { + sort: SortPayload; + defaultTitle?: string; + displayedFilters: any; + exporter?: Exporter | false; + filter?: FilterPayload; + filterValues: any; + hideFilter: (filterName: string) => void; + onSelect: (ids: RecordType['id'][]) => void; + onToggleItem: (id: RecordType['id']) => void; + onUnselectItems: () => void; + page: number; + perPage: number; + refetch: (() => void) | UseGetListHookValue['refetch']; + resource: string; + selectedIds: RecordType['id'][]; + setFilters: ( + filters: any, + displayedFilters?: any, + debounce?: boolean + ) => void; + setPage: (page: number) => void; + setPerPage: (page: number) => void; + setSort: (sort: SortPayload) => void; + showFilter: (filterName: string, defaultValue: any) => void; + hasNextPage?: boolean; + hasPreviousPage?: boolean; + isFetching?: boolean; + isLoading?: boolean; +} + +interface ListControllerLoadingResult + extends ListControllerBaseResult { + data: undefined; + total: undefined; + error: null; + isPending: true; +} +interface ListControllerLoadingErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends ListControllerBaseResult { + data: undefined; + total: undefined; + error: TError; + isPending: false; +} +interface ListControllerRefetchErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends ListControllerBaseResult { + data: RecordType[]; + total: number; + error: TError; + isPending: false; +} +interface ListControllerSuccessResult + extends ListControllerBaseResult { + data: RecordType[]; + total: number; + error: null; + isPending: false; +} + +export type ListControllerResult = + | ListControllerLoadingResult + | ListControllerLoadingErrorResult + | ListControllerRefetchErrorResult + | ListControllerSuccessResult; From 6d730c65e08ea0908b095763eac077d0a7abff91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Mon, 25 Mar 2024 23:43:01 +0100 Subject: [PATCH 03/19] Fix other controller return types --- .../src/controller/edit/EditContext.tsx | 2 + .../src/controller/edit/useEditController.ts | 46 +++++++++++++++---- .../src/controller/list/useListController.ts | 2 +- .../src/controller/show/ShowContext.tsx | 2 + .../src/controller/show/useShowController.ts | 43 ++++++++++++++--- .../src/field/ReferenceManyField.tsx | 15 +++--- .../src/input/DatagridInput.tsx | 3 ++ 7 files changed, 90 insertions(+), 23 deletions(-) diff --git a/packages/ra-core/src/controller/edit/EditContext.tsx b/packages/ra-core/src/controller/edit/EditContext.tsx index f6c89ac28f4..0a80b43507d 100644 --- a/packages/ra-core/src/controller/edit/EditContext.tsx +++ b/packages/ra-core/src/controller/edit/EditContext.tsx @@ -20,9 +20,11 @@ import { EditControllerResult } from './useEditController'; * }; */ export const EditContext = createContext({ + record: null, isFetching: false, isLoading: false, isPending: false, + error: null, redirect: false, refetch: () => Promise.reject('not implemented'), resource: '', diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts index 83baf1d7dcf..4c88d23dc2e 100644 --- a/packages/ra-core/src/controller/edit/useEditController.ts +++ b/packages/ra-core/src/controller/edit/useEditController.ts @@ -240,6 +240,7 @@ export const useEditController = < ] ); + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { defaultTitle, error, @@ -258,6 +259,8 @@ export const useEditController = < }; }; +const DefaultRedirect = 'list'; + export interface EditControllerProps< RecordType extends RaRecord = any, ErrorType = Error @@ -273,20 +276,47 @@ export interface EditControllerProps< [key: string]: any; } -export interface EditControllerResult +export interface EditControllerBaseResult extends SaveContextValue { - // Necessary for actions (EditActions) which expect a data prop containing the record - // @deprecated - to be removed in 4.0d - data?: RecordType; - error?: any; defaultTitle?: string; isFetching: boolean; isLoading: boolean; - isPending: boolean; - record?: RecordType; refetch: UseGetOneHookValue['refetch']; redirect: RedirectionSideEffect; resource: string; } -const DefaultRedirect = 'list'; +interface EditControllerLoadingResult + extends EditControllerBaseResult { + record: undefined; + error: null; + isPending: true; +} +interface EditControllerLoadingErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends EditControllerBaseResult { + record: undefined; + error: TError; + isPending: false; +} +interface EditControllerRefetchErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends EditControllerBaseResult { + record: RecordType; + error: TError; + isPending: false; +} +interface EditControllerSuccessResult + extends EditControllerBaseResult { + record: RecordType; + error: null; + isPending: false; +} + +export type EditControllerResult = + | EditControllerLoadingResult + | EditControllerLoadingErrorResult + | EditControllerRefetchErrorResult + | EditControllerSuccessResult; diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index a0eb1e8c63e..10399d87911 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -147,7 +147,7 @@ export const useListController = ( name: getResourceLabel(resource, 2), }); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { sort: currentSort, data, diff --git a/packages/ra-core/src/controller/show/ShowContext.tsx b/packages/ra-core/src/controller/show/ShowContext.tsx index 2c242ccd73f..f9b4ba1886d 100644 --- a/packages/ra-core/src/controller/show/ShowContext.tsx +++ b/packages/ra-core/src/controller/show/ShowContext.tsx @@ -20,6 +20,8 @@ import { ShowControllerResult } from './useShowController'; * }; */ export const ShowContext = createContext({ + record: null, + error: null, isFetching: false, isLoading: false, isPending: false, diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts index c36339602b8..240e9eb9fac 100644 --- a/packages/ra-core/src/controller/show/useShowController.ts +++ b/packages/ra-core/src/controller/show/useShowController.ts @@ -116,6 +116,7 @@ export const useShowController = ( : '', }); + // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { defaultTitle, error, @@ -135,16 +136,46 @@ export interface ShowControllerProps { resource?: string; } -export interface ShowControllerResult { +export interface ShowControllerBaseResult { defaultTitle?: string; - // Necessary for actions (EditActions) which expect a data prop containing the record - // @deprecated - to be removed in 4.0d - data?: RecordType; - error?: any; isFetching: boolean; isLoading: boolean; - isPending: boolean; resource: string; record?: RecordType; refetch: UseGetOneHookValue['refetch']; } + +interface ShowControllerLoadingResult + extends ShowControllerBaseResult { + record: undefined; + error: null; + isPending: true; +} +interface ShowControllerLoadingErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends ShowControllerBaseResult { + record: undefined; + error: TError; + isPending: false; +} +interface ShowControllerRefetchErrorResult< + RecordType extends RaRecord = any, + TError = Error +> extends ShowControllerBaseResult { + record: RecordType; + error: TError; + isPending: false; +} +interface ShowControllerSuccessResult + extends ShowControllerBaseResult { + record: RecordType; + error: null; + isPending: false; +} + +export type ShowControllerResult = + | ShowControllerLoadingResult + | ShowControllerLoadingErrorResult + | ShowControllerRefetchErrorResult + | ShowControllerSuccessResult; diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx index 9f648401c60..e0fa2abacd3 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx @@ -156,14 +156,13 @@ export const ReferenceManyFieldView: FC = props => ); }; -export interface ReferenceManyFieldViewProps - extends Omit< - ReferenceManyFieldProps, - 'resource' | 'page' | 'perPage' | 'sort' - >, - ListControllerResult { - children: ReactElement; -} +export type ReferenceManyFieldViewProps = Omit< + ReferenceManyFieldProps, + 'resource' | 'page' | 'perPage' | 'sort' +> & + ListControllerResult & { + children: ReactElement; + }; ReferenceManyFieldView.propTypes = { children: PropTypes.element, diff --git a/packages/ra-ui-materialui/src/input/DatagridInput.tsx b/packages/ra-ui-materialui/src/input/DatagridInput.tsx index a1232431017..d5616fb3520 100644 --- a/packages/ra-ui-materialui/src/input/DatagridInput.tsx +++ b/packages/ra-ui-materialui/src/input/DatagridInput.tsx @@ -104,6 +104,8 @@ export const DatagridInput = (props: DatagridInputProps) => { () => ({ ...choicesContext, data: availableChoices, + total: availableChoices.length, + error: null, onSelect, onToggleItem, onUnselectItems, @@ -120,6 +122,7 @@ export const DatagridInput = (props: DatagridInputProps) => { ); return (
+ {/* @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." */} {filters ? ( Array.isArray(filters) ? ( From 9d6df0c5857a9b7a71a5cf2667943034f6bf59d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Mon, 25 Mar 2024 23:47:28 +0100 Subject: [PATCH 04/19] Fix CRM build --- examples/crm/src/companies/CompanyShow.tsx | 8 ++++---- examples/crm/src/companies/GridList.tsx | 4 ++-- examples/crm/src/contacts/ContactAside.tsx | 4 ++-- examples/crm/src/contacts/ContactList.tsx | 4 ++++ examples/crm/src/deals/ContactList.tsx | 4 ++-- examples/crm/src/deals/DealCreate.tsx | 4 ++++ examples/crm/src/notes/NotesIterator.tsx | 4 ++-- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/examples/crm/src/companies/CompanyShow.tsx b/examples/crm/src/companies/CompanyShow.tsx index daf3b9cbdb3..28955ae5b73 100644 --- a/examples/crm/src/companies/CompanyShow.tsx +++ b/examples/crm/src/companies/CompanyShow.tsx @@ -146,8 +146,8 @@ const TabPanel = (props: TabPanelProps) => { }; const ContactsIterator = () => { - const { data: contacts, isPending } = useListContext(); - if (isPending) return null; + const { data: contacts, error, isPending } = useListContext(); + if (isPending || error) return null; const now = Date.now(); return ( @@ -214,8 +214,8 @@ const CreateRelatedContactButton = () => { }; const DealsIterator = () => { - const { data: deals, isPending } = useListContext(); - if (isPending) return null; + const { data: deals, error, isPending } = useListContext(); + if (isPending || error) return null; const now = Date.now(); return ( diff --git a/examples/crm/src/companies/GridList.tsx b/examples/crm/src/companies/GridList.tsx index 9e0e7b8a3da..7bc64d164fd 100644 --- a/examples/crm/src/companies/GridList.tsx +++ b/examples/crm/src/companies/GridList.tsx @@ -26,9 +26,9 @@ const LoadingGridList = () => ( ); const LoadedGridList = () => { - const { data, isPending } = useListContext(); + const { data, error, isPending } = useListContext(); - if (isPending) return null; + if (isPending || error) return null; return ( diff --git a/examples/crm/src/contacts/ContactAside.tsx b/examples/crm/src/contacts/ContactAside.tsx index ceb4e3c1c45..d2cc957fa27 100644 --- a/examples/crm/src/contacts/ContactAside.tsx +++ b/examples/crm/src/contacts/ContactAside.tsx @@ -111,8 +111,8 @@ export const ContactAside = ({ link = 'edit' }: { link?: 'edit' | 'show' }) => { }; const TasksIterator = () => { - const { data, isLoading } = useListContext(); - if (isLoading || data.length === 0) return null; + const { data, error, isPending } = useListContext(); + if (isPending || error || data.length === 0) return null; return ( Tasks diff --git a/examples/crm/src/contacts/ContactList.tsx b/examples/crm/src/contacts/ContactList.tsx index a4df3ab36e7..34298c1c7d5 100644 --- a/examples/crm/src/contacts/ContactList.tsx +++ b/examples/crm/src/contacts/ContactList.tsx @@ -38,6 +38,7 @@ import { Contact } from '../types'; const ContactListContent = () => { const { data: contacts, + error, isPending, onToggleItem, selectedIds, @@ -45,6 +46,9 @@ const ContactListContent = () => { if (isPending) { return ; } + if (error) { + return null; + } const now = Date.now(); return ( diff --git a/examples/crm/src/deals/ContactList.tsx b/examples/crm/src/deals/ContactList.tsx index 9a5e68676be..54a59a36409 100644 --- a/examples/crm/src/deals/ContactList.tsx +++ b/examples/crm/src/deals/ContactList.tsx @@ -4,9 +4,9 @@ import { Box, Link } from '@mui/material'; import { Link as RouterLink } from 'react-router-dom'; export const ContactList = () => { - const { data, isPending } = useListContext(); + const { data, error, isPending } = useListContext(); - if (isPending) return
; + if (isPending || error) return
; return ( { const queryClient = useQueryClient(); const onSuccess = async (deal: Deal) => { + if (!allDeals) { + redirect('/deals'); + return; + } // increase the index of all deals in the same stage as the new deal // first, get the list of deals in the same stage const deals = allDeals.filter( diff --git a/examples/crm/src/notes/NotesIterator.tsx b/examples/crm/src/notes/NotesIterator.tsx index c0c4bef7ab2..e4ee6f7895b 100644 --- a/examples/crm/src/notes/NotesIterator.tsx +++ b/examples/crm/src/notes/NotesIterator.tsx @@ -12,8 +12,8 @@ export const NotesIterator = ({ showStatus?: boolean; reference: 'contacts' | 'deals'; }) => { - const { data, isPending } = useListContext(); - if (isPending) return null; + const { data, error, isPending } = useListContext(); + if (isPending || error) return null; return ( <> From 362af34b3936791d93fe4e1ced947186e3569a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 26 Mar 2024 08:44:56 +0100 Subject: [PATCH 05/19] Remove fixmes --- packages/ra-core/src/controller/edit/useEditController.ts | 3 +-- .../src/controller/field/useReferenceManyFieldController.ts | 3 +-- .../ra-core/src/controller/list/useInfiniteListController.ts | 3 +-- packages/ra-core/src/controller/list/useList.ts | 3 +-- packages/ra-core/src/controller/list/useListController.ts | 3 +-- packages/ra-core/src/controller/show/useShowController.ts | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts index 4c88d23dc2e..ec12090fdba 100644 --- a/packages/ra-core/src/controller/edit/useEditController.ts +++ b/packages/ra-core/src/controller/edit/useEditController.ts @@ -240,7 +240,6 @@ export const useEditController = < ] ); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { defaultTitle, error, @@ -256,7 +255,7 @@ export const useEditController = < save, saving, unregisterMutationMiddleware, - }; + } as EditControllerResult; }; const DefaultRedirect = 'list'; diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index eb2e5a46971..990b28df655 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -203,7 +203,6 @@ export const useReferenceManyFieldController = < } ); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort, data, @@ -235,5 +234,5 @@ export const useReferenceManyFieldController = < setSort, showFilter, total, - }; + } as ListControllerResult; }; diff --git a/packages/ra-core/src/controller/list/useInfiniteListController.ts b/packages/ra-core/src/controller/list/useInfiniteListController.ts index 705ac6e20b2..b34576148f0 100644 --- a/packages/ra-core/src/controller/list/useInfiniteListController.ts +++ b/packages/ra-core/src/controller/list/useInfiniteListController.ts @@ -167,7 +167,6 @@ export const useInfiniteListController = ( [data] ); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort: currentSort, data: unwrappedData, @@ -201,7 +200,7 @@ export const useInfiniteListController = ( isFetchingNextPage, fetchPreviousPage, isFetchingPreviousPage, - }; + } as InfiniteListControllerResult; }; export interface InfiniteListControllerProps< diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts index 50ac63d947e..ead7ae74a67 100644 --- a/packages/ra-core/src/controller/list/useList.ts +++ b/packages/ra-core/src/controller/list/useList.ts @@ -256,7 +256,6 @@ export const useList = ( } }, [isPending, pendingState, setPendingState]); - // @ts-ignore FIXME cannot find another way to fox this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'."" return { sort, data: finalItems?.data, @@ -287,7 +286,7 @@ export const useList = ( setSort, showFilter, total: finalItems?.total, - }; + } as UseListValue; }; export interface UseListOptions { diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 10399d87911..f0318867371 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -147,7 +147,6 @@ export const useListController = ( name: getResourceLabel(resource, 2), }); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { sort: currentSort, data, @@ -181,7 +180,7 @@ export const useListController = ( ? query.page * query.perPage < total : undefined, hasPreviousPage: pageInfo ? pageInfo.hasPreviousPage : query.page > 1, - }; + } as ListControllerResult; }; export interface ListControllerProps { diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts index 240e9eb9fac..2313c8aeece 100644 --- a/packages/ra-core/src/controller/show/useShowController.ts +++ b/packages/ra-core/src/controller/show/useShowController.ts @@ -116,7 +116,6 @@ export const useShowController = ( : '', }); - // @ts-ignore FIXME cannot find another way to fix this error: "Types of property 'isPending' are incompatible: Type 'boolean' is not assignable to type 'false'." return { defaultTitle, error, @@ -126,7 +125,7 @@ export const useShowController = ( record, refetch, resource, - }; + } as ShowControllerResult; }; export interface ShowControllerProps { From 85ce0f776c5c5d87861855c4b103c69e9bf23b26 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 26 Mar 2024 19:04:34 +0100 Subject: [PATCH 06/19] Do not provide empty value for controller context --- .../src/controller/create/CreateContext.tsx | 9 +--- .../src/controller/edit/EditContext.tsx | 12 +---- .../src/controller/list/ListContext.tsx | 51 +----------------- .../src/controller/list/ListFilterContext.tsx | 17 ++---- .../controller/list/ListPaginationContext.tsx | 19 ++----- .../src/controller/list/ListSortContext.tsx | 14 ++--- .../controller/list/useListFilterContext.ts | 52 +++++++++++-------- .../src/controller/list/useListSortContext.ts | 37 ++++++------- .../src/controller/show/ShowContext.tsx | 10 +--- .../src/button/ExportButton.spec.tsx | 4 -- .../src/input/DatagridInput.tsx | 2 +- 11 files changed, 60 insertions(+), 167 deletions(-) diff --git a/packages/ra-core/src/controller/create/CreateContext.tsx b/packages/ra-core/src/controller/create/CreateContext.tsx index b07ea491726..d2d660b3e9d 100644 --- a/packages/ra-core/src/controller/create/CreateContext.tsx +++ b/packages/ra-core/src/controller/create/CreateContext.tsx @@ -19,13 +19,6 @@ import { CreateControllerResult } from './useCreateController'; * ); * }; */ -export const CreateContext = createContext({ - isFetching: false, - isLoading: false, - isPending: false, - redirect: false, - resource: '', - saving: false, -}); +export const CreateContext = createContext(null); CreateContext.displayName = 'CreateContext'; diff --git a/packages/ra-core/src/controller/edit/EditContext.tsx b/packages/ra-core/src/controller/edit/EditContext.tsx index 0a80b43507d..11b04cbf102 100644 --- a/packages/ra-core/src/controller/edit/EditContext.tsx +++ b/packages/ra-core/src/controller/edit/EditContext.tsx @@ -19,16 +19,6 @@ import { EditControllerResult } from './useEditController'; * ); * }; */ -export const EditContext = createContext({ - record: null, - isFetching: false, - isLoading: false, - isPending: false, - error: null, - redirect: false, - refetch: () => Promise.reject('not implemented'), - resource: '', - saving: false, -}); +export const EditContext = createContext(null); EditContext.displayName = 'EditContext'; diff --git a/packages/ra-core/src/controller/list/ListContext.tsx b/packages/ra-core/src/controller/list/ListContext.tsx index f97638f5f89..f4af11d4495 100644 --- a/packages/ra-core/src/controller/list/ListContext.tsx +++ b/packages/ra-core/src/controller/list/ListContext.tsx @@ -1,7 +1,5 @@ import { createContext } from 'react'; import { ListControllerResult } from './useListController'; -import { th } from 'date-fns/locale'; -import { SORT_ASC } from './queryReducer'; /** * Context to store the result of the useListController() hook. @@ -54,53 +52,6 @@ import { SORT_ASC } from './queryReducer'; * ); * }; */ -export const ListContext = createContext({ - data: [], - total: 0, - error: null, - sort: { - field: 'id', - order: SORT_ASC, - }, - displayedFilters: null, - filterValues: null, - hasNextPage: false, - hasPreviousPage: false, - hideFilter: () => { - throw new Error('not implemented'); - }, - isFetching: false, - isLoading: false, - isPending: false, - onSelect: () => { - throw new Error('not implemented'); - }, - onToggleItem: () => { - throw new Error('not implemented'); - }, - onUnselectItems: () => { - throw new Error('not implemented'); - }, - page: 1, - perPage: 25, - refetch: () => Promise.reject('not implemented'), - resource: '', - selectedIds: [], - setFilters: () => { - throw new Error('not implemented'); - }, - setPage: () => { - throw new Error('not implemented'); - }, - setPerPage: () => { - throw new Error('not implemented'); - }, - setSort: () => { - throw new Error('not implemented'); - }, - showFilter: () => { - throw new Error('not implemented'); - }, -}); +export const ListContext = createContext(null); ListContext.displayName = 'ListContext'; diff --git a/packages/ra-core/src/controller/list/ListFilterContext.tsx b/packages/ra-core/src/controller/list/ListFilterContext.tsx index fb59667a793..e096d947f9c 100644 --- a/packages/ra-core/src/controller/list/ListFilterContext.tsx +++ b/packages/ra-core/src/controller/list/ListFilterContext.tsx @@ -37,20 +37,9 @@ import { ListControllerResult } from './useListController'; * ); * }; */ -export const ListFilterContext = createContext({ - displayedFilters: null, - filterValues: null, - hideFilter: () => { - throw new Error('not implemented'); - }, - setFilters: () => { - throw new Error('not implemented'); - }, - showFilter: () => { - throw new Error('not implemented'); - }, - resource: '', -}); +export const ListFilterContext = createContext< + ListFilterContextValue | undefined +>(undefined); export type ListFilterContextValue = Pick< ListControllerResult, diff --git a/packages/ra-core/src/controller/list/ListPaginationContext.tsx b/packages/ra-core/src/controller/list/ListPaginationContext.tsx index ed615d17003..5793bc7386b 100644 --- a/packages/ra-core/src/controller/list/ListPaginationContext.tsx +++ b/packages/ra-core/src/controller/list/ListPaginationContext.tsx @@ -41,22 +41,9 @@ import { ListControllerResult } from './useListController'; * ); * }; */ -export const ListPaginationContext = createContext({ - isLoading: false, - isPending: false, - total: 0, - page: 1, - perPage: 25, - setPage: () => { - throw new Error('not implemented'); - }, - setPerPage: () => { - throw new Error('not implemented'); - }, - hasPreviousPage: false, - hasNextPage: false, - resource: '', -}); +export const ListPaginationContext = createContext< + ListPaginationContextValue | undefined +>(undefined); ListPaginationContext.displayName = 'ListPaginationContext'; diff --git a/packages/ra-core/src/controller/list/ListSortContext.tsx b/packages/ra-core/src/controller/list/ListSortContext.tsx index ddf3df0df53..3d66b883e06 100644 --- a/packages/ra-core/src/controller/list/ListSortContext.tsx +++ b/packages/ra-core/src/controller/list/ListSortContext.tsx @@ -1,7 +1,6 @@ import { createContext, useMemo } from 'react'; import pick from 'lodash/pick'; import { ListControllerResult } from './useListController'; -import { SORT_ASC } from './queryReducer'; /** * Context to store the sort part of the useListController() result. @@ -35,16 +34,9 @@ import { SORT_ASC } from './queryReducer'; * ); * }; */ -export const ListSortContext = createContext({ - sort: { - field: 'id', - order: SORT_ASC, - }, - setSort: () => { - throw new Error('not implemented'); - }, - resource: '', -}); +export const ListSortContext = createContext( + undefined +); export type ListSortContextValue = Pick< ListControllerResult, diff --git a/packages/ra-core/src/controller/list/useListFilterContext.ts b/packages/ra-core/src/controller/list/useListFilterContext.ts index 770c91f834a..11fc18ff428 100644 --- a/packages/ra-core/src/controller/list/useListFilterContext.ts +++ b/packages/ra-core/src/controller/list/useListFilterContext.ts @@ -1,12 +1,12 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import defaults from 'lodash/defaults'; import { ListFilterContext, ListFilterContextValue } from './ListFilterContext'; /** - * Hook to read the list controller props from the ListContext. + * Hook to read the list props from the ListFilterContext. * - * Must be used within a (e.g. as a descendent of - * or ). + * Must be used within a . * * @typedef {Object} ListFilterContextValue * @prop {Object} filterValues a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' } @@ -22,23 +22,29 @@ import { ListFilterContext, ListFilterContextValue } from './ListFilterContext'; */ export const useListFilterContext = (props?: any): ListFilterContextValue => { const context = useContext(ListFilterContext); - if (!context.hideFilter) { - /** - * 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; - } - return context; + return useMemo( + () => + defaults( + {}, + props != null ? extractListPaginationContextProps(props) : {}, + context + ), + [context, props] + ); }; + +const extractListPaginationContextProps = ({ + displayedFilters, + filterValues, + hideFilter, + setFilters, + showFilter, + resource, +}) => ({ + displayedFilters, + filterValues, + hideFilter, + setFilters, + showFilter, + resource, +}); diff --git a/packages/ra-core/src/controller/list/useListSortContext.ts b/packages/ra-core/src/controller/list/useListSortContext.ts index 9884f36076e..7bd80b6f548 100644 --- a/packages/ra-core/src/controller/list/useListSortContext.ts +++ b/packages/ra-core/src/controller/list/useListSortContext.ts @@ -1,4 +1,5 @@ -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import defaults from 'lodash/defaults'; import { ListSortContext, ListSortContextValue } from './ListSortContext'; @@ -19,23 +20,19 @@ import { ListSortContext, ListSortContextValue } from './ListSortContext'; */ export const useListSortContext = (props?: any): ListSortContextValue => { const context = useContext(ListSortContext); - if (!context.setSort) { - /** - * 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; - } - return context; + return useMemo( + () => + defaults( + {}, + props != null ? extractListPaginationContextProps(props) : {}, + context + ), + [context, props] + ); }; + +const extractListPaginationContextProps = ({ sort, setSort, resource }) => ({ + sort, + setSort, + resource, +}); diff --git a/packages/ra-core/src/controller/show/ShowContext.tsx b/packages/ra-core/src/controller/show/ShowContext.tsx index f9b4ba1886d..993919785c2 100644 --- a/packages/ra-core/src/controller/show/ShowContext.tsx +++ b/packages/ra-core/src/controller/show/ShowContext.tsx @@ -19,14 +19,6 @@ import { ShowControllerResult } from './useShowController'; * ); * }; */ -export const ShowContext = createContext({ - record: null, - error: null, - isFetching: false, - isLoading: false, - isPending: false, - refetch: () => Promise.reject('not implemented'), - resource: '', -}); +export const ShowContext = createContext(null); ShowContext.displayName = 'ShowContext'; diff --git a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx index 011dd94616c..c5935b3f674 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx @@ -32,10 +32,6 @@ describe('', () => { await waitFor(() => { expect(dataProvider.getList).toHaveBeenCalledWith('test', { - sort: { - field: 'id', - order: 'ASC', - }, filter: { filters: 'override' }, pagination: { page: 1, perPage: 1000 }, meta: { pass: 'meta' }, diff --git a/packages/ra-ui-materialui/src/input/DatagridInput.tsx b/packages/ra-ui-materialui/src/input/DatagridInput.tsx index d5616fb3520..6e11fe6842e 100644 --- a/packages/ra-ui-materialui/src/input/DatagridInput.tsx +++ b/packages/ra-ui-materialui/src/input/DatagridInput.tsx @@ -104,7 +104,7 @@ export const DatagridInput = (props: DatagridInputProps) => { () => ({ ...choicesContext, data: availableChoices, - total: availableChoices.length, + total: availableChoices?.length, error: null, onSelect, onToggleItem, From c83ebb76e3ac12401b5c16d922196390b212ab35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 26 Mar 2024 20:08:00 +0100 Subject: [PATCH 07/19] Controller context returns partial controller value --- .../controller/create/useCreateContext.tsx | 18 ++++++++---------- .../src/controller/edit/useEditContext.tsx | 13 ++++++------- .../src/controller/list/WithListContext.tsx | 2 +- .../src/controller/list/useListContext.ts | 9 ++++----- .../src/controller/show/useShowContext.tsx | 19 +++++++------------ 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/ra-core/src/controller/create/useCreateContext.tsx b/packages/ra-core/src/controller/create/useCreateContext.tsx index df1bc91f7df..ecd3b2027f4 100644 --- a/packages/ra-core/src/controller/create/useCreateContext.tsx +++ b/packages/ra-core/src/controller/create/useCreateContext.tsx @@ -23,19 +23,17 @@ import { CreateControllerResult } from './useCreateController'; * */ export const useCreateContext = ( - props?: Partial> -): CreateControllerResult => { - const context = useContext>( - // Can't find a way to specify the RecordType when CreateContext is declared - // @ts-ignore - CreateContext - ); + props?: any +): Partial> => { + const context = useContext(CreateContext); // Props take precedence over the context return useMemo( () => defaults( {}, - props != null ? extractCreateContextProps(props) : {}, + props != null + ? extractCreateContextProps(props) + : {}, context ), [context, props] @@ -49,7 +47,7 @@ export const useCreateContext = ( * * @returns {CreateControllerResult} create controller props */ -const extractCreateContextProps = ({ +const extractCreateContextProps = ({ record, defaultTitle, isFetching, @@ -59,7 +57,7 @@ const extractCreateContextProps = ({ resource, save, saving, -}: any) => ({ +}: Partial>) => ({ record, defaultTitle, isFetching, diff --git a/packages/ra-core/src/controller/edit/useEditContext.tsx b/packages/ra-core/src/controller/edit/useEditContext.tsx index c41b0997b0c..ddd9baf3f04 100644 --- a/packages/ra-core/src/controller/edit/useEditContext.tsx +++ b/packages/ra-core/src/controller/edit/useEditContext.tsx @@ -23,18 +23,17 @@ import { EditControllerResult } from './useEditController'; * */ export const useEditContext = ( - props?: Partial> -): EditControllerResult => { + props?: any +): Partial> => { // Can't find a way to specify the RecordType when EditContext is declared - // @ts-ignore - const context = useContext>(EditContext); + const context = useContext(EditContext); // Props take precedence over the context return useMemo( () => defaults( {}, - props != null ? extractEditContextProps(props) : {}, + props != null ? extractEditContextProps(props) : {}, context ), [context, props] @@ -48,7 +47,7 @@ export const useEditContext = ( * * @returns {EditControllerResult} edit controller props */ -const extractEditContextProps = ({ +const extractEditContextProps = ({ data, record, defaultTitle, @@ -60,7 +59,7 @@ const extractEditContextProps = ({ resource, save, saving, -}: any) => ({ +}: Partial> & Record) => ({ // Necessary for actions (EditActions) which expect a data prop containing the record // @deprecated - to be removed in 4.0d data: record || data, diff --git a/packages/ra-core/src/controller/list/WithListContext.tsx b/packages/ra-core/src/controller/list/WithListContext.tsx index a4c51c05bc4..11ea0d57bb3 100644 --- a/packages/ra-core/src/controller/list/WithListContext.tsx +++ b/packages/ra-core/src/controller/list/WithListContext.tsx @@ -26,7 +26,7 @@ export const WithListContext = ({ export interface WithListContextProps { render: ( - context: ListControllerResult + context: Partial> ) => ReactElement | false | null; label?: string; } diff --git a/packages/ra-core/src/controller/list/useListContext.ts b/packages/ra-core/src/controller/list/useListContext.ts index 189825a998f..4cc53617d94 100644 --- a/packages/ra-core/src/controller/list/useListContext.ts +++ b/packages/ra-core/src/controller/list/useListContext.ts @@ -92,15 +92,14 @@ import { RaRecord } from '../../types'; */ export const useListContext = ( props?: any -): ListControllerResult => { +): Partial> => { const context = useContext(ListContext); // Props take precedence over the context - // @ts-ignore return useMemo( () => defaults( {}, - props != null ? extractListContextProps(props) : {}, + props != null ? extractListContextProps(props) : {}, context ), [context, props] @@ -114,7 +113,7 @@ export const useListContext = ( * * @returns {ListControllerResult} List controller props */ -const extractListContextProps = ({ +const extractListContextProps = ({ sort, data, defaultTitle, @@ -140,7 +139,7 @@ const extractListContextProps = ({ setSort, showFilter, total, -}) => ({ +}: Partial> & Record) => ({ sort, data, defaultTitle, diff --git a/packages/ra-core/src/controller/show/useShowContext.tsx b/packages/ra-core/src/controller/show/useShowContext.tsx index a836ac15372..0d470d756c8 100644 --- a/packages/ra-core/src/controller/show/useShowContext.tsx +++ b/packages/ra-core/src/controller/show/useShowContext.tsx @@ -23,18 +23,17 @@ import { ShowControllerResult } from './useShowController'; * */ export const useShowContext = ( - props?: Partial> -): ShowControllerResult => { + props?: any +): Partial> => { // Can't find a way to specify the RecordType when ShowContext is declared - // @ts-ignore - const context = useContext>(ShowContext); + const context = useContext(ShowContext); // Props take precedence over the context return useMemo( () => defaults( {}, - props != null ? extractShowContextProps(props) : {}, + props != null ? extractShowContextProps(props) : {}, context ), [context, props] @@ -48,19 +47,15 @@ export const useShowContext = ( * * @returns {ShowControllerResult} show controller props */ -const extractShowContextProps = ({ +const extractShowContextProps = ({ record, - data, defaultTitle, isFetching, isLoading, isPending, resource, -}: any) => ({ - // 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, +}: Partial>) => ({ + record, defaultTitle, isFetching, isLoading, From 83e69f2e3619225cd4d3324c612d22a1cec3641c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 26 Mar 2024 20:43:58 +0100 Subject: [PATCH 08/19] Revert partial context --- packages/ra-core/src/controller/create/useCreateContext.tsx | 2 +- packages/ra-core/src/controller/edit/useEditContext.tsx | 4 +--- packages/ra-core/src/controller/list/useListContext.ts | 2 +- packages/ra-core/src/controller/show/useShowContext.tsx | 4 +--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/ra-core/src/controller/create/useCreateContext.tsx b/packages/ra-core/src/controller/create/useCreateContext.tsx index ecd3b2027f4..dab149b9d9a 100644 --- a/packages/ra-core/src/controller/create/useCreateContext.tsx +++ b/packages/ra-core/src/controller/create/useCreateContext.tsx @@ -24,7 +24,7 @@ import { CreateControllerResult } from './useCreateController'; */ export const useCreateContext = ( props?: any -): Partial> => { +): CreateControllerResult => { const context = useContext(CreateContext); // Props take precedence over the context return useMemo( diff --git a/packages/ra-core/src/controller/edit/useEditContext.tsx b/packages/ra-core/src/controller/edit/useEditContext.tsx index ddd9baf3f04..3b10f24a241 100644 --- a/packages/ra-core/src/controller/edit/useEditContext.tsx +++ b/packages/ra-core/src/controller/edit/useEditContext.tsx @@ -24,10 +24,8 @@ import { EditControllerResult } from './useEditController'; */ export const useEditContext = ( props?: any -): Partial> => { - // Can't find a way to specify the RecordType when EditContext is declared +): EditControllerResult => { const context = useContext(EditContext); - // Props take precedence over the context return useMemo( () => diff --git a/packages/ra-core/src/controller/list/useListContext.ts b/packages/ra-core/src/controller/list/useListContext.ts index 4cc53617d94..c4b82ef94f4 100644 --- a/packages/ra-core/src/controller/list/useListContext.ts +++ b/packages/ra-core/src/controller/list/useListContext.ts @@ -92,7 +92,7 @@ import { RaRecord } from '../../types'; */ export const useListContext = ( props?: any -): Partial> => { +): ListControllerResult => { const context = useContext(ListContext); // Props take precedence over the context return useMemo( diff --git a/packages/ra-core/src/controller/show/useShowContext.tsx b/packages/ra-core/src/controller/show/useShowContext.tsx index 0d470d756c8..de61460dd52 100644 --- a/packages/ra-core/src/controller/show/useShowContext.tsx +++ b/packages/ra-core/src/controller/show/useShowContext.tsx @@ -24,10 +24,8 @@ import { ShowControllerResult } from './useShowController'; */ export const useShowContext = ( props?: any -): Partial> => { - // Can't find a way to specify the RecordType when ShowContext is declared +): ShowControllerResult => { const context = useContext(ShowContext); - // Props take precedence over the context return useMemo( () => From 3b8578360a38f910773c34eb0e22cca8635f5bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 26 Mar 2024 20:47:01 +0100 Subject: [PATCH 09/19] Export partial interfaces --- .../ra-core/src/controller/edit/useEditController.ts | 8 ++++---- .../ra-core/src/controller/list/useListController.ts | 10 +++++----- .../ra-core/src/controller/show/useShowController.ts | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts index ec12090fdba..ee1aa7ce78a 100644 --- a/packages/ra-core/src/controller/edit/useEditController.ts +++ b/packages/ra-core/src/controller/edit/useEditController.ts @@ -285,13 +285,13 @@ export interface EditControllerBaseResult resource: string; } -interface EditControllerLoadingResult +export interface EditControllerLoadingResult extends EditControllerBaseResult { record: undefined; error: null; isPending: true; } -interface EditControllerLoadingErrorResult< +export interface EditControllerLoadingErrorResult< RecordType extends RaRecord = any, TError = Error > extends EditControllerBaseResult { @@ -299,7 +299,7 @@ interface EditControllerLoadingErrorResult< error: TError; isPending: false; } -interface EditControllerRefetchErrorResult< +export interface EditControllerRefetchErrorResult< RecordType extends RaRecord = any, TError = Error > extends EditControllerBaseResult { @@ -307,7 +307,7 @@ interface EditControllerRefetchErrorResult< error: TError; isPending: false; } -interface EditControllerSuccessResult +export interface EditControllerSuccessResult extends EditControllerBaseResult { record: RecordType; error: null; diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index f0318867371..831190ad869 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -444,7 +444,7 @@ export const sanitizeListRestProps = props => .filter(propName => !injectedProps.includes(propName)) .reduce((acc, key) => ({ ...acc, [key]: props[key] }), {}); -interface ListControllerBaseResult { +export interface ListControllerBaseResult { sort: SortPayload; defaultTitle?: string; displayedFilters: any; @@ -475,14 +475,14 @@ interface ListControllerBaseResult { isLoading?: boolean; } -interface ListControllerLoadingResult +export interface ListControllerLoadingResult extends ListControllerBaseResult { data: undefined; total: undefined; error: null; isPending: true; } -interface ListControllerLoadingErrorResult< +export interface ListControllerLoadingErrorResult< RecordType extends RaRecord = any, TError = Error > extends ListControllerBaseResult { @@ -491,7 +491,7 @@ interface ListControllerLoadingErrorResult< error: TError; isPending: false; } -interface ListControllerRefetchErrorResult< +export interface ListControllerRefetchErrorResult< RecordType extends RaRecord = any, TError = Error > extends ListControllerBaseResult { @@ -500,7 +500,7 @@ interface ListControllerRefetchErrorResult< error: TError; isPending: false; } -interface ListControllerSuccessResult +export interface ListControllerSuccessResult extends ListControllerBaseResult { data: RecordType[]; total: number; diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts index 2313c8aeece..533170dae63 100644 --- a/packages/ra-core/src/controller/show/useShowController.ts +++ b/packages/ra-core/src/controller/show/useShowController.ts @@ -144,13 +144,13 @@ export interface ShowControllerBaseResult { refetch: UseGetOneHookValue['refetch']; } -interface ShowControllerLoadingResult +export interface ShowControllerLoadingResult extends ShowControllerBaseResult { record: undefined; error: null; isPending: true; } -interface ShowControllerLoadingErrorResult< +export interface ShowControllerLoadingErrorResult< RecordType extends RaRecord = any, TError = Error > extends ShowControllerBaseResult { @@ -158,7 +158,7 @@ interface ShowControllerLoadingErrorResult< error: TError; isPending: false; } -interface ShowControllerRefetchErrorResult< +export interface ShowControllerRefetchErrorResult< RecordType extends RaRecord = any, TError = Error > extends ShowControllerBaseResult { @@ -166,7 +166,7 @@ interface ShowControllerRefetchErrorResult< error: TError; isPending: false; } -interface ShowControllerSuccessResult +export interface ShowControllerSuccessResult extends ShowControllerBaseResult { record: RecordType; error: null; From 931a4a95a0bf732b78c8e46fe05217faae8bfea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Tue, 26 Mar 2024 21:10:02 +0100 Subject: [PATCH 10/19] Misc fixes --- examples/demo/src/orders/MobileGrid.tsx | 2 +- examples/demo/src/visitors/MobileGrid.tsx | 2 +- packages/ra-core/src/controller/list/WithListContext.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo/src/orders/MobileGrid.tsx b/examples/demo/src/orders/MobileGrid.tsx index a75588f77bc..dae5c0c82fe 100644 --- a/examples/demo/src/orders/MobileGrid.tsx +++ b/examples/demo/src/orders/MobileGrid.tsx @@ -18,7 +18,7 @@ import { Order } from '../types'; const MobileGrid = () => { const { data, error, isPending } = useListContext(); const translate = useTranslate(); - if (isPending || (data && data.length === 0) || error) { + if (isPending || error || data.length === 0) { return null; } return ( diff --git a/examples/demo/src/visitors/MobileGrid.tsx b/examples/demo/src/visitors/MobileGrid.tsx index b60ad10bba4..2ea31e9d890 100644 --- a/examples/demo/src/visitors/MobileGrid.tsx +++ b/examples/demo/src/visitors/MobileGrid.tsx @@ -19,7 +19,7 @@ const MobileGrid = () => { const translate = useTranslate(); const { data, error, isPending } = useListContext(); - if (isPending || error || (data && data.length === 0)) { + if (isPending || error || data.length === 0) { return null; } diff --git a/packages/ra-core/src/controller/list/WithListContext.tsx b/packages/ra-core/src/controller/list/WithListContext.tsx index 11ea0d57bb3..a4c51c05bc4 100644 --- a/packages/ra-core/src/controller/list/WithListContext.tsx +++ b/packages/ra-core/src/controller/list/WithListContext.tsx @@ -26,7 +26,7 @@ export const WithListContext = ({ export interface WithListContextProps { render: ( - context: Partial> + context: ListControllerResult ) => ReactElement | false | null; label?: string; } From 6585422d5fe8a702c85516ac5c772c1ad5fc5b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 13:31:12 +0100 Subject: [PATCH 11/19] Remove ability to override context with props in create, edit, and show context --- .../controller/create/useCreateContext.tsx | 66 +++--------------- .../src/controller/edit/useEditContext.tsx | 69 +++---------------- .../src/controller/show/useShowContext.tsx | 57 +++------------ .../ra-ui-materialui/src/detail/Create.tsx | 4 +- .../src/detail/CreateView.tsx | 11 +-- packages/ra-ui-materialui/src/detail/Edit.tsx | 2 +- .../src/detail/EditActions.tsx | 7 +- .../ra-ui-materialui/src/detail/EditView.tsx | 8 +-- .../ra-ui-materialui/src/detail/ShowView.tsx | 2 +- packages/ra-ui-materialui/src/types.ts | 2 + 10 files changed, 45 insertions(+), 183 deletions(-) diff --git a/packages/ra-core/src/controller/create/useCreateContext.tsx b/packages/ra-core/src/controller/create/useCreateContext.tsx index dab149b9d9a..5a3a08407b4 100644 --- a/packages/ra-core/src/controller/create/useCreateContext.tsx +++ b/packages/ra-core/src/controller/create/useCreateContext.tsx @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { RaRecord } from '../../types'; import { CreateContext } from './CreateContext'; @@ -8,63 +7,20 @@ import { CreateControllerResult } 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 + * Used within a (e.g. as a descendent of ). * * @returns {CreateControllerResult} create controller props * * @see useCreateController for how it is filled - * */ -export const useCreateContext = ( - props?: any -): CreateControllerResult => { +export const useCreateContext = < + RecordType extends RaRecord = RaRecord +>(): CreateControllerResult => { const context = useContext(CreateContext); - // Props take precedence over the context - return useMemo( - () => - defaults( - {}, - props != null - ? extractCreateContextProps(props) - : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useCreateContext must be used inside a CreateContextProvider' + ); + } + return context; }; - -/** - * Extract only the create controller props - * - * @param {Object} props props passed to the useCreateContext hook - * - * @returns {CreateControllerResult} create controller props - */ -const extractCreateContextProps = ({ - record, - defaultTitle, - isFetching, - isLoading, - isPending, - redirect, - resource, - save, - saving, -}: Partial>) => ({ - record, - defaultTitle, - isFetching, - isLoading, - isPending, - redirect, - resource, - save, - saving, -}); diff --git a/packages/ra-core/src/controller/edit/useEditContext.tsx b/packages/ra-core/src/controller/edit/useEditContext.tsx index 3b10f24a241..0e4ebe5ebcd 100644 --- a/packages/ra-core/src/controller/edit/useEditContext.tsx +++ b/packages/ra-core/src/controller/edit/useEditContext.tsx @@ -6,69 +6,22 @@ import { EditContext } from './EditContext'; import { EditControllerResult } from './useEditController'; /** - * Hook to read the edit controller props from the CreateContext. + * Hook to read the edit controller props from the EditContext. * - * 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 + * Used within a (e.g. as a descendent of ). * * @returns {EditControllerResult} edit controller props * * @see useEditController for how it is filled - * */ -export const useEditContext = ( - props?: any -): EditControllerResult => { +export const useEditContext = < + RecordType extends RaRecord = any +>(): EditControllerResult => { const context = useContext(EditContext); - // Props take precedence over the context - return useMemo( - () => - defaults( - {}, - props != null ? extractEditContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useEditContext must be used inside an EditContextProvider' + ); + } + return context; }; - -/** - * Extract only the edit controller props - * - * @param {Object} props props passed to the useEditContext hook - * - * @returns {EditControllerResult} edit controller props - */ -const extractEditContextProps = ({ - data, - record, - defaultTitle, - isFetching, - isLoading, - isPending, - mutationMode, - redirect, - resource, - save, - saving, -}: Partial> & Record) => ({ - // 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, - isFetching, - isLoading, - isPending, - mutationMode, - redirect, - resource, - save, - saving, -}); diff --git a/packages/ra-core/src/controller/show/useShowContext.tsx b/packages/ra-core/src/controller/show/useShowContext.tsx index de61460dd52..e4f39404daf 100644 --- a/packages/ra-core/src/controller/show/useShowContext.tsx +++ b/packages/ra-core/src/controller/show/useShowContext.tsx @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { RaRecord } from '../../types'; import { ShowContext } from './ShowContext'; @@ -8,55 +7,21 @@ import { ShowControllerResult } 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} ShowControllerResult + * Used within a (e.g. as a descendent of ). * * @returns {ShowControllerResult} create controller props * * @see useShowController for how it is filled - * */ -export const useShowContext = ( - props?: any -): ShowControllerResult => { +export const useShowContext = < + RecordType extends RaRecord = any +>(): ShowControllerResult => { const context = useContext(ShowContext); // Props take precedence over the context - return useMemo( - () => - defaults( - {}, - props != null ? extractShowContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useShowContext must be used inside a ShowContextProvider' + ); + } + return context; }; - -/** - * Extract only the show controller props - * - * @param {Object} props props passed to the useShowContext hook - * - * @returns {ShowControllerResult} show controller props - */ -const extractShowContextProps = ({ - record, - defaultTitle, - isFetching, - isLoading, - isPending, - resource, -}: Partial>) => ({ - record, - defaultTitle, - isFetching, - isLoading, - isPending, - resource, -}); diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx index a5e6618d5d2..5654846ea5b 100644 --- a/packages/ra-ui-materialui/src/detail/Create.tsx +++ b/packages/ra-ui-materialui/src/detail/Create.tsx @@ -54,9 +54,7 @@ export const Create = < RecordType extends Omit = any, ResultRecordType extends RaRecord = RecordType & { id: Identifier } >( - props: CreateProps & { - children: ReactNode; - } + props: CreateProps ): ReactElement => { useCheckMinimumRequiredProps('Create', ['children'], props); const { diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx index 67aabe42731..474b4a7717d 100644 --- a/packages/ra-ui-materialui/src/detail/CreateView.tsx +++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { Card } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { RaRecord, CreateControllerProps, useCreateContext } from 'ra-core'; +import { useCreateContext } from 'ra-core'; import clsx from 'clsx'; import { CreateProps } from '../types'; @@ -20,7 +19,7 @@ export const CreateView = (props: CreateViewProps) => { ...rest } = props; - const { resource, defaultTitle } = useCreateContext(props); + const { resource, defaultTitle } = useCreateContext(); return ( { ); }; -interface CreateViewProps - extends CreateProps, - Omit, 'resource'> { - children: ReactNode; -} +export type CreateViewProps = CreateProps; CreateView.propTypes = { actions: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]), diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx index 24038cad311..3967e62eece 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.tsx +++ b/packages/ra-ui-materialui/src/detail/Edit.tsx @@ -52,7 +52,7 @@ import { EditBase } from 'ra-core'; * export default App; */ export const Edit = ( - props: EditProps & { children: ReactNode } + props: EditProps ) => { useCheckMinimumRequiredProps('Edit', ['children'], props); const { diff --git a/packages/ra-ui-materialui/src/detail/EditActions.tsx b/packages/ra-ui-materialui/src/detail/EditActions.tsx index acf6b82bacf..61eb55ac4f1 100644 --- a/packages/ra-ui-materialui/src/detail/EditActions.tsx +++ b/packages/ra-ui-materialui/src/detail/EditActions.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { RaRecord, useEditContext, useResourceDefinition } from 'ra-core'; +import { RaRecord, useResourceDefinition } from 'ra-core'; import { ShowButton } from '../button'; import TopToolbar from '../layout/TopToolbar'; @@ -31,12 +31,11 @@ import TopToolbar from '../layout/TopToolbar'; * ); */ export const EditActions = ({ className, ...rest }: EditActionsProps) => { - const { record } = useEditContext(rest); const { hasShow } = useResourceDefinition(rest); return ( - {hasShow && } + {hasShow && } ); }; @@ -53,7 +52,6 @@ const sanitizeRestProps = ({ export interface EditActionsProps { className?: string; - data?: RaRecord; hasCreate?: boolean; hasEdit?: boolean; hasList?: boolean; @@ -63,7 +61,6 @@ export interface EditActionsProps { EditActions.propTypes = { className: PropTypes.string, - data: PropTypes.object, hasCreate: PropTypes.bool, hasEdit: PropTypes.bool, hasShow: PropTypes.bool, diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx index 46e2fe99c5b..87d628dba2c 100644 --- a/packages/ra-ui-materialui/src/detail/EditView.tsx +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -28,7 +28,7 @@ export const EditView = (props: EditViewProps) => { } = props; const { hasShow } = useResourceDefinition(); - const { resource, defaultTitle, record } = useEditContext(props); + const { resource, defaultTitle, record } = useEditContext(); const finalActions = typeof actions === 'undefined' && hasShow ? ( @@ -64,11 +64,7 @@ export const EditView = (props: EditViewProps) => { ); }; -interface EditViewProps - extends EditProps, - Omit { - children: ReactNode; -} +export type EditViewProps = EditProps; EditView.propTypes = { actions: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]), diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx index 8a33f4ef8aa..aca615f0e4e 100644 --- a/packages/ra-ui-materialui/src/detail/ShowView.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx @@ -23,7 +23,7 @@ export const ShowView = (props: ShowViewProps) => { ...rest } = props; - const { resource, defaultTitle, record } = useShowContext(props); + const { resource, defaultTitle, record } = useShowContext(); const { hasEdit } = useResourceDefinition(props); const finalActions = diff --git a/packages/ra-ui-materialui/src/types.ts b/packages/ra-ui-materialui/src/types.ts index de21c0252cd..2801b12d4f7 100644 --- a/packages/ra-ui-materialui/src/types.ts +++ b/packages/ra-ui-materialui/src/types.ts @@ -17,6 +17,7 @@ export interface EditProps< > { actions?: ReactElement | false; aside?: ReactElement; + children: ReactNode; className?: string; component?: ElementType; disableAuthentication?: boolean; @@ -42,6 +43,7 @@ export interface CreateProps< > { actions?: ReactElement | false; aside?: ReactElement; + children: ReactNode; className?: string; component?: ElementType; disableAuthentication?: boolean; From 24f2549dd5af940562d77dbd0fbb2a7e4e44f325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 14:33:52 +0100 Subject: [PATCH 12/19] Make useListContext throw an error when used outside of a ListContext --- docs/Datagrid.md | 2 +- .../src/controller/edit/useEditContext.tsx | 3 +- packages/ra-core/src/controller/list/index.ts | 1 + .../controller/list/useListContext.spec.tsx | 16 ++- .../src/controller/list/useListContext.ts | 117 ++--------------- .../list/useListContextWithProps.spec.tsx | 42 ++++++ .../list/useListContextWithProps.ts | 123 ++++++++++++++++++ .../controller/list/useListFilterContext.ts | 44 ++----- .../list/useListPaginationContext.ts | 68 ++-------- .../src/controller/list/useListSortContext.ts | 35 ++--- .../button/BulkDeleteWithConfirmButton.tsx | 4 +- .../src/button/BulkDeleteWithUndoButton.tsx | 4 +- .../src/button/BulkExportButton.tsx | 13 +- .../button/BulkUpdateWithConfirmButton.tsx | 3 +- .../src/button/BulkUpdateWithUndoButton.tsx | 4 +- .../src/button/ExportButton.tsx | 7 +- .../src/button/UpdateWithConfirmButton.tsx | 1 - .../src/button/UpdateWithUndoButton.tsx | 2 - .../ra-ui-materialui/src/detail/Create.tsx | 2 +- packages/ra-ui-materialui/src/detail/Edit.tsx | 1 - .../src/detail/EditActions.tsx | 2 +- .../ra-ui-materialui/src/detail/EditView.tsx | 2 - .../src/field/ReferenceArrayField.tsx | 2 +- .../src/list/BulkActionsToolbar.tsx | 10 +- .../ra-ui-materialui/src/list/ListActions.tsx | 19 +-- .../ra-ui-materialui/src/list/ListGuesser.tsx | 2 +- .../ra-ui-materialui/src/list/ListView.tsx | 2 +- .../src/list/SimpleList/SimpleList.tsx | 6 +- .../src/list/SingleFieldList.tsx | 4 +- .../src/list/datagrid/Datagrid.stories.tsx | 3 +- .../src/list/datagrid/Datagrid.tsx | 6 +- .../src/list/datagrid/DatagridHeader.tsx | 12 +- .../src/list/datagrid/DatagridRow.tsx | 1 - .../src/list/filter/FilterButton.tsx | 17 +-- .../src/list/filter/FilterForm.tsx | 22 +--- .../src/list/pagination/Pagination.tsx | 7 +- packages/ra-ui-materialui/src/types.ts | 6 +- 37 files changed, 268 insertions(+), 347 deletions(-) create mode 100644 packages/ra-core/src/controller/list/useListContextWithProps.spec.tsx create mode 100644 packages/ra-core/src/controller/list/useListContextWithProps.ts diff --git a/docs/Datagrid.md b/docs/Datagrid.md index 44ffe061ec9..0c2dfd79ef5 100644 --- a/docs/Datagrid.md +++ b/docs/Datagrid.md @@ -1128,7 +1128,7 @@ const MyCustomList = () => { }; ``` -This list has no filtering, sorting, or row selection - it's static. If you want to allow users to interact with this list, you should pass more props to the `` component, but the logic isn't trivial. Fortunately, react-admin provides [the `useList` hook](./useList.md) to build callbacks to manipulate local data. You just have to put the result in a `ListContext` to have an interactive ``: +This list has no filtering, sorting, or row selection - it's static. If you want to allow users to interact with the ``, use [the `useList` hook](./useList.md) to build callbacks to manipulate local data. You will have to put the result in a `` parent component: ```tsx import { diff --git a/packages/ra-core/src/controller/edit/useEditContext.tsx b/packages/ra-core/src/controller/edit/useEditContext.tsx index 0e4ebe5ebcd..4bd0e5e39de 100644 --- a/packages/ra-core/src/controller/edit/useEditContext.tsx +++ b/packages/ra-core/src/controller/edit/useEditContext.tsx @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { RaRecord } from '../../types'; import { EditContext } from './EditContext'; diff --git a/packages/ra-core/src/controller/list/index.ts b/packages/ra-core/src/controller/list/index.ts index 7be1bbdf41f..e1f1c39a4f2 100644 --- a/packages/ra-core/src/controller/list/index.ts +++ b/packages/ra-core/src/controller/list/index.ts @@ -13,6 +13,7 @@ export * from './useInfiniteListController'; export * from './useInfinitePaginationContext'; export * from './useList'; export * from './useListContext'; +export * from './useListContextWithProps'; export * from './useListController'; export * from './useListFilterContext'; export * from './useListPaginationContext'; diff --git a/packages/ra-core/src/controller/list/useListContext.spec.tsx b/packages/ra-core/src/controller/list/useListContext.spec.tsx index 47717ccb8e0..1c9078d50da 100644 --- a/packages/ra-core/src/controller/list/useListContext.spec.tsx +++ b/packages/ra-core/src/controller/list/useListContext.spec.tsx @@ -6,8 +6,11 @@ import { ListContext } from './ListContext'; import { useListContext } from './useListContext'; describe('useListContext', () => { - const NaiveList = props => { - const { data } = useListContext(props); + const NaiveList = () => { + const { isPending, error, data } = useListContext(); + if (isPending || error) { + return null; + } return (
    {data.map(record => ( @@ -32,11 +35,10 @@ describe('useListContext', () => { expect(getByText('hello')).not.toBeNull(); }); - it('should return injected props if the context was not set', () => { - jest.spyOn(console, 'log').mockImplementationOnce(() => {}); - const { getByText } = render( - + it('should throw when called outside of a ListContextProvider', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + expect(() => render()).toThrow( + 'useListContext must be used inside a ListContextProvider' ); - expect(getByText('hello')).not.toBeNull(); }); }); diff --git a/packages/ra-core/src/controller/list/useListContext.ts b/packages/ra-core/src/controller/list/useListContext.ts index c4b82ef94f4..d3b86884baa 100644 --- a/packages/ra-core/src/controller/list/useListContext.ts +++ b/packages/ra-core/src/controller/list/useListContext.ts @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { ListContext } from './ListContext'; import { ListControllerResult } from './useListController'; @@ -8,36 +7,7 @@ import { RaRecord } from '../../types'; /** * Hook to read the list controller props from the ListContext. * - * 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 array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }] - * @prop {integer} total the total number of results for the current filters, excluding pagination. Useful to build the pagination controls. e.g. 23 - * @prop {boolean} isFetching boolean that is true on mount, and false once the data was fetched - * @prop {boolean} isLoading boolean that is false until the data is available - * @prop {integer} page the current page. Starts at 1 - * @prop {Function} setPage a callback to change the page, e.g. setPage(3) - * @prop {integer} perPage the number of results per page. Defaults to 25 - * @prop {Function} setPerPage a callback to change the number of results per page, e.g. setPerPage(25) - * @prop {Object} sort a sort object { field, order }, e.g. { field: 'date', order: 'DESC' } - * @prop {Function} setSort a callback to change the sort, e.g. setSort({ field : 'name', order: 'ASC' }) - * @prop {Object} filterValues a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' } - * @prop {Function} setFilters a callback to update the filters, e.g. setFilters(filters, displayedFilters) - * @prop {Object} displayedFilters a dictionary of the displayed filters, e.g. { title: true, nationality: true } - * @prop {Function} showFilter a callback to show one of the filters, e.g. showFilter('title', defaultValue) - * @prop {Function} hideFilter a callback to hide one of the filters, e.g. hideFilter('title') - * @prop {Array} selectedIds an array listing the ids of the selected rows, e.g. [123, 456] - * @prop {Function} onSelect callback to change the list of selected rows, e.g. onSelect([456, 789]) - * @prop {Function} onToggleItem callback to toggle the selection of a given record based on its id, e.g. onToggleItem(456) - * @prop {Function} onUnselectItems callback to clear the selection, e.g. onUnselectItems(); - * @prop {string} defaultTitle the translated title based on the resource, e.g. 'Posts' - * @prop {string} resource the resource name, deduced from the location. e.g. 'posts' + * Used within a (e.g. as a descendent of ). * * @returns {ListControllerResult} list controller props * @@ -90,79 +60,14 @@ import { RaRecord } from '../../types'; * ); * } */ -export const useListContext = ( - props?: any -): ListControllerResult => { +export const useListContext = < + RecordType extends RaRecord = any +>(): ListControllerResult => { const context = useContext(ListContext); - // Props take precedence over the context - return useMemo( - () => - defaults( - {}, - props != null ? extractListContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useListContext must be used inside a ListContextProvider' + ); + } + return context; }; - -/** - * Extract only the list controller props - * - * @param {Object} props Props passed to the useListContext hook - * - * @returns {ListControllerResult} List controller props - */ -const extractListContextProps = ({ - sort, - data, - defaultTitle, - displayedFilters, - exporter, - filterValues, - hasCreate, - hideFilter, - isFetching, - isLoading, - isPending, - onSelect, - onToggleItem, - onUnselectItems, - page, - perPage, - refetch, - resource, - selectedIds, - setFilters, - setPage, - setPerPage, - setSort, - showFilter, - total, -}: Partial> & Record) => ({ - sort, - data, - defaultTitle, - displayedFilters, - exporter, - filterValues, - hasCreate, - hideFilter, - isFetching, - isLoading, - isPending, - onSelect, - onToggleItem, - onUnselectItems, - page, - perPage, - refetch, - resource, - selectedIds, - setFilters, - setPage, - setPerPage, - setSort, - showFilter, - total, -}); diff --git a/packages/ra-core/src/controller/list/useListContextWithProps.spec.tsx b/packages/ra-core/src/controller/list/useListContextWithProps.spec.tsx new file mode 100644 index 00000000000..47d8d8901ce --- /dev/null +++ b/packages/ra-core/src/controller/list/useListContextWithProps.spec.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import expect from 'expect'; +import { render } from '@testing-library/react'; + +import { ListContext } from './ListContext'; +import { useListContextWithProps } from './useListContextWithProps'; + +describe('useListContextWithProps', () => { + const NaiveList = props => { + const { data } = useListContextWithProps(props); + return ( +
      + {data?.map(record => ( +
    • {record.title}
    • + ))} +
    + ); + }; + + it('should return the listController props form the ListContext', () => { + const { getByText } = render( + + + + ); + expect(getByText('hello')).not.toBeNull(); + }); + + it('should return injected props if the context was not set', () => { + jest.spyOn(console, 'log').mockImplementationOnce(() => {}); + const { getByText } = render( + + ); + expect(getByText('hello')).not.toBeNull(); + }); +}); diff --git a/packages/ra-core/src/controller/list/useListContextWithProps.ts b/packages/ra-core/src/controller/list/useListContextWithProps.ts new file mode 100644 index 00000000000..206be897b56 --- /dev/null +++ b/packages/ra-core/src/controller/list/useListContextWithProps.ts @@ -0,0 +1,123 @@ +import { useContext, useMemo } from 'react'; +import defaults from 'lodash/defaults'; + +import { ListContext } from './ListContext'; +import { ListControllerResult } from './useListController'; +import { RaRecord } from '../../types'; + +/** + * Hook to read the list controller props from the ListContext. + * + * 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 array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }] + * @prop {integer} total the total number of results for the current filters, excluding pagination. Useful to build the pagination controls. e.g. 23 + * @prop {boolean} isFetching boolean that is true on mount, and false once the data was fetched + * @prop {boolean} isLoading boolean that is false until the data is available + * @prop {integer} page the current page. Starts at 1 + * @prop {Function} setPage a callback to change the page, e.g. setPage(3) + * @prop {integer} perPage the number of results per page. Defaults to 25 + * @prop {Function} setPerPage a callback to change the number of results per page, e.g. setPerPage(25) + * @prop {Object} sort a sort object { field, order }, e.g. { field: 'date', order: 'DESC' } + * @prop {Function} setSort a callback to change the sort, e.g. setSort({ field : 'name', order: 'ASC' }) + * @prop {Object} filterValues a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' } + * @prop {Function} setFilters a callback to update the filters, e.g. setFilters(filters, displayedFilters) + * @prop {Object} displayedFilters a dictionary of the displayed filters, e.g. { title: true, nationality: true } + * @prop {Function} showFilter a callback to show one of the filters, e.g. showFilter('title', defaultValue) + * @prop {Function} hideFilter a callback to hide one of the filters, e.g. hideFilter('title') + * @prop {Array} selectedIds an array listing the ids of the selected rows, e.g. [123, 456] + * @prop {Function} onSelect callback to change the list of selected rows, e.g. onSelect([456, 789]) + * @prop {Function} onToggleItem callback to toggle the selection of a given record based on its id, e.g. onToggleItem(456) + * @prop {Function} onUnselectItems callback to clear the selection, e.g. onUnselectItems(); + * @prop {string} defaultTitle the translated title based on the resource, e.g. 'Posts' + * @prop {string} resource the resource name, deduced from the location. e.g. 'posts' + * + * @param {ListControllerProps} props Props passed to the useListContext hook + * + * @returns {ListControllerResult} list controller props + * + * @see useListController for how it is filled + */ +export const useListContextWithProps = ( + props?: any +): Partial> => { + const context = useContext(ListContext); + // Props take precedence over the context + return useMemo( + () => + defaults( + {}, + props != null ? extractListContextProps(props) : {}, + context + ), + [context, props] + ); +}; + +/** + * Extract only the list controller props + * + * @param {Object} props Props passed to the useListContext hook + * + * @returns {ListControllerResult} List controller props + */ +const extractListContextProps = ({ + sort, + data, + defaultTitle, + displayedFilters, + exporter, + filterValues, + hasCreate, + hideFilter, + isFetching, + isLoading, + isPending, + onSelect, + onToggleItem, + onUnselectItems, + page, + perPage, + refetch, + resource, + selectedIds, + setFilters, + setPage, + setPerPage, + setSort, + showFilter, + total, +}: Partial> & Record) => ({ + sort, + data, + defaultTitle, + displayedFilters, + exporter, + filterValues, + hasCreate, + hideFilter, + isFetching, + isLoading, + isPending, + onSelect, + onToggleItem, + onUnselectItems, + page, + perPage, + refetch, + resource, + selectedIds, + setFilters, + setPage, + setPerPage, + setSort, + showFilter, + total, +}); diff --git a/packages/ra-core/src/controller/list/useListFilterContext.ts b/packages/ra-core/src/controller/list/useListFilterContext.ts index 11fc18ff428..935f80f0cf2 100644 --- a/packages/ra-core/src/controller/list/useListFilterContext.ts +++ b/packages/ra-core/src/controller/list/useListFilterContext.ts @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { ListFilterContext, ListFilterContextValue } from './ListFilterContext'; @@ -8,43 +7,16 @@ import { ListFilterContext, ListFilterContextValue } from './ListFilterContext'; * * Must be used within a . * - * @typedef {Object} ListFilterContextValue - * @prop {Object} filterValues a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' } - * @prop {Function} setFilters a callback to update the filters, e.g. setFilters(filters, displayedFilters) - * @prop {Object} displayedFilters a dictionary of the displayed filters, e.g. { title: true, nationality: true } - * @prop {Function} showFilter a callback to show one of the filters, e.g. showFilter('title', defaultValue) - * @prop {Function} hideFilter a callback to hide one of the filters, e.g. hideFilter('title') - * @prop {string} resource the resource name, deduced from the location. e.g. 'posts' - * * @returns {ListFilterContextValue} list controller props * * @see useListController for how it is filled */ -export const useListFilterContext = (props?: any): ListFilterContextValue => { +export const useListFilterContext = (): ListFilterContextValue => { const context = useContext(ListFilterContext); - return useMemo( - () => - defaults( - {}, - props != null ? extractListPaginationContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useListFilterContext must be used inside a ListFilterContextProvider' + ); + } + return context; }; - -const extractListPaginationContextProps = ({ - displayedFilters, - filterValues, - hideFilter, - setFilters, - showFilter, - resource, -}) => ({ - displayedFilters, - filterValues, - hideFilter, - setFilters, - showFilter, - resource, -}); diff --git a/packages/ra-core/src/controller/list/useListPaginationContext.ts b/packages/ra-core/src/controller/list/useListPaginationContext.ts index 41691390238..11923418eed 100644 --- a/packages/ra-core/src/controller/list/useListPaginationContext.ts +++ b/packages/ra-core/src/controller/list/useListPaginationContext.ts @@ -1,5 +1,4 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { ListPaginationContext, @@ -7,68 +6,21 @@ import { } from './ListPaginationContext'; /** - * Hook to read the list controller props from the ListContext. + * Hook to read the list pagination controller props from the ListPaginationContext. * - * Must be used within a (e.g. as a descendent of + * Must be used within a (e.g. as a descendent of * or ). * - * @typedef {Object} ListPaginationContextValue - * @prop {integer} total the total number of results for the current filters, excluding pagination. Useful to build the pagination controls. e.g. 23 - * @prop {integer} page the current page. Starts at 1 - * @prop {Function} setPage a callback to change the page, e.g. setPage(3) - * @prop {integer} perPage the number of results per page. Defaults to 25 - * @prop {Function} setPerPage a callback to change the number of results per page, e.g. setPerPage(25) - * @prop {Boolean} hasPreviousPage true if the current page is not the first one - * @prop {Boolean} hasNextPage true if the current page is not the last one - * @prop {string} resource the resource name, deduced from the location. e.g. 'posts' - * * @returns {ListPaginationContextValue} list controller props * * @see useListController for how it is filled */ -export const useListPaginationContext = ( - props?: any -): ListPaginationContextValue => { +export const useListPaginationContext = (): ListPaginationContextValue => { const context = useContext(ListPaginationContext); - return useMemo( - () => - defaults( - {}, - props != null ? extractListPaginationContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useListPaginationContext must be used inside a ListPaginationContextProvider' + ); + } + return context; }; - -/** - * Extract only the list controller props - * - * @param {Object} props Props passed to the useListContext hook - * - * @returns {ListControllerResult} List controller props - */ -const extractListPaginationContextProps = ({ - isLoading, - isPending, - page, - perPage, - setPage, - setPerPage, - hasPreviousPage, - hasNextPage, - total, - resource, -}) => ({ - isLoading, - isPending, - page, - perPage, - setPage, - setPerPage, - hasPreviousPage, - hasNextPage, - total, - resource, -}); -export default useListPaginationContext; diff --git a/packages/ra-core/src/controller/list/useListSortContext.ts b/packages/ra-core/src/controller/list/useListSortContext.ts index 7bd80b6f548..39c836888c0 100644 --- a/packages/ra-core/src/controller/list/useListSortContext.ts +++ b/packages/ra-core/src/controller/list/useListSortContext.ts @@ -1,38 +1,23 @@ -import { useContext, useMemo } from 'react'; -import defaults from 'lodash/defaults'; +import { useContext } from 'react'; import { ListSortContext, ListSortContextValue } from './ListSortContext'; /** - * Hook to read the list controller props from the ListContext. + * Hook to read the list sort controller props from the ListSortContext. * - * Must be used within a (e.g. as a descendent of + * Must be used within a (e.g. as a descendent of * or ). * - * @typedef {Object} ListSortContextValue - * @prop {Object} sort a sort object { field, order }, e.g. { field: 'date', order: 'DESC' } - * @prop {Function} setSort a callback to change the sort, e.g. setSort({ field: 'name', order: 'ASC' }) - * @prop {string} resource the resource name, deduced from the location. e.g. 'posts' - * * @returns {ListSortContextValue} list controller props * * @see useListController for how it is filled */ -export const useListSortContext = (props?: any): ListSortContextValue => { +export const useListSortContext = (): ListSortContextValue => { const context = useContext(ListSortContext); - return useMemo( - () => - defaults( - {}, - props != null ? extractListPaginationContextProps(props) : {}, - context - ), - [context, props] - ); + if (!context) { + throw new Error( + 'useListSortContext must be used inside a ListSortContextProvider' + ); + } + return context; }; - -const extractListPaginationContextProps = ({ sort, setSort, resource }) => ({ - sort, - setSort, - resource, -}); diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx index 53a82ed3e99..c336a23e045 100644 --- a/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithConfirmButton.tsx @@ -37,7 +37,7 @@ export const BulkDeleteWithConfirmButton = ( ...rest } = props; const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions; - const { selectedIds, onUnselectItems } = useListContext(props); + const { selectedIds, onUnselectItems } = useListContext(); const [isOpen, setOpen] = useSafeSetState(false); const notify = useNotify(); const resource = useResourceContext(props); @@ -138,9 +138,7 @@ export const BulkDeleteWithConfirmButton = ( const sanitizeRestProps = ({ classes, - filterValues, label, - selectedIds, ...rest }: Omit< BulkDeleteWithConfirmButtonProps, diff --git a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx index 4b2f539ec9a..61d2dcd472f 100644 --- a/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkDeleteWithUndoButton.tsx @@ -29,7 +29,7 @@ export const BulkDeleteWithUndoButton = ( ...rest } = props; const { meta: mutationMeta, ...otherMutationOptions } = mutationOptions; - const { selectedIds, onUnselectItems } = useListContext(props); + const { selectedIds, onUnselectItems } = useListContext(); const notify = useNotify(); const resource = useResourceContext(props); @@ -93,9 +93,7 @@ const defaultIcon = ; const sanitizeRestProps = ({ classes, - filterValues, label, - selectedIds, ...rest }: Omit) => rest; diff --git a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx index 0074d99ec13..d3b328f2c77 100644 --- a/packages/ra-ui-materialui/src/button/BulkExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkExportButton.tsx @@ -6,9 +6,9 @@ import { fetchRelatedRecords, useDataProvider, useNotify, - Identifier, Exporter, useListContext, + useResourceContext, } from 'ra-core'; import { Button, ButtonProps } from './Button'; @@ -45,11 +45,8 @@ export const BulkExportButton = (props: BulkExportButtonProps) => { meta, ...rest } = props; - const { - exporter: exporterFromContext, - resource, - selectedIds, - } = useListContext(props); + const resource = useResourceContext(props); + const { exporter: exporterFromContext, selectedIds } = useListContext(); const exporter = customExporter || exporterFromContext; const dataProvider = useDataProvider(); const notify = useNotify(); @@ -93,19 +90,15 @@ export const BulkExportButton = (props: BulkExportButtonProps) => { const defaultIcon = ; const sanitizeRestProps = ({ - filterValues, - selectedIds, resource, ...rest }: Omit) => rest; interface Props { exporter?: Exporter; - filterValues?: any; icon?: JSX.Element; label?: string; onClick?: (e: Event) => void; - selectedIds?: Identifier[]; resource?: string; meta?: any; } diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx index 7166aa91568..012446451b3 100644 --- a/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithConfirmButton.tsx @@ -29,7 +29,7 @@ export const BulkUpdateWithConfirmButton = ( const resource = useResourceContext(props); const unselectAll = useUnselectAll(resource); const [isOpen, setOpen] = useState(false); - const { selectedIds } = useListContext(props); + const { selectedIds } = useListContext(); const { confirmTitle = 'ra.message.bulk_update_title', @@ -138,7 +138,6 @@ export const BulkUpdateWithConfirmButton = ( }; const sanitizeRestProps = ({ - filterValues, label, onSuccess, onError, diff --git a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx index ec2ceac65c0..7c70b9f696c 100644 --- a/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/BulkUpdateWithUndoButton.tsx @@ -22,7 +22,7 @@ import { BulkActionProps } from '../types'; export const BulkUpdateWithUndoButton = ( props: BulkUpdateWithUndoButtonProps ) => { - const { selectedIds } = useListContext(props); + const { selectedIds } = useListContext(); const notify = useNotify(); const resource = useResourceContext(props); @@ -99,9 +99,7 @@ export const BulkUpdateWithUndoButton = ( const defaultIcon = ; const sanitizeRestProps = ({ - filterValues, label, - selectedIds, onSuccess, onError, ...rest diff --git a/packages/ra-ui-materialui/src/button/ExportButton.tsx b/packages/ra-ui-materialui/src/button/ExportButton.tsx index fe5ef0bb3c7..6d542436494 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.tsx @@ -7,9 +7,7 @@ import { useDataProvider, useNotify, useListContext, - SortPayload, Exporter, - FilterPayload, useResourceContext, } from 'ra-core'; import { Button, ButtonProps } from './Button'; @@ -30,7 +28,7 @@ export const ExportButton = (props: ExportButtonProps) => { sort, exporter: exporterFromContext, total, - } = useListContext(props); + } = useListContext(); const resource = useResourceContext(props); const exporter = customExporter || exporterFromContext; const dataProvider = useDataProvider(); @@ -93,7 +91,6 @@ export const ExportButton = (props: ExportButtonProps) => { const defaultIcon = ; const sanitizeRestProps = ({ - filterValues, resource, ...rest }: Omit< @@ -103,13 +100,11 @@ const sanitizeRestProps = ({ interface Props { exporter?: Exporter; - filterValues?: FilterPayload; icon?: JSX.Element; label?: string; maxResults?: number; onClick?: (e: Event) => void; resource?: string; - sort?: SortPayload; meta?: any; } diff --git a/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx b/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx index 5f5593004e8..1db347e6b4b 100644 --- a/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx +++ b/packages/ra-ui-materialui/src/button/UpdateWithConfirmButton.tsx @@ -142,7 +142,6 @@ export const UpdateWithConfirmButton = ( }; const sanitizeRestProps = ({ - filterValues, label, ...rest }: Omit< diff --git a/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx b/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx index b603addba26..1faf0586a06 100644 --- a/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx +++ b/packages/ra-ui-materialui/src/button/UpdateWithUndoButton.tsx @@ -97,9 +97,7 @@ export const UpdateWithUndoButton = (props: UpdateWithUndoButtonProps) => { const defaultIcon = ; const sanitizeRestProps = ({ - filterValues, label, - selectedIds, ...rest }: Omit) => rest; diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx index 5654846ea5b..9c09a352132 100644 --- a/packages/ra-ui-materialui/src/detail/Create.tsx +++ b/packages/ra-ui-materialui/src/detail/Create.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ReactElement, ReactNode } from 'react'; +import { ReactElement } from 'react'; import PropTypes from 'prop-types'; import { Identifier, RaRecord, useCheckMinimumRequiredProps } from 'ra-core'; diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx index 3967e62eece..8051789f0d8 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.tsx +++ b/packages/ra-ui-materialui/src/detail/Edit.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { useCheckMinimumRequiredProps, RaRecord } from 'ra-core'; import { EditProps } from '../types'; diff --git a/packages/ra-ui-materialui/src/detail/EditActions.tsx b/packages/ra-ui-materialui/src/detail/EditActions.tsx index 61eb55ac4f1..738da4e5b74 100644 --- a/packages/ra-ui-materialui/src/detail/EditActions.tsx +++ b/packages/ra-ui-materialui/src/detail/EditActions.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { RaRecord, useResourceDefinition } from 'ra-core'; +import { useResourceDefinition } from 'ra-core'; import { ShowButton } from '../button'; import TopToolbar from '../layout/TopToolbar'; diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx index 87d628dba2c..4cdeb8cc3b6 100644 --- a/packages/ra-ui-materialui/src/detail/EditView.tsx +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { ReactNode } from 'react'; import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; import { Card, CardContent } from '@mui/material'; import clsx from 'clsx'; import { - EditControllerProps, ComponentPropType, useEditContext, useResourceDefinition, diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx index 87e5f634bbb..6663cc00e9f 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx @@ -151,7 +151,7 @@ export interface ReferenceArrayFieldViewProps export const ReferenceArrayFieldView: FC = props => { const { children, pagination, className, sx } = props; - const { isPending, total } = useListContext(props); + const { isPending, total } = useListContext(); return ( diff --git a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx index 57455cd6793..4441ef0f519 100644 --- a/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx +++ b/packages/ra-ui-materialui/src/list/BulkActionsToolbar.tsx @@ -8,12 +8,7 @@ import Typography from '@mui/material/Typography'; import { lighten } from '@mui/material/styles'; import IconButton from '@mui/material/IconButton'; import CloseIcon from '@mui/icons-material/Close'; -import { - useTranslate, - sanitizeListRestProps, - useListContext, - Identifier, -} from 'ra-core'; +import { useTranslate, sanitizeListRestProps, useListContext } from 'ra-core'; import TopToolbar from '../layout/TopToolbar'; @@ -24,7 +19,7 @@ export const BulkActionsToolbar = (props: BulkActionsToolbarProps) => { className, ...rest } = props; - const { selectedIds = [], onUnselectItems } = useListContext(props); + const { selectedIds = [], onUnselectItems } = useListContext(); const translate = useTranslate(); @@ -75,7 +70,6 @@ BulkActionsToolbar.propTypes = { export interface BulkActionsToolbarProps { children?: ReactNode; label?: string; - selectedIds?: Identifier[]; className?: string; } diff --git a/packages/ra-ui-materialui/src/list/ListActions.tsx b/packages/ra-ui-materialui/src/list/ListActions.tsx index e6dc072dbb7..30ff4580181 100644 --- a/packages/ra-ui-materialui/src/list/ListActions.tsx +++ b/packages/ra-ui-materialui/src/list/ListActions.tsx @@ -3,7 +3,6 @@ import { cloneElement, useMemo, useContext, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { sanitizeListRestProps, - Identifier, SortPayload, Exporter, useListContext, @@ -46,14 +45,7 @@ import { FilterButton } from './filter'; * ); */ export const ListActions = (props: ListActionsProps) => { - const { - className, - filters: filtersProp, - hasCreate: _, - selectedIds = defaultSelectedIds, - onUnselectItems = defaultOnUnselectItems, - ...rest - } = props; + const { className, filters: filtersProp, hasCreate: _, ...rest } = props; const { sort, @@ -62,7 +54,7 @@ export const ListActions = (props: ListActionsProps) => { exporter, showFilter, total, - } = useListContext({ ...props, selectedIds, onUnselectItems }); + } = useListContext(); const resource = useResourceContext(props); const { hasCreate } = useResourceDefinition(props); const filters = useContext(FilterContext) || filtersProp; @@ -118,8 +110,6 @@ ListActions.propTypes = { filterValues: PropTypes.object, hasCreate: PropTypes.bool, resource: PropTypes.string, - onUnselectItems: PropTypes.func, - selectedIds: PropTypes.arrayOf(PropTypes.any), showFilter: PropTypes.func, total: PropTypes.number, }; @@ -134,11 +124,6 @@ export interface ListActionsProps extends ToolbarProps { filterValues?: any; permanentFilter?: any; hasCreate?: boolean; - selectedIds?: Identifier[]; - onUnselectItems?: () => void; showFilter?: (filterName: string, defaultValue: any) => void; total?: number; } - -const defaultSelectedIds = []; -const defaultOnUnselectItems = () => null; diff --git a/packages/ra-ui-materialui/src/list/ListGuesser.tsx b/packages/ra-ui-materialui/src/list/ListGuesser.tsx index b400d50b159..5c9e3adae73 100644 --- a/packages/ra-ui-materialui/src/list/ListGuesser.tsx +++ b/packages/ra-ui-materialui/src/list/ListGuesser.tsx @@ -84,7 +84,7 @@ export const ListGuesser = ( const ListViewGuesser = ( props: Omit & { enableLog?: boolean } ) => { - const { data } = useListContext(props); + const { data } = useListContext(); const resource = useResourceContext(); const [child, setChild] = useState(null); const { diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx index 9f8369fd1cc..de21d0d35da 100644 --- a/packages/ra-ui-materialui/src/list/ListView.tsx +++ b/packages/ra-ui-materialui/src/list/ListView.tsx @@ -42,7 +42,7 @@ export const ListView = ( isPending, filterValues, resource, - } = useListContext(props); + } = useListContext(); if (!children || (!data && isPending && emptyWhileLoading)) { return null; diff --git a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx index 7ad68c65de9..822a4bcac7e 100644 --- a/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx +++ b/packages/ra-ui-materialui/src/list/SimpleList/SimpleList.tsx @@ -22,7 +22,7 @@ import { RaRecord, RecordContextProvider, sanitizeListRestProps, - useListContext, + useListContextWithProps, useResourceContext, useGetRecordRepresentation, useCreatePath, @@ -86,7 +86,9 @@ export const SimpleList = ( rowStyle, ...rest } = props; - const { data, isPending, total } = useListContext(props); + const { data, isPending, total } = useListContextWithProps( + props + ); const resource = useResourceContext(props); const getRecordRepresentation = useGetRecordRepresentation(resource); const translate = useTranslate(); diff --git a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx index 02c993fb7eb..d8bab89be4f 100644 --- a/packages/ra-ui-materialui/src/list/SingleFieldList.tsx +++ b/packages/ra-ui-materialui/src/list/SingleFieldList.tsx @@ -3,7 +3,7 @@ import { Chip, Stack, StackProps, styled } from '@mui/material'; import PropTypes from 'prop-types'; import { sanitizeListRestProps, - useListContext, + useListContextWithProps, useResourceContext, RaRecord, RecordContextProvider, @@ -59,7 +59,7 @@ export const SingleFieldList = (props: SingleFieldListProps) => { direction = 'row', ...rest } = props; - const { data, total, isPending } = useListContext(props); + const { data, total, isPending } = useListContextWithProps(props); const resource = useResourceContext(props); const createPath = useCreatePath(); diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.stories.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.stories.tsx index 31139af8abd..113c6a099f2 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.stories.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.stories.tsx @@ -10,6 +10,7 @@ import { useGetList, useList, TestMemoryRouter, + SortPayload, } from 'ra-core'; import fakeRestDataProvider from 'ra-data-fakerest'; import defaultMessages from 'ra-language-english'; @@ -326,7 +327,7 @@ export const ColumnStyles = () => ( ); -const sort = { field: 'id', order: 'DESC' }; +const sort = { field: 'id', order: 'DESC' } as SortPayload; const MyCustomList = () => { const { data, total, isLoading } = useGetList('books', { diff --git a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx index fa02b3457ac..9c296ced984 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/Datagrid.tsx @@ -14,7 +14,7 @@ import { import PropTypes from 'prop-types'; import { sanitizeListRestProps, - useListContext, + useListContextWithProps, Identifier, RaRecord, SortPayload, @@ -146,7 +146,7 @@ export const Datagrid: FC = React.forwardRef((props, ref) => { selectedIds, setSort, total, - } = useListContext(props); + } = useListContextWithProps(props); const hasBulkActions = !!bulkActionButtons !== false; @@ -234,7 +234,7 @@ export const Datagrid: FC = React.forwardRef((props, ref) => { className={clsx(DatagridClasses.root, className)} > {bulkActionButtons !== false ? ( - + {isValidElement(bulkActionButtons) ? bulkActionButtons : defaultBulkActionButtons} diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx index 19ecb1c2a1f..b2d236907e3 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridHeader.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Children, isValidElement, useCallback } from 'react'; import PropTypes from 'prop-types'; import { - useListContext, + useListContextWithProps, useResourceContext, Identifier, RaRecord, @@ -32,9 +32,13 @@ export const DatagridHeader = (props: DatagridHeaderProps) => { } = props; const resource = useResourceContext(props); const translate = useTranslate(); - const { sort, data, onSelect, selectedIds, setSort } = useListContext( - props - ); + const { + sort, + data, + onSelect, + selectedIds, + setSort, + } = useListContextWithProps(props); const { expandSingle } = useDatagridContext(); const updateSortCallback = useCallback( diff --git a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx index 70a4ebd44e5..0c2c8b1ab6a 100644 --- a/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx +++ b/packages/ra-ui-materialui/src/list/datagrid/DatagridRow.tsx @@ -1,6 +1,5 @@ import React, { isValidElement, - cloneElement, createElement, useState, useEffect, diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx index 4238128efb1..ade40e657e8 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.tsx @@ -42,7 +42,7 @@ export const FilterButton = (props: FilterButtonProps): JSX.Element => { setFilters, showFilter, sort, - } = useListContext(props); + } = useListContext(); const hasFilterValues = !isEqual(filterValues, {}); const validSavedQueries = extractValidSavedQueries(savedQueries); const hasSavedCurrentQuery = validSavedQueries.some(savedQuery => @@ -248,22 +248,17 @@ const sanitizeRestProps = ({ /* eslint-enable @typescript-eslint/no-unused-vars */ FilterButton.propTypes = { - resource: PropTypes.string, - filters: PropTypes.arrayOf(PropTypes.node), - displayedFilters: PropTypes.object, - filterValues: PropTypes.object, - showFilter: PropTypes.func, className: PropTypes.string, + disableSaveQuery: PropTypes.bool, + filters: PropTypes.arrayOf(PropTypes.node), + resource: PropTypes.string, }; export interface FilterButtonProps extends HtmlHTMLAttributes { className?: string; - resource?: string; - filterValues?: any; - showFilter?: (filterName: string, defaultValue: any) => void; - displayedFilters?: any; - filters?: ReactNode[]; disableSaveQuery?: boolean; + filters?: ReactNode[]; + resource?: string; } const PREFIX = 'RaFilterButton'; diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx index 11fdf5258d5..de9aa6d2a95 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.tsx @@ -10,7 +10,6 @@ import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; import { FormGroupsProvider, - ListFilterContextValue, SourceContextProvider, SourceContextValue, useListContext, @@ -34,9 +33,7 @@ import { FilterContext } from '../FilterContext'; export const FilterForm = (props: FilterFormProps) => { const { defaultValues, filters: filtersProps, ...rest } = props; - const { setFilters, displayedFilters, filterValues } = useListContext( - props - ); + const { setFilters, displayedFilters, filterValues } = useListContext(); const filters = useContext(FilterContext) || filtersProps; const mergedInitialValuesWithDefaultValues = mergeInitialValuesWithDefaultValues( @@ -105,7 +102,7 @@ export const FilterFormBase = (props: FilterFormBaseProps) => { const { className, filters, ...rest } = props; const resource = useResourceContext(props); const form = useFormContext(); - const { displayedFilters = {}, hideFilter } = useListContext(props); + const { displayedFilters = {}, hideFilter } = useListContext(); useEffect(() => { filters.forEach((filter: JSX.Element) => { @@ -180,11 +177,7 @@ FilterFormBase.propTypes = { }; const sanitizeRestProps = ({ - displayedFilters, - filterValues, hasCreate, - hideFilter, - setFilters, resource, ...props }: Partial & { hasCreate?: boolean }) => props; @@ -192,12 +185,11 @@ const sanitizeRestProps = ({ export type FilterFormBaseProps = Omit< HtmlHTMLAttributes, 'children' -> & - Partial & { - className?: string; - resource?: string; - filters?: ReactNode[]; - }; +> & { + className?: string; + resource?: string; + filters?: ReactNode[]; +}; export const mergeInitialValuesWithDefaultValues = ( initialValues, diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx index 452e9b7263f..bdc00cc4b1a 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.tsx @@ -13,7 +13,6 @@ import { useListPaginationContext, sanitizeListRestProps, ComponentPropType, - ListPaginationContextValue, } from 'ra-core'; import { PaginationActions, PaginationActionsProps } from './PaginationActions'; @@ -33,7 +32,7 @@ export const Pagination: FC = memo(props => { total, setPage, setPerPage, - } = useListPaginationContext(props); + } = useListPaginationContext(); const translate = useTranslate(); const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md') @@ -164,9 +163,7 @@ Pagination.propTypes = { const DefaultRowsPerPageOptions = [5, 10, 25, 50]; const emptyArray = []; -export interface PaginationProps - extends TablePaginationBaseProps, - Partial { +export interface PaginationProps extends TablePaginationBaseProps { rowsPerPageOptions?: Array; actions?: FC; limit?: ReactElement; diff --git a/packages/ra-ui-materialui/src/types.ts b/packages/ra-ui-materialui/src/types.ts index 2801b12d4f7..0a3166ba73c 100644 --- a/packages/ra-ui-materialui/src/types.ts +++ b/packages/ra-ui-materialui/src/types.ts @@ -77,8 +77,4 @@ export interface ShowProps { sx?: SxProps; } -export interface BulkActionProps { - filterValues?: any; - resource?: string; - selectedIds?: Identifier[]; -} +export interface BulkActionProps {} From 56f831e3c5fc7fafa74f81fc52b03e572bb58799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 14:47:23 +0100 Subject: [PATCH 13/19] Fix demo build --- examples/demo/src/reviews/ReviewEdit.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/demo/src/reviews/ReviewEdit.tsx b/examples/demo/src/reviews/ReviewEdit.tsx index ae9b5c7f82a..2100971d633 100644 --- a/examples/demo/src/reviews/ReviewEdit.tsx +++ b/examples/demo/src/reviews/ReviewEdit.tsx @@ -5,7 +5,6 @@ import { TextInput, SimpleForm, DateField, - EditProps, Labeled, } from 'react-admin'; import { Box, Grid, Stack, IconButton, Typography } from '@mui/material'; @@ -17,11 +16,12 @@ import StarRatingField from './StarRatingField'; import ReviewEditToolbar from './ReviewEditToolbar'; import { Review } from '../types'; -interface Props extends EditProps { +interface ReviewEditProps { + id: Review['id']; onCancel: () => void; } -const ReviewEdit = ({ id, onCancel }: Props) => { +const ReviewEdit = ({ id, onCancel }: ReviewEditProps) => { const translate = useTranslate(); return ( From 204e6884d45751e4cc95463c9a4aba71ceab8817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 15:03:19 +0100 Subject: [PATCH 14/19] Fix tests --- .../src/button/ExportButton.spec.tsx | 25 ++- .../src/button/ExportButton.tsx | 2 +- .../src/list/filter/FilterButton.spec.tsx | 87 ++++++---- .../src/list/filter/FilterForm.spec.tsx | 84 +++++---- .../src/list/pagination/Pagination.spec.tsx | 163 ++++++++---------- 5 files changed, 200 insertions(+), 161 deletions(-) diff --git a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx index c5935b3f674..093c1b917ae 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.spec.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import { screen, render, waitFor, fireEvent } from '@testing-library/react'; import expect from 'expect'; -import { CoreAdminContext, testDataProvider } from 'ra-core'; +import { + CoreAdminContext, + testDataProvider, + ListContextProvider, +} from 'ra-core'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { ExportButton } from './ExportButton'; @@ -18,12 +22,19 @@ describe('', () => { render( - + + + ); diff --git a/packages/ra-ui-materialui/src/button/ExportButton.tsx b/packages/ra-ui-materialui/src/button/ExportButton.tsx index 6d542436494..2dcacff5c16 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.tsx @@ -25,11 +25,11 @@ export const ExportButton = (props: ExportButtonProps) => { const { filter, filterValues, + resource, sort, exporter: exporterFromContext, total, } = useListContext(); - const resource = useResourceContext(props); const exporter = customExporter || exporterFromContext; const dataProvider = useDataProvider(); const notify = useNotify(); diff --git a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx index bff73bff80c..c6216dae059 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterButton.spec.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import expect from 'expect'; import { render, fireEvent, screen, waitFor } from '@testing-library/react'; import { createTheme } from '@mui/material/styles'; +import { ListContextProvider, ListControllerResult } from 'ra-core'; import { AdminContext } from '../../AdminContext'; import { FilterButton } from './FilterButton'; @@ -11,18 +12,21 @@ import { Basic, WithAutoCompleteArrayInput } from './FilterButton.stories'; const theme = createTheme(); describe('', () => { - const defaultProps = { + const defaultListContext = ({ resource: 'post', - filters: [ - , - , - ], displayedFilters: { title: true, 'customer.name': true, }, showFilter: () => {}, filterValues: {}, + } as unknown) as ListControllerResult; + + const defaultProps = { + filters: [ + , + , + ], }; beforeAll(() => { @@ -40,10 +44,12 @@ describe('', () => { ); const { getByLabelText, queryByText } = render( - + + + ); @@ -56,18 +62,26 @@ describe('', () => { it('should display the filter button if all filters are shown and there is a filter value', () => { render( - , - , - ]} - displayedFilters={{ - title: true, - 'customer.name': true, + + > + , + , + ]} + /> + ); expect( @@ -83,10 +97,11 @@ describe('', () => { ); const { getByLabelText, queryByText } = render( - + + + ); @@ -172,14 +187,22 @@ describe('', () => { it('should not display save query in filter button', async () => { const { queryByText } = render( - , - ]} - disableSaveQuery - /> + + , + ]} + disableSaveQuery + /> + ); expect( diff --git a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx index b6f8b0bf1e2..d96cbf033b7 100644 --- a/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx +++ b/packages/ra-ui-materialui/src/list/filter/FilterForm.spec.tsx @@ -2,9 +2,11 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import expect from 'expect'; import { ListContext, + ListContextProvider, minLength, ResourceContextProvider, testDataProvider, + ListControllerResult, } from 'ra-core'; import * as React from 'react'; @@ -23,13 +25,12 @@ import { } from './FilterForm'; describe('', () => { - const defaultProps = { + const defaultListContext = ({ resource: 'post', - filters: [], - setFilters: () => {}, + showFilter: () => {}, hideFilter: () => {}, displayedFilters: {}, - }; + } as unknown) as ListControllerResult; beforeAll(() => { window.scrollTo = jest.fn(); @@ -52,12 +53,15 @@ describe('', () => { render( - + + + ); expect(screen.queryAllByLabelText('Title')).toHaveLength(1); @@ -84,12 +88,15 @@ describe('', () => { expect(() => { render( - + + + ); }).not.toThrow(); @@ -105,12 +112,15 @@ describe('', () => { render( - + + + ); fireEvent.change(screen.queryByLabelText('Title') as Element, { @@ -140,12 +150,15 @@ describe('', () => { render( - + + + ); fireEvent.change(screen.queryByLabelText('Title') as HTMLElement, { @@ -202,12 +215,15 @@ describe('', () => { render( - + + + ); fireEvent.change(screen.queryByLabelText('Title') as Element, { diff --git a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx index e8ecfc15546..4db80aaf9ac 100644 --- a/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx +++ b/packages/ra-ui-materialui/src/list/pagination/Pagination.spec.tsx @@ -48,12 +48,14 @@ describe('', () => { render( @@ -69,12 +71,14 @@ describe('', () => { render( @@ -90,12 +94,14 @@ describe('', () => { render( @@ -114,13 +120,15 @@ describe('', () => { render( @@ -136,13 +144,15 @@ describe('', () => { render( @@ -158,13 +168,15 @@ describe('', () => { render( @@ -180,13 +192,15 @@ describe('', () => { render( @@ -205,12 +219,14 @@ describe('', () => { render( @@ -227,12 +243,14 @@ describe('', () => { render( @@ -245,35 +263,6 @@ describe('', () => { }); }); - it('should work outside of a ListContext', () => { - render( - - null} - isLoading={false} - setPerPage={() => {}} - hasNextPage={undefined} - hasPreviousPage={undefined} - perPage={1} - total={2} - page={1} - rowsPerPageOptions={[1]} - /> - - ); - const nextButton = screen.queryByLabelText( - 'Go to next page' - ) as HTMLButtonElement; - expect(nextButton).not.toBeNull(); - expect(nextButton.disabled).toBe(false); - const prevButton = screen.queryByLabelText( - 'Go to previous page' - ) as HTMLButtonElement; - expect(prevButton).not.toBeNull(); - expect(prevButton.disabled).toBe(true); - }); - describe('rowsPerPageOptions', () => { it('should accept an array of options with label and value', () => { render(); From 6f64d4155eaee63f49ca935e50dc90893ccb69cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 15:29:29 +0100 Subject: [PATCH 15/19] Fix e2e tests --- examples/simple/src/customRouteLayout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/simple/src/customRouteLayout.tsx b/examples/simple/src/customRouteLayout.tsx index b2765022f77..60bec0225a0 100644 --- a/examples/simple/src/customRouteLayout.tsx +++ b/examples/simple/src/customRouteLayout.tsx @@ -30,6 +30,8 @@ const CustomRouteLayout = ({ title = 'Posts' }) => { isPending={isPending} total={total} rowClick="edit" + bulkActionButtons={false} + resource="posts" > From 391de65448338475581ea05b13113c8d8c80683b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 15:29:38 +0100 Subject: [PATCH 16/19] Begin upgrade guide --- docs/Upgrade.md | 102 ++++++++++++++++++ .../src/button/ExportButton.tsx | 1 - 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index cb421c716fc..c35e7c0d06e 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -751,6 +751,108 @@ describe('my test suite', () => { }); ``` +## TypeScript: Page Contexts Are Now Types Instead of Interfaces + +The return type of page controllers is now a type. If you were using an interface extending one of: + +- `ListControllerResult`, +- `InfiniteListControllerResult`, +- `EditControllerResult`, +- `ShowControllerResult`, or +- `CreateControllerResult`, + +you'll have to change it to a type: + +```diff +import { ListControllerResult } from 'react-admin'; + +-interface MyListControllerResult extends ListControllerResult { ++type MyListControllerResult = ListControllerResult & { + customProp: string; +}; +``` + +## TypeScript: Stronger Types For Page Contexts + +The return type of page context hooks is now smarter. This concerns the following hooks: + +- `useListContext`, +- `useEditContext`, +- `useShowContext`, and +- `useCreateContext` + +Depending on the fetch status of the data, the type of the `data`, `error`, and `isPending` properties will be more precise: + +- Loading: `{ data: undefined, error: undefined, isPending: true }` +- Success: `{ data: , error: undefined, isPending: false }` +- Error: `{ data: undefined, error: , isPending: false }` +- Error After Refetch: `{ data: , error: , isPending: false }` + +This means that TypeScript may complain if you use the `data` property without checking if it's defined first. You'll have to update your code to handle the different states: + +```diff +const MyCustomList = () => { + const { data, error, isPending } = useListContext(); + if (isPending) return ; ++ if (error) return ; + return ( +
      + {data.map(record => ( +
    • {record.name}
    • + ))} +
    + ); +}; +``` + +Besides, these hooks will now throw an error when called outside of a page context. This means that you can't use them in a custom component that is not a child of a ``, ``, ``, ``, ``, ``, ``, or `` component. + +## List Components Can No Longer Be Used In Standalone + +An undocumented feature allowed some components designed for list pages to be used outside of a list page, by relying on their props instead of the `ListContext`. This feature was removed in v5. + +This concerns the following components: + +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` +- `` + +To continue using these components, you'll have to wrap them in a `` component: + +```diff +const MyPagination = ({ + page, + perPage, + total, + setPage, + setPerPage, +}) => { + return ( +- ++ ++ ++ + ); +}; +``` + +The following components are not affected and can still be used in standalone mode: + +- `` +- `` +- `` + ## Upgrading to v4 If you are on react-admin v3, follow the [Upgrading to v4](https://marmelab.com/react-admin/doc/4.16/Upgrade.html) guide before upgrading to v5. diff --git a/packages/ra-ui-materialui/src/button/ExportButton.tsx b/packages/ra-ui-materialui/src/button/ExportButton.tsx index 2dcacff5c16..70ab766dff3 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.tsx @@ -8,7 +8,6 @@ import { useNotify, useListContext, Exporter, - useResourceContext, } from 'ra-core'; import { Button, ButtonProps } from './Button'; From c4094530c26b5b8c173d1e8ebc893107eea8cf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 16:05:16 +0100 Subject: [PATCH 17/19] Continue Upgrade guide --- docs/Upgrade.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index c35e7c0d06e..bdf38b2e836 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -270,6 +270,26 @@ const App = () => ( ); ``` +## Custom Edit or Show Actions No Longer Receive Any Props + +React-admin used to inject the `record` and `resource` props to custom edit or show actions. These props are no longer injected in v5. If you need them, you'll have to use the `useRecordContext` and `useResourceContext` hooks instead. But if you use the standard react-admin buttons like ``, which already uses these hooks, you don't need inject anything. + +```diff +-const MyEditActions = ({ data }) => ( ++const MyEditActions = () => ( + +- ++ + +); + +const PostEdit = () => ( + } {...props}> + ... + +); +``` + ## Removed deprecated hooks The following deprecated hooks have been removed From 92b77e8757321f7fd1e5b67b275246d8f1004f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Wed, 27 Mar 2024 16:22:21 +0100 Subject: [PATCH 18/19] More upgrade guide --- docs/Upgrade.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index bdf38b2e836..b94408ae5f8 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -827,6 +827,18 @@ const MyCustomList = () => { Besides, these hooks will now throw an error when called outside of a page context. This means that you can't use them in a custom component that is not a child of a ``, ``, ``, ``, ``, ``, ``, or `` component. +## TypeScript: `EditProps` and `CreateProps` now expect a `children` prop + +`EditProps` and `CreateProps` now expect a `children` prop, just like `ListProps` and `ShowProps`. If you were using these types in your custom components, you'll have to update them: + +```diff +-const ReviewEdit = ({ id }: EditProps) => ( ++const ReviewEdit = ({ id }: Omit) => ( + + + ... +``` + ## List Components Can No Longer Be Used In Standalone An undocumented feature allowed some components designed for list pages to be used outside of a list page, by relying on their props instead of the `ListContext`. This feature was removed in v5. From c170fd317ee59645644a56ac67238c0c7a721409 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 09:50:09 +0200 Subject: [PATCH 19/19] Fix after rebase --- packages/ra-core/src/controller/list/useList.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts index 1cb232acf10..009479079ff 100644 --- a/packages/ra-core/src/controller/list/useList.ts +++ b/packages/ra-core/src/controller/list/useList.ts @@ -105,7 +105,13 @@ export const useList = ( ); // selection logic - const [selectedIds, selectionModifiers] = useRecordSelection({ resource }); + const [selectedIds, selectionModifiers] = useRecordSelection( + resource + ? { + resource, + } + : { disableSyncWithStore: true } + ); // filter logic const filterRef = useRef(filter);