diff --git a/UPGRADE.md b/UPGRADE.md index b5739bec4e4..a1c6243708c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -640,6 +640,17 @@ test('MyComponent', () => { }); ``` +## `useAuthenticated` Signature hsa Changed + +`useAuthenticated` uses to accept only the parameters passed to the `authProvider.checkAuth` function. It now accept an option object with two properties: +- `enabled`: whether it should check for an authenticated user +- `params`: the parameters to pass to `checkAuth` + +```diff +- useAuthenticated('permissions.posts.can_create'); ++ useAuthenticated({ params: 'permissions.posts.can_create' }) +``` + # Upgrade to 3.0 We took advantage of the major release to fix all the problems in react-admin that required a breaking change. As a consequence, you'll need to do many small changes in the code of existing react-admin v2 applications. Follow this step-by-step guide to upgrade to react-admin v3. diff --git a/docs/Authentication.md b/docs/Authentication.md index 4c125553a52..9667c0c3a76 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -701,11 +701,15 @@ const MyPage = () => { export default MyPage; ``` -If you call `useAuthenticated()` with a parameter, this parameter is passed to the `authProvider` call as second parameter. that allows you to add authentication logic depending on the context of the call: +`useAuthenticated` accepts an options object as its only argument, with the following properties: +- `enabled`: whether it should check for an authenticated user (`true` by default) +- `params`: the parameters to pass to `checkAuth` + +If you call `useAuthenticated()` with a `params` option, those parameters are passed to the `authProvider.checkAuth` call. That allows you to add authentication logic depending on the context of the call: ```jsx const MyPage = () => { - useAuthenticated({ foo: 'bar' }); // calls authProvider.checkAuth({ foo: 'bar' }) + useAuthenticated({ params: foo: 'bar' } }); // calls authProvider.checkAuth({ foo: 'bar' }) return (
... @@ -1165,3 +1169,31 @@ const Menu = ({ onMenuClick, logout }) => { ); } ``` + +### Allowing Anonymous Access to Custom Views + +You might have custom views that leverage react-admin components or hooks such as: + +- `Create`, `CreateBase` `CreateController` and `useCreateController` +- `Edit`, `EditBase`, `EditController` and `useEditController` +- `List`, `ListBase`, `ListController` and `useListController` +- `Show`, `ShowBase`, `ShowController` and `useShowController` + +By default, they all redirect anonymous users to the login page. You can disable this behavior by passing the `disableAuthentication` boolean prop: + +```jsx +const MostRecentComments = () => { + const { data, loaded } = useListController({ + disableAuthentication: true, + resource: 'comments', + sort: { field: 'created_at', order: 'desc' }, + perPage: 10 + }); + + if (!loaded) { + return null; + } + + return +} +``` diff --git a/examples/simple/src/users/UserCreate.tsx b/examples/simple/src/users/UserCreate.tsx index 63d6765502b..18806c8d061 100644 --- a/examples/simple/src/users/UserCreate.tsx +++ b/examples/simple/src/users/UserCreate.tsx @@ -9,6 +9,7 @@ import { TextInput, Toolbar, required, + usePermissions, } from 'react-admin'; import Aside from './Aside'; @@ -38,32 +39,35 @@ const isValidName = async value => ) ); -const UserCreate = ({ permissions, ...props }) => ( - }> - }> - - - - {permissions === 'admin' && ( - - { + const { permissions } = usePermissions(); + return ( + }> + }> + + - )} - - -); + {permissions === 'admin' && ( + + + + )} + + + ); +}; export default UserCreate; diff --git a/package.json b/package.json index 54f85a2e30e..64eae7d3e4b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.6", - "eslint-plugin-react-hooks": "^4.1.0", + "eslint-plugin-react-hooks": "^4.3.0", "express": "~4.16.3", "full-icu": "~1.3.1", "husky": "^2.3.0", diff --git a/packages/ra-core/src/auth/Authenticated.tsx b/packages/ra-core/src/auth/Authenticated.tsx index 417000986ed..99d34b396bd 100644 --- a/packages/ra-core/src/auth/Authenticated.tsx +++ b/packages/ra-core/src/auth/Authenticated.tsx @@ -1,6 +1,6 @@ import { cloneElement, ReactElement } from 'react'; -import useAuthenticated from './useAuthenticated'; +import { useAuthenticated } from './useAuthenticated'; export interface AuthenticatedProps { children: ReactElement; @@ -43,7 +43,7 @@ const Authenticated = (props: AuthenticatedProps) => { location, // kept for backwards compatibility, unused ...rest } = props; - useAuthenticated(authParams); + useAuthenticated({ params: authParams }); // render the child even though the useAuthenticated() call isn't finished (optimistic rendering) // the above hook will log out if the authProvider doesn't validate that the user is authenticated return cloneElement(children, rest); diff --git a/packages/ra-core/src/auth/WithPermissions.tsx b/packages/ra-core/src/auth/WithPermissions.tsx index 69ec032aec6..04461aba66e 100644 --- a/packages/ra-core/src/auth/WithPermissions.tsx +++ b/packages/ra-core/src/auth/WithPermissions.tsx @@ -2,7 +2,7 @@ import { Children, ReactElement, ComponentType, createElement } from 'react'; import { Location } from 'history'; import warning from '../util/warning'; -import useAuthenticated from './useAuthenticated'; +import { useAuthenticated } from './useAuthenticated'; import usePermissionsOptimized from './usePermissionsOptimized'; export interface WithPermissionsChildrenParams { diff --git a/packages/ra-core/src/auth/index.ts b/packages/ra-core/src/auth/index.ts index 807c1afe08c..a21f211ee59 100644 --- a/packages/ra-core/src/auth/index.ts +++ b/packages/ra-core/src/auth/index.ts @@ -4,16 +4,17 @@ import useAuthProvider from './useAuthProvider'; import useAuthState from './useAuthState'; import usePermissions from './usePermissions'; import usePermissionsOptimized from './usePermissionsOptimized'; -import useAuthenticated from './useAuthenticated'; import WithPermissions, { WithPermissionsProps } from './WithPermissions'; import useLogin from './useLogin'; import useLogout from './useLogout'; -import useCheckAuth from './useCheckAuth'; import useGetIdentity from './useGetIdentity'; import useGetPermissions from './useGetPermissions'; import useLogoutIfAccessDenied from './useLogoutIfAccessDenied'; import convertLegacyAuthProvider from './convertLegacyAuthProvider'; + export * from './types'; +export * from './useAuthenticated'; +export * from './useCheckAuth'; export { AuthContext, @@ -22,7 +23,6 @@ export { // low-level hooks for calling a particular verb on the authProvider useLogin, useLogout, - useCheckAuth, useGetIdentity, useGetPermissions, // hooks with state management @@ -30,7 +30,6 @@ export { usePermissionsOptimized, useAuthState, // hook with immediate effect - useAuthenticated, useLogoutIfAccessDenied, // components Authenticated, diff --git a/packages/ra-core/src/auth/useAuthState.ts b/packages/ra-core/src/auth/useAuthState.ts index 6e25e272a2e..b3bfa9e87c8 100644 --- a/packages/ra-core/src/auth/useAuthState.ts +++ b/packages/ra-core/src/auth/useAuthState.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -import useCheckAuth from './useCheckAuth'; +import { useCheckAuth } from './useCheckAuth'; import { useSafeSetState } from '../util/hooks'; interface State { diff --git a/packages/ra-core/src/auth/useAuthenticated.tsx b/packages/ra-core/src/auth/useAuthenticated.ts similarity index 63% rename from packages/ra-core/src/auth/useAuthenticated.tsx rename to packages/ra-core/src/auth/useAuthenticated.ts index dd6f781acb8..75fa3cfe605 100644 --- a/packages/ra-core/src/auth/useAuthenticated.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.ts @@ -1,7 +1,5 @@ import { useEffect } from 'react'; -import useCheckAuth from './useCheckAuth'; - -const emptyParams = {}; +import { useCheckAuth } from './useCheckAuth'; /** * Restrict access to authenticated users. @@ -28,9 +26,21 @@ const emptyParams = {}; * * ); */ -export default (params: any = emptyParams) => { +export const useAuthenticated = ( + options: UseAuthenticatedOptions = {} +) => { + const { enabled = true, params = emptyParams } = options; const checkAuth = useCheckAuth(); useEffect(() => { - checkAuth(params).catch(() => {}); - }, [checkAuth, params]); + if (enabled) { + checkAuth(params).catch(() => {}); + } + }, [checkAuth, enabled, params]); +}; + +export type UseAuthenticatedOptions = { + enabled?: boolean; + params?: ParamsType; }; + +const emptyParams = {}; diff --git a/packages/ra-core/src/auth/useCheckAuth.spec.tsx b/packages/ra-core/src/auth/useCheckAuth.spec.tsx index 6b4fcd8d422..c609dcd9966 100644 --- a/packages/ra-core/src/auth/useCheckAuth.spec.tsx +++ b/packages/ra-core/src/auth/useCheckAuth.spec.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import expect from 'expect'; import { render, waitFor } from '@testing-library/react'; -import useCheckAuth from './useCheckAuth'; +import { useCheckAuth } from './useCheckAuth'; import AuthContext from './AuthContext'; import useLogout from './useLogout'; import useNotify from '../sideEffect/useNotify'; diff --git a/packages/ra-core/src/auth/useCheckAuth.ts b/packages/ra-core/src/auth/useCheckAuth.ts index fe8e695db06..4f8b5319a8e 100644 --- a/packages/ra-core/src/auth/useCheckAuth.ts +++ b/packages/ra-core/src/auth/useCheckAuth.ts @@ -40,7 +40,7 @@ import useNotify from '../sideEffect/useNotify'; * return authenticated ? : ; * } // tip: use useAuthState() hook instead */ -const useCheckAuth = (): CheckAuth => { +export const useCheckAuth = (): CheckAuth => { const authProvider = useAuthProvider(); const notify = useNotify(); const logout = useLogout(); @@ -90,7 +90,7 @@ const checkAuthWithoutAuthProvider = () => Promise.resolve(); * * @return {Promise} Resolved to the authProvider response if the user passes the check, or rejected with an error otherwise */ -type CheckAuth = ( +export type CheckAuth = ( params?: any, logoutOnFailure?: boolean, redirectTo?: string, @@ -104,5 +104,3 @@ const getErrorMessage = (error, defaultMessage) => : typeof error === 'undefined' || !error.message ? defaultMessage : error.message; - -export default useCheckAuth; diff --git a/packages/ra-core/src/controller/create/index.ts b/packages/ra-core/src/controller/create/index.ts index 0a6dddeb990..246c389caaa 100644 --- a/packages/ra-core/src/controller/create/index.ts +++ b/packages/ra-core/src/controller/create/index.ts @@ -1,18 +1,6 @@ -import { - CreateControllerResult, - useCreateController, -} from './useCreateController'; - export * from './CreateBase'; export * from './CreateContext'; export * from './CreateContextProvider'; export * from './CreateController'; - export * from './useCreateContext'; -export * from '../show/useShowContext'; - -// We don't want to export CreateProps and EditProps as they should -// not be used outside ra-core, since it would conflict with ra-ui-materialui types, -// hence the named imports/exports -export type { CreateControllerResult as CreateControllerProps }; -export { useCreateController }; +export * from './useCreateController'; diff --git a/packages/ra-core/src/controller/create/useCreateController.spec.tsx b/packages/ra-core/src/controller/create/useCreateController.spec.tsx index e1042b894bc..58d9c821fe1 100644 --- a/packages/ra-core/src/controller/create/useCreateController.spec.tsx +++ b/packages/ra-core/src/controller/create/useCreateController.spec.tsx @@ -1,60 +1,49 @@ import React from 'react'; import expect from 'expect'; import { act } from '@testing-library/react'; +import { Location } from 'history'; -import { getRecord } from './useCreateController'; +import { getRecordFromLocation } from './useCreateController'; import { CreateController } from './CreateController'; import { renderWithRedux } from 'ra-test'; import { DataProviderContext } from '../../dataProvider'; import { DataProvider } from '../../types'; describe('useCreateController', () => { - describe('getRecord', () => { - const location = { + describe('getRecordFromLocation', () => { + const location: Location = { + key: 'a_key', pathname: '/foo', search: undefined, state: undefined, hash: undefined, }; - it('should return an empty record by default', () => { - expect(getRecord(location, undefined)).toEqual({}); - }); - it('should return location state record when set', () => { expect( - getRecord( - { - ...location, - state: { record: { foo: 'bar' } }, - }, - undefined - ) + getRecordFromLocation({ + ...location, + state: { record: { foo: 'bar' } }, + }) ).toEqual({ foo: 'bar' }); }); it('should return location search when set', () => { expect( - getRecord( - { - ...location, - search: '?source={"foo":"baz","array":["1","2"]}', - }, - undefined - ) + getRecordFromLocation({ + ...location, + search: '?source={"foo":"baz","array":["1","2"]}', + }) ).toEqual({ foo: 'baz', array: ['1', '2'] }); }); it('should return location state record when both state and search are set', () => { expect( - getRecord( - { - ...location, - state: { record: { foo: 'bar' } }, - search: '?foo=baz', - }, - undefined - ) + getRecordFromLocation({ + ...location, + state: { record: { foo: 'bar' } }, + search: '?foo=baz', + }) ).toEqual({ foo: 'bar' }); }); }); diff --git a/packages/ra-core/src/controller/create/useCreateController.ts b/packages/ra-core/src/controller/create/useCreateController.ts index 8300e1862b6..e6572bba17d 100644 --- a/packages/ra-core/src/controller/create/useCreateController.ts +++ b/packages/ra-core/src/controller/create/useCreateController.ts @@ -2,7 +2,9 @@ import { useCallback, MutableRefObject } from 'react'; // @ts-ignore import { parse } from 'query-string'; import { useLocation } from 'react-router-dom'; +import { Location } from 'history'; +import { useAuthenticated } from '../../auth'; import { useCreate } from '../../dataProvider'; import { useNotify, @@ -49,20 +51,23 @@ export const useCreateController = < props: CreateControllerProps = {} ): CreateControllerResult => { const { - record = {}, - successMessage, + disableAuthentication, onSuccess, onFailure, + record, + successMessage, transform, } = props; + useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); const { hasEdit, hasShow } = useResourceDefinition(props); const location = useLocation(); const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); - const recordToUse = getRecord(location, record); + const recordToUse = + record ?? getRecordFromLocation(location) ?? emptyRecord; const version = useVersion(); if (process.env.NODE_ENV !== 'production' && successMessage) { @@ -190,12 +195,13 @@ export const useCreateController = < export interface CreateControllerProps< RecordType extends Omit = Record > { + disableAuthentication?: boolean; record?: Partial; resource?: string; onSuccess?: OnSuccess; onFailure?: OnFailure; - transform?: TransformData; successMessage?: string; + transform?: TransformData; } export interface CreateControllerResult< @@ -230,7 +236,12 @@ export interface CreateControllerResult< version: number; } -export const getRecord = ({ state, search }, record: any = {}) => { +const emptyRecord = {}; +/** + * Get the initial record from the location, whether it comes from the location + * state or is serialized in the url search part. + */ +export const getRecordFromLocation = ({ state, search }: Location) => { if (state && state.record) { return state.record; } @@ -252,7 +263,7 @@ export const getRecord = ({ state, search }, record: any = {}) => { ); } } - return record; + return null; }; const getDefaultRedirectRoute = (hasShow, hasEdit) => { diff --git a/packages/ra-core/src/controller/edit/useEditController.ts b/packages/ra-core/src/controller/edit/useEditController.ts index 8a9adfcae45..9993f27fd3f 100644 --- a/packages/ra-core/src/controller/edit/useEditController.ts +++ b/packages/ra-core/src/controller/edit/useEditController.ts @@ -2,6 +2,7 @@ import { useCallback, MutableRefObject } from 'react'; import { useParams } from 'react-router-dom'; import { UseQueryOptions, UseMutationOptions } from 'react-query'; +import { useAuthenticated } from '../../auth'; import useVersion from '../useVersion'; import { Record, @@ -54,12 +55,14 @@ export const useEditController = ( props: EditControllerProps = {} ): EditControllerResult => { const { + disableAuthentication, id: propsId, mutationMode = 'undoable', transform, queryOptions = {}, mutationOptions = {}, } = props; + useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); const translate = useTranslate(); const notify = useNotify(); @@ -214,17 +217,18 @@ export const useEditController = ( }; export interface EditControllerProps { + disableAuthentication?: boolean; id?: Identifier; - resource?: string; mutationMode?: MutationMode; - queryOptions?: UseQueryOptions; mutationOptions?: UseMutationOptions< RecordType, unknown, UpdateParams >; - onSuccess?: OnSuccess; onFailure?: OnFailure; + onSuccess?: OnSuccess; + queryOptions?: UseQueryOptions; + resource?: string; transform?: TransformData; [key: string]: any; } diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 6c90dafc31c..ed753bfcecb 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -1,6 +1,7 @@ import { isValidElement, ReactElement, useEffect, useMemo } from 'react'; import { Location } from 'history'; +import { useAuthenticated } from '../../auth'; import { useTranslate } from '../../i18n'; import { useNotify } from '../../sideEffect'; import { useGetMainList, Refetch } from '../../dataProvider'; @@ -44,6 +45,7 @@ export const useListController = ( props: ListControllerProps = {} ): ListControllerResult => { const { + disableAuthentication, exporter = defaultExporter, filterDefaultValues, sort = defaultSort, @@ -52,6 +54,7 @@ export const useListController = ( debounce = 500, disableSyncWithLocation, } = props; + useAuthenticated({ enabled: !disableAuthentication }); const resource = useResourceContext(props); const { hasCreate } = useResourceDefinition(props); @@ -181,6 +184,7 @@ export const useListController = ( }; export interface ListControllerProps { + disableAuthentication?: boolean; // the props you can change filter?: FilterPayload; filters?: ReactElement | ReactElement[]; diff --git a/packages/ra-core/src/controller/show/useShowController.ts b/packages/ra-core/src/controller/show/useShowController.ts index da0fff8f12f..c36d7bc10c6 100644 --- a/packages/ra-core/src/controller/show/useShowController.ts +++ b/packages/ra-core/src/controller/show/useShowController.ts @@ -1,6 +1,7 @@ import { useParams } from 'react-router-dom'; import { UseQueryOptions } from 'react-query'; +import { useAuthenticated } from '../../auth'; import useVersion from '../useVersion'; import { Record, Identifier } from '../../types'; import { useGetOne, Refetch } from '../../dataProvider'; @@ -43,7 +44,10 @@ import { useResourceContext, useGetResourceLabel } from '../../core'; export const useShowController = ( props: ShowControllerProps = {} ): ShowControllerResult => { - const { id: propsId, queryOptions = {} } = props; + const { disableAuthentication, id: propsId, queryOptions = {} } = props; + + useAuthenticated({ enabled: !disableAuthentication }); + const resource = useResourceContext(props); const translate = useTranslate(); const notify = useNotify(); @@ -94,6 +98,7 @@ export const useShowController = ( }; export interface ShowControllerProps { + disableAuthentication?: boolean; id?: Identifier; queryOptions?: UseQueryOptions; resource?: string; diff --git a/packages/ra-core/src/core/Resource.spec.tsx b/packages/ra-core/src/core/Resource.spec.tsx index 3b8f21baaf6..20e3b7c786f 100644 --- a/packages/ra-core/src/core/Resource.spec.tsx +++ b/packages/ra-core/src/core/Resource.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import expect from 'expect'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import CoreAdminContext from './CoreAdminContext'; @@ -46,36 +46,4 @@ describe('', () => { history.push('/create'); expect(screen.getByText('PostCreate')).not.toBeNull(); }); - it('injects permissions to the resource routes', async () => { - const history = createMemoryHistory(); - const authProvider = { - login: jest.fn().mockResolvedValue(''), - logout: jest.fn().mockResolvedValue(''), - checkAuth: jest.fn().mockResolvedValue(''), - checkError: jest.fn().mockResolvedValue(''), - getPermissions: jest.fn().mockResolvedValue('admin'), - }; - - const { getByText } = render( - - ( - Permissions: {permissions} - )} - /> - - ); - // Resource does not declare a route matching its name, it only renders its child routes - // so we don't need to navigate to a path matching its name - history.push('/'); - await waitFor(() => { - expect(getByText('Permissions: admin')).not.toBeNull(); - }); - }); }); diff --git a/packages/ra-core/src/core/Resource.tsx b/packages/ra-core/src/core/Resource.tsx index e43b85f3756..715525df657 100644 --- a/packages/ra-core/src/core/Resource.tsx +++ b/packages/ra-core/src/core/Resource.tsx @@ -1,45 +1,20 @@ import * as React from 'react'; -import { useMemo } from 'react'; import { Route, Routes } from 'react-router-dom'; -import WithPermissions from '../auth/WithPermissions'; import { ResourceProps } from '../types'; import { ResourceContextProvider } from './ResourceContextProvider'; export const Resource = (props: ResourceProps) => { - const { name, list, create, edit, show } = props; + const { create: Create, edit: Edit, list: List, name, show: Show } = props; - // match tends to change even on the same route ; using memo to avoid an extra render - return useMemo(() => { - return ( - - - {create && ( - } - /> - )} - {show && ( - } - /> - )} - {edit && ( - } - /> - )} - {list && ( - } - /> - )} - - - ); - }, [name, create, edit, list, show]); + return ( + + + {Create && } />} + {Show && } />} + {Edit && } />} + {List && } />} + + + ); }; diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx index a9304c0e565..d107050228e 100644 --- a/packages/ra-ui-materialui/src/detail/Create.tsx +++ b/packages/ra-ui-materialui/src/detail/Create.tsx @@ -3,6 +3,7 @@ import { ReactElement } from 'react'; import PropTypes from 'prop-types'; import { CreateContextProvider, + Record, ResourceContextProvider, useCheckMinimumRequiredProps, useCreateController, @@ -54,11 +55,11 @@ import { CreateView } from './CreateView'; * ); * export default App; */ -export const Create = ( - props: CreateProps & { children: ReactElement } +export const Create = ( + props: CreateProps & { children: ReactElement } ): ReactElement => { useCheckMinimumRequiredProps('Create', ['children'], props); - const controllerProps = useCreateController(props); + const controllerProps = useCreateController(props); const body = ( diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx index fc67475775d..fbcf445387f 100644 --- a/packages/ra-ui-materialui/src/detail/CreateView.tsx +++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx @@ -98,7 +98,11 @@ CreateView.propTypes = { hasList: PropTypes.bool, hasShow: PropTypes.bool, record: PropTypes.object, - redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + redirect: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + PropTypes.func, + ]), resource: PropTypes.string, save: PropTypes.func, title: PropTypes.node, diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx index cb1ab4dc817..1bfd881d955 100644 --- a/packages/ra-ui-materialui/src/detail/EditView.tsx +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -125,7 +125,11 @@ EditView.propTypes = { hasShow: PropTypes.bool, mutationMode: PropTypes.oneOf(['pessimistic', 'optimistic', 'undoable']), record: PropTypes.object, - redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + redirect: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + PropTypes.func, + ]), resource: PropTypes.string, save: PropTypes.func, title: PropTypes.node, diff --git a/packages/ra-ui-materialui/src/types.ts b/packages/ra-ui-materialui/src/types.ts index 7b506a999d2..0aab3499051 100644 --- a/packages/ra-ui-materialui/src/types.ts +++ b/packages/ra-ui-materialui/src/types.ts @@ -9,6 +9,7 @@ import { MutationMode, OnSuccess, OnFailure, + TransformData, UpdateParams, } from 'ra-core'; import { UseQueryOptions, UseMutationOptions } from 'react-query'; @@ -48,21 +49,21 @@ export interface EditProps { UpdateParams >; resource?: string; - transform?: (data: RaRecord) => RaRecord | Promise; + transform?: TransformData; title?: string | ReactElement; } -export interface CreateProps { +export interface CreateProps { actions?: ReactElement | false; aside?: ReactElement; classes?: any; className?: string; component?: ElementType; - record?: Partial; + record?: Partial; resource?: string; onSuccess?: OnSuccess; onFailure?: OnFailure; - transform?: (data: RaRecord) => RaRecord | Promise; + transform?: TransformData; title?: string | ReactElement; } diff --git a/yarn.lock b/yarn.lock index 75e33cce15f..836cdc80a8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11619,16 +11619,16 @@ eslint-plugin-prettier@^3.1.4: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.0.tgz#6323fbd5e650e84b2987ba76370523a60f4e7925" - integrity sha512-36zilUcDwDReiORXmcmTc6rRumu9JIM3WjSvV0nclHoUQ0CNrX866EwONvLR/UqaeqFutbAnVu8PEmctdo2SRQ== - eslint-plugin-react-hooks@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== +eslint-plugin-react-hooks@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" + integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + eslint-plugin-react@^7.20.6: version "7.20.6" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60"