From 557ad3d50f442396bbb6c8a7efecc76379d88527 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 16:39:23 +0200 Subject: [PATCH 01/15] [TS] Fix strict null check error in CoreAdminContext --- packages/ra-core/src/auth/AuthContext.tsx | 6 ++---- packages/ra-core/src/auth/index.ts | 3 +-- packages/ra-core/src/auth/useAuthProvider.ts | 2 +- packages/ra-core/src/core/CoreAdminContext.tsx | 14 ++++++++++---- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/ra-core/src/auth/AuthContext.tsx b/packages/ra-core/src/auth/AuthContext.tsx index 78165fe4494..032c67585eb 100644 --- a/packages/ra-core/src/auth/AuthContext.tsx +++ b/packages/ra-core/src/auth/AuthContext.tsx @@ -4,7 +4,7 @@ import { AuthProvider, UserIdentity } from '../types'; const defaultIdentity: UserIdentity = { id: '' }; -const defaultProvider: AuthProvider = { +export const defaultAuthProvider: AuthProvider = { login: () => Promise.resolve(), logout: () => Promise.resolve(), checkAuth: () => Promise.resolve(), @@ -13,8 +13,6 @@ const defaultProvider: AuthProvider = { getIdentity: () => Promise.resolve(defaultIdentity), }; -const AuthContext = createContext(defaultProvider); +export const AuthContext = createContext(defaultAuthProvider); AuthContext.displayName = 'AuthContext'; - -export default AuthContext; diff --git a/packages/ra-core/src/auth/index.ts b/packages/ra-core/src/auth/index.ts index 023b4f8de99..b8cf8dec7cb 100644 --- a/packages/ra-core/src/auth/index.ts +++ b/packages/ra-core/src/auth/index.ts @@ -1,4 +1,3 @@ -import AuthContext from './AuthContext'; import useAuthProvider from './useAuthProvider'; import useAuthState from './useAuthState'; import usePermissions from './usePermissions'; @@ -10,6 +9,7 @@ import useLogoutIfAccessDenied from './useLogoutIfAccessDenied'; import convertLegacyAuthProvider from './convertLegacyAuthProvider'; export * from './Authenticated'; +export * from './AuthContext'; export * from './types'; export * from './useAuthenticated'; export * from './useCheckAuth'; @@ -19,7 +19,6 @@ export * from './addRefreshAuthToAuthProvider'; export * from './addRefreshAuthToDataProvider'; export { - AuthContext, useAuthProvider, convertLegacyAuthProvider, // low-level hooks for calling a particular verb on the authProvider diff --git a/packages/ra-core/src/auth/useAuthProvider.ts b/packages/ra-core/src/auth/useAuthProvider.ts index d17d26501c3..93a78faf60e 100644 --- a/packages/ra-core/src/auth/useAuthProvider.ts +++ b/packages/ra-core/src/auth/useAuthProvider.ts @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { AuthProvider } from '../types'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; export const defaultAuthParams = { loginUrl: '/login', diff --git a/packages/ra-core/src/core/CoreAdminContext.tsx b/packages/ra-core/src/core/CoreAdminContext.tsx index 1fd7ca4edb3..28bd896d60a 100644 --- a/packages/ra-core/src/core/CoreAdminContext.tsx +++ b/packages/ra-core/src/core/CoreAdminContext.tsx @@ -3,7 +3,11 @@ import { useMemo } from 'react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { AdminRouter } from '../routing'; -import { AuthContext, convertLegacyAuthProvider } from '../auth'; +import { + AuthContext, + defaultAuthProvider, + convertLegacyAuthProvider, +} from '../auth'; import { DataProviderContext, convertLegacyDataProvider, @@ -187,9 +191,11 @@ React-admin requires a valid dataProvider function to work.`); const finalAuthProvider = useMemo( () => - authProvider instanceof Function - ? convertLegacyAuthProvider(authProvider) - : authProvider, + authProvider + ? authProvider instanceof Function + ? convertLegacyAuthProvider(authProvider) + : authProvider + : defaultAuthProvider, [authProvider] ); From 6838ae39001fa0bdbadc10ef19a3018e643c0427 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 16:58:02 +0200 Subject: [PATCH 02/15] [TS] Fix errors in core --- packages/ra-core/src/core/CoreAdminRoutes.tsx | 5 ++++ .../useConfigureAdminRouterFromChildren.tsx | 29 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/ra-core/src/core/CoreAdminRoutes.tsx b/packages/ra-core/src/core/CoreAdminRoutes.tsx index c2bd3b923eb..65db8e83a56 100644 --- a/packages/ra-core/src/core/CoreAdminRoutes.tsx +++ b/packages/ra-core/src/core/CoreAdminRoutes.tsx @@ -48,6 +48,11 @@ export const CoreAdminRoutes = (props: CoreAdminRoutesProps) => { }, [checkAuth, requireAuth]); if (status === 'empty') { + if (!Ready) { + throw new Error( + 'The admin is empty. Please provide an empty component, or pass Resource or CustomRoutes as children.' + ); + } return ; } diff --git a/packages/ra-core/src/core/useConfigureAdminRouterFromChildren.tsx b/packages/ra-core/src/core/useConfigureAdminRouterFromChildren.tsx index 993b089ce72..34b26dd327d 100644 --- a/packages/ra-core/src/core/useConfigureAdminRouterFromChildren.tsx +++ b/packages/ra-core/src/core/useConfigureAdminRouterFromChildren.tsx @@ -91,6 +91,9 @@ const useRoutesAndResourcesFromChildren = ( ...routesAndResources, }) ); + if (!status) { + throw new Error('Status should be defined'); + } useEffect(() => { const resolveChildFunction = async ( @@ -196,7 +199,7 @@ const useRoutesAndResourcesState = ( * @param permissions: The permissions */ const useRegisterResources = ( - resources: (ReactElement & ResourceWithRegisterFunction)[], + resources: (ReactElement & ResourceWithRegisterFunction)[], permissions: any ) => { const { register, unregister } = useResourceDefinitionContext(); @@ -246,10 +249,10 @@ const getStatus = ({ customRoutesWithoutLayout, }: { children: AdminChildren; - resources: ReactElement[]; - customRoutesWithLayout: ReactElement[]; - customRoutesWithoutLayout: ReactElement[]; -}) => { + resources: ReactNode[]; + customRoutesWithLayout: ReactNode[]; + customRoutesWithoutLayout: ReactNode[]; +}): AdminRouterStatus => { return getSingleChildFunction(children) ? 'loading' : resources.length > 0 || @@ -295,9 +298,9 @@ const getSingleChildFunction = ( const getRoutesAndResourceFromNodes = ( children: AdminChildren ): RoutesAndResources => { - const customRoutesWithLayout = []; - const customRoutesWithoutLayout = []; - const resources = []; + const customRoutesWithLayout: ReactNode[] = []; + const customRoutesWithoutLayout: ReactNode[] = []; + const resources: (ReactElement & ResourceWithRegisterFunction)[] = []; if (typeof children === 'function') { return { @@ -339,7 +342,9 @@ const getRoutesAndResourceFromNodes = ( customRoutesWithLayout.push(customRoutesElement.props.children); } } else if ((element.type as any).raName === 'Resource') { - resources.push(element as ReactElement); + resources.push( + element as ReactElement & ResourceWithRegisterFunction + ); } }); @@ -351,9 +356,9 @@ const getRoutesAndResourceFromNodes = ( }; type RoutesAndResources = { - customRoutesWithLayout: ReactElement[]; - customRoutesWithoutLayout: ReactElement[]; - resources: (ReactElement & ResourceWithRegisterFunction)[]; + customRoutesWithLayout: ReactNode[]; + customRoutesWithoutLayout: ReactNode[]; + resources: (ReactElement & ResourceWithRegisterFunction)[]; }; type ResourceWithRegisterFunction = { From db5a56671cbc37049dc907ce2a8f4d58f6b863f6 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 17:03:55 +0200 Subject: [PATCH 03/15] [TS] Fix TS errors in core controllers --- .../button/useDeleteWithConfirmController.tsx | 15 ++++++++------- .../button/useDeleteWithUndoController.tsx | 13 +++++++------ .../ra-core/src/controller/list/useUnselect.ts | 6 ++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx index 997eef0f3db..422d57bf2b5 100644 --- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx +++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx @@ -68,7 +68,7 @@ const useDeleteWithConfirmController = ( ): UseDeleteWithConfirmControllerReturn => { const { record, - redirect: redirectTo, + redirect: redirectTo = 'list', mutationMode, onClick, mutationOptions = {}, @@ -81,11 +81,7 @@ const useDeleteWithConfirmController = ( const redirect = useRedirect(); const [deleteOne, { isPending }] = useDelete( resource, - { - id: record.id, - previousData: record, - meta: mutationMeta, - }, + undefined, { onSuccess: () => { setOpen(false); @@ -94,7 +90,7 @@ const useDeleteWithConfirmController = ( messageArgs: { smart_count: 1 }, undoable: mutationMode === 'undoable', }); - unselect([record.id]); + record && unselect([record.id]); redirect(redirectTo, resource); }, onError: (error: Error) => { @@ -133,6 +129,11 @@ const useDeleteWithConfirmController = ( const handleDelete = useCallback( event => { event.stopPropagation(); + if (!record) { + throw new Error( + 'The record cannot be deleted because no record has been passed' + ); + } deleteOne( resource, { diff --git a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx index 36bd051970d..547307dd0a4 100644 --- a/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx +++ b/packages/ra-core/src/controller/button/useDeleteWithUndoController.tsx @@ -59,11 +59,7 @@ const useDeleteWithUndoController = ( const redirect = useRedirect(); const [deleteOne, { isPending }] = useDelete( resource, - { - id: record.id, - previousData: record, - meta: mutationMeta, - }, + undefined, { onSuccess: () => { notify('ra.notification.deleted', { @@ -71,7 +67,7 @@ const useDeleteWithUndoController = ( messageArgs: { smart_count: 1 }, undoable: true, }); - unselect([record.id]); + record && unselect([record.id]); redirect(redirectTo, resource); }, onError: (error: Error) => { @@ -98,6 +94,11 @@ const useDeleteWithUndoController = ( const handleDelete = useCallback( event => { event.stopPropagation(); + if (!record) { + throw new Error( + 'The record cannot be deleted because no record has been passed' + ); + } deleteOne( resource, { diff --git a/packages/ra-core/src/controller/list/useUnselect.ts b/packages/ra-core/src/controller/list/useUnselect.ts index 4aafd6e1fb3..ce5742eaeba 100644 --- a/packages/ra-core/src/controller/list/useUnselect.ts +++ b/packages/ra-core/src/controller/list/useUnselect.ts @@ -11,8 +11,10 @@ import { Identifier } from '../../types'; * const unselect = useUnselect('posts'); * unselect([123, 456]); */ -export const useUnselect = (resource: string) => { - const [, { unselect }] = useRecordSelection({ resource }); +export const useUnselect = (resource?: string) => { + const [, { unselect }] = useRecordSelection( + resource ? { resource } : { disableSyncWithStore: true } + ); return useCallback( (ids: Identifier[]) => { unselect(ids); From 9715d6b6f5244330fa12d097cb581376f6a21700 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 17:19:20 +0200 Subject: [PATCH 04/15] [TS] Fix various TS compilation errors --- .../src/controller/list/useRecordSelection.ts | 32 ++++++++++++------- .../ra-core/src/form/FormDataConsumer.tsx | 3 +- .../src/form/useWarnWhenUnsavedChanges.tsx | 4 +-- .../src/preferences/useSetInspectorTitle.ts | 8 ++++- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/ra-core/src/controller/list/useRecordSelection.ts b/packages/ra-core/src/controller/list/useRecordSelection.ts index 768411930a9..f986a854c0f 100644 --- a/packages/ra-core/src/controller/list/useRecordSelection.ts +++ b/packages/ra-core/src/controller/list/useRecordSelection.ts @@ -11,10 +11,21 @@ type UseRecordSelectionWithNoStoreArgs = { resource?: string; disableSyncWithStore: true; }; -type UseRecordSelectionArgs = + +export type UseRecordSelectionArgs = | UseRecordSelectionWithResourceArgs | UseRecordSelectionWithNoStoreArgs; +export type UseRecordSelectionResult = [ + RecordType['id'][], + { + select: (ids: RecordType['id'][]) => void; + unselect: (ids: RecordType['id'][]) => void; + toggle: (id: RecordType['id']) => void; + clearSelection: () => void; + } +]; + /** * Get the list of selected items for a resource, and callbacks to change the selection * @@ -25,23 +36,20 @@ type UseRecordSelectionArgs = */ export const useRecordSelection = ( args: UseRecordSelectionArgs -): [ - RecordType['id'][], - { - select: (ids: RecordType['id'][]) => void; - unselect: (ids: RecordType['id'][]) => void; - toggle: (id: RecordType['id']) => void; - clearSelection: () => void; - } -] => { +): UseRecordSelectionResult => { const { resource = '', disableSyncWithStore = false } = args; const storeKey = `${resource}.selectedIds`; - const [localIds, setLocalIds] = useState(defaultSelection); + const [localIds, setLocalIds] = useState( + defaultSelection + ); // As we can't conditionally call a hook, if the storeKey is false, // we'll ignore the params variable later on and won't call setParams either. - const [storeIds, setStoreIds] = useStore(storeKey, defaultSelection); + const [storeIds, setStoreIds] = useStore( + storeKey, + defaultSelection + ); const resetStore = useRemoveFromStore(storeKey); const ids = disableSyncWithStore ? localIds : storeIds; diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index 4a2b6126f29..05550e03af0 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -67,7 +67,8 @@ export const FormDataConsumerView = < const { children, formData, source } = props; let ret; - const finalSource = useWrappedSource(source); + const finalSource = useWrappedSource(source || ''); + // Passes an empty string here as we don't have the children sources and we just want to know if we are in an iterator const matches = ArraySourceRegex.exec(finalSource); diff --git a/packages/ra-core/src/form/useWarnWhenUnsavedChanges.tsx b/packages/ra-core/src/form/useWarnWhenUnsavedChanges.tsx index 35fbae21679..32ab655a702 100644 --- a/packages/ra-core/src/form/useWarnWhenUnsavedChanges.tsx +++ b/packages/ra-core/src/form/useWarnWhenUnsavedChanges.tsx @@ -63,9 +63,9 @@ export const useWarnWhenUnsavedChanges = ( translate('ra.message.unsaved_changes') ); if (shouldProceed) { - blocker.proceed(); + blocker.proceed && blocker.proceed(); } else { - blocker.reset(); + blocker.reset && blocker.reset(); } } setShouldNotify(false); diff --git a/packages/ra-core/src/preferences/useSetInspectorTitle.ts b/packages/ra-core/src/preferences/useSetInspectorTitle.ts index b5e6dec6208..ce990db8397 100644 --- a/packages/ra-core/src/preferences/useSetInspectorTitle.ts +++ b/packages/ra-core/src/preferences/useSetInspectorTitle.ts @@ -8,7 +8,13 @@ import { usePreferencesEditor } from './usePreferencesEditor'; * useSetInspectorTitle('Datagrid'); */ export const useSetInspectorTitle = (title: string, options?: any) => { - const { setTitle } = usePreferencesEditor(); + const preferencesEditorContext = usePreferencesEditor(); + if (!preferencesEditorContext) { + throw new Error( + 'useSetInspectorTitle cannot be called outside of a PreferencesEditorContext' + ); + } + const { setTitle } = preferencesEditorContext; useEffect(() => { setTitle(title, options); From ddb10becfd67d6b0ff529341cb1e96e17a07cc83 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 17:40:50 +0200 Subject: [PATCH 05/15] [TS] Mix more types --- .../src/controller/input/useReferenceInputController.ts | 8 +++++--- packages/ra-core/src/controller/usePrevNextController.ts | 9 ++++++--- packages/ra-core/src/core/useGetRecordRepresentation.ts | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/ra-core/src/controller/input/useReferenceInputController.ts b/packages/ra-core/src/controller/input/useReferenceInputController.ts index 103cb6b5179..7f1fec4c2aa 100644 --- a/packages/ra-core/src/controller/input/useReferenceInputController.ts +++ b/packages/ra-core/src/controller/input/useReferenceInputController.ts @@ -132,13 +132,15 @@ export const useReferenceInputController = ( // We need to delay the update of the referenceRecord and the finalData // to the next React state update, because otherwise it can raise a warning // with AutocompleteInput saying the current value is not in the list of choices - const [referenceRecord, setReferenceRecord] = useState(null); + const [referenceRecord, setReferenceRecord] = useState< + RecordType | undefined + >(undefined); useEffect(() => { setReferenceRecord(currentReferenceRecord); }, [currentReferenceRecord]); // add current value to possible sources - let finalData: RecordType[], finalTotal: number; + let finalData: RecordType[], finalTotal: number | undefined; if ( !referenceRecord || possibleValuesData.find(record => record.id === referenceRecord.id) @@ -166,7 +168,7 @@ export const useReferenceInputController = ( sort: currentSort, allChoices: finalData, availableChoices: possibleValuesData, - selectedChoices: [referenceRecord], + selectedChoices: referenceRecord ? [referenceRecord] : [], displayedFilters: params.displayedFilters, error: errorReference || errorPossibleValues, filter: params.filter, diff --git a/packages/ra-core/src/controller/usePrevNextController.ts b/packages/ra-core/src/controller/usePrevNextController.ts index 6d42e0748ed..e2e6f2d43a5 100644 --- a/packages/ra-core/src/controller/usePrevNextController.ts +++ b/packages/ra-core/src/controller/usePrevNextController.ts @@ -129,7 +129,7 @@ export const usePrevNextController = ( ); } - const [storedParams] = useStore>( + const [storedParams] = useStore( storeKey || `${resource}.listParams`, { filter: filterDefaultValues, @@ -137,6 +137,7 @@ export const usePrevNextController = ( sort: initialSort.field, page: 1, perPage: 10, + displayedFilters: {}, } ); @@ -172,8 +173,10 @@ export const usePrevNextController = ( const isRecordIndexFirstInNonFirstPage = recordIndexInQueryData === 0 && storedParams.page > 1; const isRecordIndexLastInNonLastPage = - recordIndexInQueryData === queryData?.data?.length - 1 && - storedParams.page < queryData?.total / storedParams.perPage; + queryData?.data && queryData?.total + ? recordIndexInQueryData === queryData?.data?.length - 1 && + storedParams.page < queryData?.total / storedParams.perPage + : undefined; const canUseCacheData = record && queryData?.data && diff --git a/packages/ra-core/src/core/useGetRecordRepresentation.ts b/packages/ra-core/src/core/useGetRecordRepresentation.ts index ffdb5662f72..59a251c8f9b 100644 --- a/packages/ra-core/src/core/useGetRecordRepresentation.ts +++ b/packages/ra-core/src/core/useGetRecordRepresentation.ts @@ -16,7 +16,7 @@ import { useResourceDefinition } from './useResourceDefinition'; * getRecordRepresentation({ id: 1, title: 'Hello' }); // => "Hello" */ export const useGetRecordRepresentation = ( - resource: string + resource?: string ): ((record: any) => ReactNode) => { const { recordRepresentation } = useResourceDefinition({ resource }); return useCallback( From 07dc817fbd1287ea75be4618dc8b133964522827 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 17:46:14 +0200 Subject: [PATCH 06/15] Fix tests --- packages/ra-core/src/auth/useCheckAuth.spec.tsx | 2 +- packages/ra-core/src/auth/useGetIdentity.spec.tsx | 2 +- packages/ra-core/src/auth/useGetIdentity.stories.tsx | 2 +- packages/ra-core/src/auth/useHandleAuthCallback.spec.tsx | 2 +- packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ra-core/src/auth/useCheckAuth.spec.tsx b/packages/ra-core/src/auth/useCheckAuth.spec.tsx index ba38501ef78..8f21feb25b5 100644 --- a/packages/ra-core/src/auth/useCheckAuth.spec.tsx +++ b/packages/ra-core/src/auth/useCheckAuth.spec.tsx @@ -6,7 +6,7 @@ import { Location } from 'react-router-dom'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useCheckAuth } from './useCheckAuth'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; import { BasenameContextProvider, TestMemoryRouter } from '../routing'; import { useNotify } from '../notification/useNotify'; diff --git a/packages/ra-core/src/auth/useGetIdentity.spec.tsx b/packages/ra-core/src/auth/useGetIdentity.spec.tsx index 27d2764144c..cc537062517 100644 --- a/packages/ra-core/src/auth/useGetIdentity.spec.tsx +++ b/packages/ra-core/src/auth/useGetIdentity.spec.tsx @@ -4,7 +4,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { Basic, ErrorCase, ResetIdentity } from './useGetIdentity.stories'; import useGetIdentity from './useGetIdentity'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; describe('useGetIdentity', () => { it('should return the identity', async () => { diff --git a/packages/ra-core/src/auth/useGetIdentity.stories.tsx b/packages/ra-core/src/auth/useGetIdentity.stories.tsx index 9d78a7f37ec..387345b2c9d 100644 --- a/packages/ra-core/src/auth/useGetIdentity.stories.tsx +++ b/packages/ra-core/src/auth/useGetIdentity.stories.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useGetIdentity } from './useGetIdentity'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; export default { title: 'ra-core/auth/useGetIdentity', diff --git a/packages/ra-core/src/auth/useHandleAuthCallback.spec.tsx b/packages/ra-core/src/auth/useHandleAuthCallback.spec.tsx index b51e26abf7e..1ba624e777d 100644 --- a/packages/ra-core/src/auth/useHandleAuthCallback.spec.tsx +++ b/packages/ra-core/src/auth/useHandleAuthCallback.spec.tsx @@ -5,7 +5,7 @@ import { Route, Routes } from 'react-router-dom'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useHandleAuthCallback } from './useHandleAuthCallback'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; import { AuthProvider } from '../types'; import { TestMemoryRouter } from '../routing'; diff --git a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx index e02d1e365d3..dcd79382766 100644 --- a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx +++ b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx @@ -5,7 +5,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import { Routes, Route } from 'react-router-dom'; import useLogoutIfAccessDenied from './useLogoutIfAccessDenied'; -import AuthContext from './AuthContext'; +import { AuthContext } from './AuthContext'; import useLogout from './useLogout'; import { useNotify } from '../notification/useNotify'; import { AuthProvider } from '../types'; From c34f7283a3bc896660b91b69b54f4569329c7af5 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 17:50:38 +0200 Subject: [PATCH 07/15] Fix test --- .../src/controller/input/useReferenceInputController.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/input/useReferenceInputController.spec.tsx b/packages/ra-core/src/controller/input/useReferenceInputController.spec.tsx index 57449d36adb..8ef7f60c826 100644 --- a/packages/ra-core/src/controller/input/useReferenceInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/useReferenceInputController.spec.tsx @@ -170,7 +170,7 @@ describe('useReferenceInputController', () => { ); await waitFor(() => { - expect(children.mock.calls.length).toBeGreaterThanOrEqual(4); + expect(children.mock.calls.length).toBeGreaterThanOrEqual(3); }); expect(children).toHaveBeenCalledWith( expect.objectContaining({ From 249a39499490258f6636df1bd3ec757e947a739a Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 18:03:41 +0200 Subject: [PATCH 08/15] [TS] Fix strict compilation errors in core --- packages/ra-core/src/form/FormGroupContext.ts | 4 +++- packages/ra-core/src/form/useFormGroup.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/form/FormGroupContext.ts b/packages/ra-core/src/form/FormGroupContext.ts index 104b08ecfa5..d7f1d46e1d5 100644 --- a/packages/ra-core/src/form/FormGroupContext.ts +++ b/packages/ra-core/src/form/FormGroupContext.ts @@ -7,6 +7,8 @@ import { createContext } from 'react'; * * This should only be used through a FormGroupContextProvider. */ -export const FormGroupContext = createContext(undefined); +export const FormGroupContext = createContext( + null +); export type FormGroupContextValue = string; diff --git a/packages/ra-core/src/form/useFormGroup.ts b/packages/ra-core/src/form/useFormGroup.ts index d0de0bbcb27..2d6fbba3a04 100644 --- a/packages/ra-core/src/form/useFormGroup.ts +++ b/packages/ra-core/src/form/useFormGroup.ts @@ -73,6 +73,7 @@ export const useFormGroup = (name: string): FormGroupState => { }); const updateGroupState = useCallback(() => { + if (!formGroups) return; const fields = formGroups.getGroupFields(name); const fieldStates = fields .map(field => { @@ -109,11 +110,13 @@ export const useFormGroup = (name: string): FormGroupState => { ); useEffect(() => { + if (!formGroups) return; // Whenever the group content changes (input are added or removed) // we must update its state - return formGroups.subscribe(name, () => { + const unsubscribe = formGroups.subscribe(name, () => { updateGroupState(); }); + return unsubscribe; }, [formGroups, name, updateGroupState]); return state; From 64d76bee3b0f6584cd3a97dd6c865de084b6498b Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 8 Apr 2024 18:22:31 +0200 Subject: [PATCH 09/15] [TS] Fix more compilation errors in strict mode --- .../src/controller/field/ReferenceFieldContext.tsx | 6 +++--- .../field/useReferenceArrayFieldController.ts | 5 ++--- .../field/useReferenceManyFieldController.ts | 4 ++-- .../ra-core/src/controller/input/useReferenceParams.ts | 10 +++++++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/ra-core/src/controller/field/ReferenceFieldContext.tsx b/packages/ra-core/src/controller/field/ReferenceFieldContext.tsx index 6e87f4c9ec8..0176d1a5a45 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldContext.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldContext.tsx @@ -1,9 +1,9 @@ import { createContext, useContext } from 'react'; import type { UseReferenceFieldControllerResult } from './useReferenceFieldController'; -export const ReferenceFieldContext = createContext< - UseReferenceFieldControllerResult ->(null); +export const ReferenceFieldContext = createContext( + null +); export const ReferenceFieldContextProvider = ReferenceFieldContext.Provider; diff --git a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts index 624d1de6072..8a3d33ca96c 100644 --- a/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts @@ -26,7 +26,6 @@ export interface UseReferenceArrayFieldControllerParams< const emptyArray = []; const defaultFilter = {}; -const defaultSort = { field: null, order: null }; /** * Hook that fetches records from another resource specified @@ -66,7 +65,7 @@ export const useReferenceArrayFieldController = < perPage = 1000, record, reference, - sort = defaultSort, + sort, source, queryOptions = {}, } = props; @@ -126,7 +125,7 @@ export const useReferenceArrayFieldController = < return { ...listProps, - defaultTitle: null, + defaultTitle: undefined, refetch, resource: reference, }; diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 66395fb0703..34a39731f75 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -78,7 +78,7 @@ export const useReferenceManyFieldController = < record, target, filter = defaultFilter, - source, + source = 'id', page: initialPage, perPage: initialPerPage, sort: initialSort = { field: 'id', order: 'DESC' }, @@ -223,7 +223,7 @@ export const useReferenceManyFieldController = < return { sort, data, - defaultTitle: null, + defaultTitle: undefined, displayedFilters, error, filterValues, diff --git a/packages/ra-core/src/controller/input/useReferenceParams.ts b/packages/ra-core/src/controller/input/useReferenceParams.ts index 6213523b637..d31a0a220fc 100644 --- a/packages/ra-core/src/controller/input/useReferenceParams.ts +++ b/packages/ra-core/src/controller/input/useReferenceParams.ts @@ -92,10 +92,11 @@ export const useReferenceParams = ({ const changeParams = useCallback(action => { if (!tempParams.current) { // no other changeParams action dispatched this tick - tempParams.current = queryReducer(query, action); + const newTempParams = queryReducer(query, action); + tempParams.current = newTempParams; // schedule side effects for next tick setTimeout(() => { - setParams(tempParams.current); + setParams(newTempParams); tempParams.current = undefined; }, 0); } else { @@ -178,10 +179,10 @@ export const useReferenceParams = ({ }, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps return [ { - displayedFilters: displayedFilterValues, filterValues, requestSignature, ...query, + displayedFilters: displayedFilterValues, }, { changeParams, @@ -270,6 +271,9 @@ export const getNumberOrDefault = ( possibleNumber: string | number | undefined, defaultValue: number ) => { + if (typeof possibleNumber === 'undefined') { + return defaultValue; + } const parsedNumber = typeof possibleNumber === 'string' ? parseInt(possibleNumber, 10) From 175e6fdfdce8b955823f06a73a5c7da878f1e1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Mon, 8 Apr 2024 22:02:30 +0200 Subject: [PATCH 10/15] Fix remaining compilation errors --- .../ra-core/src/controller/list/useListParams.ts | 10 +++++++++- .../saveContext/useRegisterMutationMiddleware.ts | 3 +++ packages/ra-core/src/controller/useFilterState.ts | 9 +++++---- .../ra-core/src/form/choices/useChoicesContext.ts | 12 ++++++------ .../preferences/PreferencesEditorContextProvider.tsx | 6 +++--- packages/ra-core/tsconfig.json | 3 ++- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/ra-core/src/controller/list/useListParams.ts b/packages/ra-core/src/controller/list/useListParams.ts index 5d52e795d33..5d477b6be2c 100644 --- a/packages/ra-core/src/controller/list/useListParams.ts +++ b/packages/ra-core/src/controller/list/useListParams.ts @@ -157,6 +157,11 @@ export const useListParams = ({ tempParams.current = queryReducer(query, action); // schedule side effects for next tick setTimeout(() => { + if (!tempParams.current) { + throw new Error( + 'Race condition in changeParams detected' + ); + } if (disableSyncWithLocation) { setLocalParams(tempParams.current); } else { @@ -262,10 +267,10 @@ export const useListParams = ({ return [ { - displayedFilters: displayedFilterValues, filterValues, requestSignature, ...query, + displayedFilters: displayedFilterValues, }, { changeParams, @@ -375,6 +380,9 @@ export const getNumberOrDefault = ( possibleNumber: string | number | undefined, defaultValue: number ) => { + if (typeof possibleNumber === 'undefined') { + return defaultValue; + } const parsedNumber = typeof possibleNumber === 'string' ? parseInt(possibleNumber, 10) diff --git a/packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.ts b/packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.ts index b1a8aec1882..4a08e3a10bf 100644 --- a/packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.ts +++ b/packages/ra-core/src/controller/saveContext/useRegisterMutationMiddleware.ts @@ -17,6 +17,9 @@ export const useRegisterMutationMiddleware = < } = useSaveContext(); useEffect(() => { + if (!registerMutationMiddleware || !unregisterMutationMiddleware) { + return; + } registerMutationMiddleware(callback); return () => { unregisterMutationMiddleware(callback); diff --git a/packages/ra-core/src/controller/useFilterState.ts b/packages/ra-core/src/controller/useFilterState.ts index daa352c42c6..89498992a3b 100644 --- a/packages/ra-core/src/controller/useFilterState.ts +++ b/packages/ra-core/src/controller/useFilterState.ts @@ -21,10 +21,11 @@ interface UseFilterStateProps { setFilter: (v: string) => void; } +const defaultFilter = {}; const defaultFilterToQuery = (v: string) => ({ q: v }); /** - * Hooks to provide filter state and setFilter which update the query part of the filter + * Hooks to provide filter state and setFilter which updates the query part of the filter * * @example * @@ -59,7 +60,7 @@ export default ({ }: UseFilterStateOptions): UseFilterStateProps => { const permanentFilterProp = useRef(permanentFilter); const latestValue = useRef(); - const [filter, setFilterValue] = useSafeSetState({ + const [filter, setFilterValue] = useSafeSetState({ ...permanentFilter, ...filterToQuery(''), }); @@ -76,7 +77,7 @@ export default ({ permanentFilterProp.current = permanentFilter; setFilterValue({ ...permanentFilter, - ...filterToQuery(latestValue.current), + ...filterToQuery(latestValue.current || ''), }); } }, [permanentFilterSignature, permanentFilterProp, filterToQuery]); // eslint-disable-line react-hooks/exhaustive-deps @@ -94,7 +95,7 @@ export default ({ ); return { - filter, + filter: filter ?? defaultFilter, setFilter, }; }; diff --git a/packages/ra-core/src/form/choices/useChoicesContext.ts b/packages/ra-core/src/form/choices/useChoicesContext.ts index 517fef7250d..a4cc8ea7851 100644 --- a/packages/ra-core/src/form/choices/useChoicesContext.ts +++ b/packages/ra-core/src/form/choices/useChoicesContext.ts @@ -11,9 +11,9 @@ export const useChoicesContext = ( >; const { data, ...list } = useList({ data: options.choices, - isLoading: options.isLoading, - isPending: options.isPending, - isFetching: options.isFetching, + isLoading: options.isLoading ?? false, + isPending: options.isPending ?? false, + isFetching: options.isFetching ?? false, // When not in a ChoicesContext, paginating does not make sense (e.g. AutocompleteInput). perPage: Infinity, }); @@ -33,9 +33,9 @@ export const useChoicesContext = ( hasPreviousPage: options.hasPreviousPage ?? list.hasPreviousPage, hideFilter: options.hideFilter ?? list.hideFilter, - isLoading: list.isLoading, // we must take the one for useList, otherwise the loading state isn't synchronized with the data - isPending: list.isPending, // same - isFetching: list.isFetching, // same + isLoading: list.isLoading ?? false, // we must take the one for useList, otherwise the loading state isn't synchronized with the data + isPending: list.isPending ?? false, // same + isFetching: list.isFetching ?? false, // same page: options.page ?? list.page, perPage: options.perPage ?? list.perPage, refetch: options.refetch ?? list.refetch, diff --git a/packages/ra-core/src/preferences/PreferencesEditorContextProvider.tsx b/packages/ra-core/src/preferences/PreferencesEditorContextProvider.tsx index b9c0220441b..4118b15d49f 100644 --- a/packages/ra-core/src/preferences/PreferencesEditorContextProvider.tsx +++ b/packages/ra-core/src/preferences/PreferencesEditorContextProvider.tsx @@ -7,10 +7,10 @@ import { export const PreferencesEditorContextProvider = ({ children }) => { const [isEnabled, setIsEnabled] = useState(false); - const [editor, setEditor] = useState(null); + const [editor, setEditor] = useState(null); const [preferenceKey, setPreferenceKey] = useState(); - const [path, setPath] = useState(null); - const [title, setTitleString] = useState(null); + const [path, setPath] = useState(null); + const [title, setTitleString] = useState(null); const [titleOptions, setTitleOptions] = useState(); const enable = useCallback(() => setIsEnabled(true), []); const disable = useCallback(() => { diff --git a/packages/ra-core/tsconfig.json b/packages/ra-core/tsconfig.json index 6fb08aeb372..d4c2c252ddb 100644 --- a/packages/ra-core/tsconfig.json +++ b/packages/ra-core/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "allowJs": false + "allowJs": false, + "strictNullChecks": true }, "exclude": [ "**/*.spec.ts", From 6e48022e349606337b09d54cf12bce5d456ae3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?= Date: Mon, 8 Apr 2024 22:32:09 +0200 Subject: [PATCH 11/15] Fix demo compilation, add upgrade guide --- docs/Upgrade.md | 21 +++++++++++++++++++ examples/crm/src/companies/CompanyShow.tsx | 2 +- examples/crm/src/contacts/ContactAside.tsx | 6 +++++- examples/crm/src/contacts/TagsListEdit.tsx | 15 +++++++++++-- examples/crm/src/notes/Note.tsx | 4 ++-- .../products/CreateRelatedReviewButton.tsx | 2 +- examples/demo/src/reviews/AcceptButton.tsx | 2 +- examples/demo/src/reviews/RejectButton.tsx | 2 +- examples/demo/src/visitors/Aside.tsx | 18 ++++++++-------- 9 files changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/Upgrade.md b/docs/Upgrade.md index b94408ae5f8..434624e65bf 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -771,6 +771,27 @@ describe('my test suite', () => { }); ``` +## TypeScript: `useRecordContext` Returns `undefined` When No Record Is Available + +The `useRecordContext` hook reads the current record from the `RecordContext`. This context may be empty (e.g. while the record is being fetched). The return type for `useRecordContext` has been modified to `Record | undefined` instead of `Record` to denote this possibility. + +As a consequence, the TypeScript compilation of your project may fail if you don't check the existence of the record before reading it. + +To fix this error, your code should handle the case where `useRecordContext` returns `undefined`: + +```diff +const MyComponent = () => { + const record = useRecordContext(); ++ if (!record) return null; + return ( +
+

{record.title}

+

{record.body}

+
+ ); +}; +``` + ## 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: diff --git a/examples/crm/src/companies/CompanyShow.tsx b/examples/crm/src/companies/CompanyShow.tsx index 28955ae5b73..02ffb139eee 100644 --- a/examples/crm/src/companies/CompanyShow.tsx +++ b/examples/crm/src/companies/CompanyShow.tsx @@ -202,7 +202,7 @@ const CreateRelatedContactButton = () => {