diff --git a/packages/ra-core/src/controller/RecordContext.tsx b/packages/ra-core/src/controller/RecordContext.tsx new file mode 100644 index 00000000000..c87ef4e1471 --- /dev/null +++ b/packages/ra-core/src/controller/RecordContext.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { createContext, ReactNode, useContext, useMemo } from 'react'; +import pick from 'lodash/pick'; +import { Record } from '../types'; + +/** + * Context to store the current record. + * + * Use the useRecordContext() hook to read the context. That's what the Edit and Show components do in react-admin. + * + * @example + * + * import { useEditController, EditContext } from 'ra-core'; + * + * const Edit = props => { + * const { record }= useEditController(props); + * return ( + * + * ... + * + * ); + * }; + */ +export const RecordContext = createContext>( + undefined +); + +export const RecordContextProvider = ({ + children, + value, +}: RecordContextOptions) => ( + {children} +); +RecordContext.displayName = 'RecordContext'; + +export const usePickRecordContext = < + RecordType extends Record | Omit = Record +>( + context: RecordType +) => { + const value = useMemo(() => pick(context, ['record']), [context.record]); // eslint-disable-line + return value; +}; + +/** + * Hook to read the record from a RecordContext. + * + * Must be used within a such as provided by the + * (e.g. as a descendent of or ) or within a + * (e.g. as a descendent of or ) + * + * @returns {Record} The record context + */ +export const useRecordContext = < + RecordType extends Record | Omit = Record +>( + props: RecordType +) => { + // Can't find a way to specify the RecordType when CreateContext is declared + // @ts-ignore + const context = useContext(RecordContext); + + if (!context) { + // As the record could very well be undefined because not yet loaded + // We don't display a deprecation warning yet + // @deprecated - to be removed in 4.0 + return props; + } + + return context; +}; + +export interface RecordContextOptions< + RecordType extends Record | Omit = Record +> { + children: ReactNode; + value?: RecordType; +} diff --git a/packages/ra-core/src/controller/details/CreateBase.tsx b/packages/ra-core/src/controller/details/CreateBase.tsx new file mode 100644 index 00000000000..dd8e7ff15ad --- /dev/null +++ b/packages/ra-core/src/controller/details/CreateBase.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { useCreateController } from './useCreateController'; +import { CreateContextProvider } from './CreateContextProvider'; + +/** + * Call useCreateController and put the value in a CreateContext + * + * Base class for components, without UI. + * + * Accepts any props accepted by useCreateController: + * - id: The record identifier + * - resource: The resource + * + * @example // Custom edit layout + * + * const PostCreate = props => ( + * + * + * + * + * ... + * + * + * + * Create instructions... + * + * + *
+ * Post related links... + *
+ *
+ * ); + */ +export const CreateBase = ({ children, ...props }) => ( + + {children} + +); diff --git a/packages/ra-core/src/controller/details/CreateContext.tsx b/packages/ra-core/src/controller/details/CreateContext.tsx new file mode 100644 index 00000000000..56578ebf8d6 --- /dev/null +++ b/packages/ra-core/src/controller/details/CreateContext.tsx @@ -0,0 +1,39 @@ +import { createContext } from 'react'; +import { CreateControllerProps } from './useCreateController'; + +/** + * Context to store the result of the useCreateController() hook. + * + * Use the useCreateContext() hook to read the context. That's what the Create components do in react-admin. + * + * @example + * + * import { useCreateController, CreateContextProvider } from 'ra-core'; + * + * const Create = props => { + * const controllerProps = useCreateController(props); + * return ( + * + * ... + * + * ); + * }; + */ +export const CreateContext = createContext({ + basePath: null, + record: null, + defaultTitle: null, + loaded: null, + loading: null, + redirect: null, + setOnFailure: null, + setOnSuccess: null, + setTransform: null, + resource: null, + save: null, + saving: null, + successMessage: null, + version: null, +}); + +CreateContext.displayName = 'CreateContext'; diff --git a/packages/ra-core/src/controller/details/CreateContextProvider.tsx b/packages/ra-core/src/controller/details/CreateContextProvider.tsx new file mode 100644 index 00000000000..e85b01e7044 --- /dev/null +++ b/packages/ra-core/src/controller/details/CreateContextProvider.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { ReactElement } from 'react'; +import { RecordContextProvider, usePickRecordContext } from '../RecordContext'; +import { CreateContext } from './CreateContext'; +import { CreateControllerProps } from './useCreateController'; +import { SaveContextProvider, usePickSaveContext } from './SaveContext'; + +/** + * Create a Create Context. + * + * @example + * + * const MyCreate = (props) => { + * const controllerProps = useCreateController(props); + * return ( + * + * + * + * ); + * }; + * + * const MyCreateView = () => { + * const { record } = useRecordContext(); + * // or, to rerender only when the save operation change but not data + * const { saving } = useCreateContext(); + * } + * + * @see CreateContext + * @see RecordContext + */ +export const CreateContextProvider = ({ + children, + value, +}: { + children: ReactElement; + value: CreateControllerProps; +}) => ( + + + + {children} + + + +); diff --git a/packages/ra-core/src/controller/CreateController.tsx b/packages/ra-core/src/controller/details/CreateController.tsx similarity index 80% rename from packages/ra-core/src/controller/CreateController.tsx rename to packages/ra-core/src/controller/details/CreateController.tsx index 592c13a45dd..150a8d6ba5a 100644 --- a/packages/ra-core/src/controller/CreateController.tsx +++ b/packages/ra-core/src/controller/details/CreateController.tsx @@ -1,6 +1,7 @@ -import { Translate } from '../types'; -import { useTranslate } from '../i18n'; -import useCreateController, { +import { Translate } from '../../types'; +import { useTranslate } from '../../i18n'; +import { + useCreateController, CreateProps, CreateControllerProps, } from './useCreateController'; @@ -26,10 +27,8 @@ interface Props extends CreateProps { * * ); */ -const CreateController = ({ children, ...props }: Props) => { +export const CreateController = ({ children, ...props }: Props) => { const controllerProps = useCreateController(props); const translate = useTranslate(); // injected for backwards compatibility return children({ translate, ...controllerProps }); }; - -export default CreateController; diff --git a/packages/ra-core/src/controller/details/EditBase.tsx b/packages/ra-core/src/controller/details/EditBase.tsx new file mode 100644 index 00000000000..f79a81630a9 --- /dev/null +++ b/packages/ra-core/src/controller/details/EditBase.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { useEditController } from './useEditController'; +import { EditContextProvider } from './EditContextProvider'; + +/** + * Call useEditController and put the value in a EditContext + * + * Base class for components, without UI. + * + * Accepts any props accepted by useEditController: + * - id: The record identifier + * - resource: The resource + * + * @example // Custom edit layout + * + * const PostEdit = props => ( + * + * + * + * + * ... + * + * + * + * Edit instructions... + * + * + *
+ * Post related links... + *
+ * + * ); + */ +export const EditBase = ({ children, ...props }) => ( + + {children} + +); diff --git a/packages/ra-core/src/controller/details/EditContext.tsx b/packages/ra-core/src/controller/details/EditContext.tsx new file mode 100644 index 00000000000..ede20ef1306 --- /dev/null +++ b/packages/ra-core/src/controller/details/EditContext.tsx @@ -0,0 +1,39 @@ +import { createContext } from 'react'; +import { EditControllerProps } from './useEditController'; + +/** + * Context to store the result of the useEditController() hook. + * + * Use the useEditContext() hook to read the context. That's what the Edit components do in react-admin. + * + * @example + * + * import { useEditController, EditContextProvider } from 'ra-core'; + * + * const Edit = props => { + * const controllerProps = useEditController(props); + * return ( + * + * ... + * + * ); + * }; + */ +export const EditContext = createContext({ + basePath: null, + record: null, + defaultTitle: null, + loaded: null, + loading: null, + redirect: null, + setOnFailure: null, + setOnSuccess: null, + setTransform: null, + resource: null, + save: null, + saving: null, + successMessage: null, + version: null, +}); + +EditContext.displayName = 'EditContext'; diff --git a/packages/ra-core/src/controller/details/EditContextProvider.tsx b/packages/ra-core/src/controller/details/EditContextProvider.tsx new file mode 100644 index 00000000000..94e4cd14a1f --- /dev/null +++ b/packages/ra-core/src/controller/details/EditContextProvider.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { ReactElement } from 'react'; +import { RecordContextProvider, usePickRecordContext } from '../RecordContext'; +import { EditContext } from './EditContext'; +import { EditControllerProps } from './useEditController'; +import { SaveContextProvider, usePickSaveContext } from './SaveContext'; + +/** + * Create an Edit Context. + * + * @example + * + * const MyEdit = (props) => { + * const controllerProps = useEditController(props); + * return ( + * + * + * + * ); + * }; + * + * const MyEditView = () => { + * const { record } = useRecordContext(); + * // or, to rerender only when the save operation change but not data + * const { saving } = useEditContext(); + * } + * + * @see EditContext + * @see RecordContext + */ +export const EditContextProvider = ({ + children, + value, +}: { + children: ReactElement; + value: EditControllerProps; +}) => ( + + + + {children} + + + +); diff --git a/packages/ra-core/src/controller/EditController.tsx b/packages/ra-core/src/controller/details/EditController.tsx similarity index 79% rename from packages/ra-core/src/controller/EditController.tsx rename to packages/ra-core/src/controller/details/EditController.tsx index 8b36cdd9be0..afa6ac27efd 100644 --- a/packages/ra-core/src/controller/EditController.tsx +++ b/packages/ra-core/src/controller/details/EditController.tsx @@ -1,6 +1,7 @@ -import { Translate } from '../types'; -import { useTranslate } from '../i18n'; -import useEditController, { +import { Translate } from '../../types'; +import { useTranslate } from '../../i18n'; +import { + useEditController, EditProps, EditControllerProps, } from './useEditController'; @@ -26,10 +27,8 @@ interface Props extends EditProps { * * ); */ -const EditController = ({ children, ...props }: Props) => { +export const EditController = ({ children, ...props }: Props) => { const controllerProps = useEditController(props); const translate = useTranslate(); // injected for backwards compatibility return children({ translate, ...controllerProps }); }; - -export default EditController; diff --git a/packages/ra-core/src/controller/details/SaveContext.tsx b/packages/ra-core/src/controller/details/SaveContext.tsx new file mode 100644 index 00000000000..e6064f1ac32 --- /dev/null +++ b/packages/ra-core/src/controller/details/SaveContext.tsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { createContext, useContext, useMemo } from 'react'; +import pick from 'lodash/pick'; + +import { RedirectionSideEffect } from '../../sideEffect'; +import { Record } from '../../types'; +import { + OnFailure, + OnSuccess, + SideEffectContextValue, + TransformData, +} from '../saveModifiers'; + +interface SaveContextValue extends SideEffectContextValue { + save?: ( + record: Partial, + redirect: RedirectionSideEffect, + callbacks?: { + onSuccess?: OnSuccess; + onFailure?: OnFailure; + transform?: TransformData; + } + ) => void; + saving?: boolean; +} + +export const SaveContext = createContext({}); + +export const SaveContextProvider = ({ children, value }) => ( + {children} +); + +/** + * Get the save() function and its status + * + * Used in forms. + * + * @example + * + * const { + * save, + * saving + * } = useSaveContext(); + */ +export const useSaveContext = < + PropsType extends SaveContextValue = SaveContextValue +>( + props?: PropsType +) => { + const context = useContext(SaveContext); + + if (!context.save || !context.setOnFailure) { + /** + * The element isn't inside a + * To avoid breakage in that case, fallback to props + * + * @deprecated - to be removed in 4.0 + */ + if (process.env.NODE_ENV !== 'production') { + console.log( + "Edit or Create child components must be used inside a . Relying on props rather than context to get persistance related data and callbacks is deprecated and won't be supported in the next major version of react-admin." + ); + } + + return props; + } + + return context; +}; + +export const usePickSaveContext = < + ContextType extends SaveContextValue = SaveContextValue +>( + context: ContextType +): SaveContextValue => { + const value = useMemo( + () => + pick(context, [ + 'save', + 'saving', + 'setOnFailure', + 'setOnSuccess', + 'setTransform', + ]), + /* eslint-disable react-hooks/exhaustive-deps */ + [ + context.save, + context.saving, + context.setOnFailure, + context.setOnSuccess, + context.setTransform, + ] + /* eslint-enable react-hooks/exhaustive-deps */ + ); + + return value; +}; diff --git a/packages/ra-core/src/controller/details/ShowBase.tsx b/packages/ra-core/src/controller/details/ShowBase.tsx new file mode 100644 index 00000000000..bfbb30a8ccf --- /dev/null +++ b/packages/ra-core/src/controller/details/ShowBase.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { useShowController } from './useShowController'; +import { ShowContextProvider } from './ShowContextProvider'; + +/** + * Call useShowController and put the value in a ShowContext + * + * Base class for components, without UI. + * + * Accepts any props accepted by useShowController: + * - id: The record identifier + * - resource: The resource + * + * @example // Custom edit layout + * + * const PostShow = props => ( + * + * + * + * + * ... + * + * + * + * Show instructions... + * + * + *
+ * Post related links... + *
+ * + * ); + */ +export const ShowBase = ({ children, ...props }) => ( + + {children} + +); diff --git a/packages/ra-core/src/controller/details/ShowContext.tsx b/packages/ra-core/src/controller/details/ShowContext.tsx new file mode 100644 index 00000000000..c263407a5cb --- /dev/null +++ b/packages/ra-core/src/controller/details/ShowContext.tsx @@ -0,0 +1,32 @@ +import { createContext } from 'react'; +import { ShowControllerProps } from './useShowController'; + +/** + * Context to store the result of the useShowController() hook. + * + * Use the useShowContext() hook to read the context. That's what the Show components do in react-admin. + * + * @example + * + * import { useShowController, ShowContextProvider } from 'ra-core'; + * + * const Show = props => { + * const controllerProps = useShowController(props); + * return ( + * + * ... + * + * ); + * }; + */ +export const ShowContext = createContext({ + basePath: null, + record: null, + defaultTitle: null, + loaded: null, + loading: null, + resource: null, + version: null, +}); + +ShowContext.displayName = 'ShowContext'; diff --git a/packages/ra-core/src/controller/details/ShowContextProvider.tsx b/packages/ra-core/src/controller/details/ShowContextProvider.tsx new file mode 100644 index 00000000000..871b4683243 --- /dev/null +++ b/packages/ra-core/src/controller/details/ShowContextProvider.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { ReactElement } from 'react'; +import { RecordContextProvider, usePickRecordContext } from '../RecordContext'; +import { ShowContext } from './ShowContext'; +import { ShowControllerProps } from './useShowController'; + +/** + * Create a Show Context. + * + * @example + * + * const MyShow = (props) => { + * const controllerProps = useShowController(props); + * return ( + * + * + * + * ); + * }; + * + * const MyShowView = () => { + * const { record } = useRecordContext(); + * } + * + * @see ShowContext + * @see RecordContext + */ +export const ShowContextProvider = ({ + children, + value, +}: { + children: ReactElement; + value: ShowControllerProps; +}) => ( + + + {children} + + +); diff --git a/packages/ra-core/src/controller/ShowController.tsx b/packages/ra-core/src/controller/details/ShowController.tsx similarity index 79% rename from packages/ra-core/src/controller/ShowController.tsx rename to packages/ra-core/src/controller/details/ShowController.tsx index ffcd0c462a6..83d3fd35def 100644 --- a/packages/ra-core/src/controller/ShowController.tsx +++ b/packages/ra-core/src/controller/details/ShowController.tsx @@ -1,9 +1,10 @@ -import useShowController, { +import { + useShowController, ShowProps, ShowControllerProps, } from './useShowController'; -import { Translate } from '../types'; -import { useTranslate } from '../i18n'; +import { Translate } from '../../types'; +import { useTranslate } from '../../i18n'; interface ShowControllerComponentProps extends ShowControllerProps { translate: Translate; @@ -26,10 +27,8 @@ interface Props extends ShowProps { * * ); */ -const ShowController = ({ children, ...props }: Props) => { +export const ShowController = ({ children, ...props }: Props) => { const controllerProps = useShowController(props); const translate = useTranslate(); // injected for backwards compatibility return children({ translate, ...controllerProps }); }; - -export default ShowController; diff --git a/packages/ra-core/src/controller/details/index.ts b/packages/ra-core/src/controller/details/index.ts new file mode 100644 index 00000000000..617ca37a3c5 --- /dev/null +++ b/packages/ra-core/src/controller/details/index.ts @@ -0,0 +1,29 @@ +import { + CreateControllerProps, + useCreateController, +} from './useCreateController'; +import { EditControllerProps, useEditController } from './useEditController'; +import { ShowControllerProps, useShowController } from './useShowController'; + +export * from './CreateBase'; +export * from './CreateContext'; +export * from './CreateContextProvider'; +export * from './CreateController'; +export * from './EditBase'; +export * from './EditContext'; +export * from './EditContextProvider'; +export * from './EditController'; +export * from './ShowBase'; +export * from './ShowContext'; +export * from './ShowContextProvider'; +export * from './ShowController'; +export * from './SaveContext'; +export * from './useCreateContext'; +export * from './useEditContext'; +export * from './useShowContext'; + +// We don't want to export CreateProps, EditProps and ShowProps as they should +// not be used outside of ra-core would conflict with ra-ui-materialui types, +// hence the named imports/exports +export type { CreateControllerProps, EditControllerProps, ShowControllerProps }; +export { useCreateController, useEditController, useShowController }; diff --git a/packages/ra-core/src/controller/details/useCreateContext.tsx b/packages/ra-core/src/controller/details/useCreateContext.tsx new file mode 100644 index 00000000000..afef3680630 --- /dev/null +++ b/packages/ra-core/src/controller/details/useCreateContext.tsx @@ -0,0 +1,34 @@ +import { useContext } from 'react'; +import { Record } from '../../types'; +import { CreateContext } from './CreateContext'; +import { CreateControllerProps } from './useCreateController'; + +export const useCreateContext = < + RecordType extends Omit = Omit +>( + props?: Partial> +): Partial> => { + const context = useContext>( + // Can't find a way to specify the RecordType when CreateContext is declared + // @ts-ignore + CreateContext + ); + + if (!context.resource) { + /** + * The element isn't inside a + * To avoid breakage in that case, fallback to props + * + * @deprecated - to be removed in 4.0 + */ + if (process.env.NODE_ENV !== 'production') { + console.log( + "Create components must be used inside a . Relying on props rather than context to get Create data and callbacks is deprecated and won't be supported in the next major version of react-admin." + ); + } + + return props; + } + + return context; +}; diff --git a/packages/ra-core/src/controller/useCreateController.spec.tsx b/packages/ra-core/src/controller/details/useCreateController.spec.tsx similarity index 98% rename from packages/ra-core/src/controller/useCreateController.spec.tsx rename to packages/ra-core/src/controller/details/useCreateController.spec.tsx index 3e4cf5884f3..fdb6ad6541f 100644 --- a/packages/ra-core/src/controller/useCreateController.spec.tsx +++ b/packages/ra-core/src/controller/details/useCreateController.spec.tsx @@ -3,10 +3,10 @@ import expect from 'expect'; import { act, cleanup } from '@testing-library/react'; import { getRecord } from './useCreateController'; -import CreateController from './CreateController'; -import renderWithRedux from '../util/renderWithRedux'; -import { DataProviderContext } from '../dataProvider'; -import { DataProvider } from '../types'; +import { CreateController } from './CreateController'; +import renderWithRedux from '../../util/renderWithRedux'; +import { DataProviderContext } from '../../dataProvider'; +import { DataProvider } from '../../types'; describe('useCreateController', () => { afterEach(cleanup); diff --git a/packages/ra-core/src/controller/useCreateController.ts b/packages/ra-core/src/controller/details/useCreateController.ts similarity index 86% rename from packages/ra-core/src/controller/useCreateController.ts rename to packages/ra-core/src/controller/details/useCreateController.ts index 7fd0edf530e..8884a137f84 100644 --- a/packages/ra-core/src/controller/useCreateController.ts +++ b/packages/ra-core/src/controller/details/useCreateController.ts @@ -5,9 +5,13 @@ import { parse } from 'query-string'; import { Location } from 'history'; import { match as Match, useLocation } from 'react-router-dom'; -import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; -import { useCreate } from '../dataProvider'; -import { useNotify, useRedirect, RedirectionSideEffect } from '../sideEffect'; +import { useCheckMinimumRequiredProps } from '../checkMinimumRequiredProps'; +import { useCreate } from '../../dataProvider'; +import { + useNotify, + useRedirect, + RedirectionSideEffect, +} from '../../sideEffect'; import { OnSuccess, SetOnSuccess, @@ -16,13 +20,13 @@ import { TransformData, SetTransformData, useSaveModifiers, -} from './saveModifiers'; -import { useTranslate } from '../i18n'; -import { useVersion } from '.'; -import { CRUD_CREATE } from '../actions'; -import { Record } from '../types'; +} from '../saveModifiers'; +import { useTranslate } from '../../i18n'; +import useVersion from '../useVersion'; +import { CRUD_CREATE } from '../../actions'; +import { Record } from '../../types'; -export interface CreateProps { +export interface CreateProps = Record> { basePath?: string; hasCreate?: boolean; hasEdit?: boolean; @@ -30,7 +34,7 @@ export interface CreateProps { hasShow?: boolean; location?: Location; match?: Match; - record?: Partial; + record?: Partial; resource?: string; onSuccess?: OnSuccess; onFailure?: OnFailure; @@ -38,11 +42,20 @@ export interface CreateProps { successMessage?: string; } -export interface CreateControllerProps { +export interface CreateControllerProps< + RecordType extends Omit = Record +> { + basePath?: string; + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + data?: RecordType; + defaultTitle: string; loading: boolean; loaded: boolean; - saving: boolean; - defaultTitle: string; + hasCreate?: boolean; + hasEdit?: boolean; + hasList?: boolean; + hasShow?: boolean; save: ( record: Partial, redirect: RedirectionSideEffect, @@ -52,13 +65,14 @@ export interface CreateControllerProps { transform?: TransformData; } ) => void; + saving: boolean; setOnSuccess: SetOnSuccess; setOnFailure: SetOnFailure; setTransform: SetTransformData; - resource: string; - basePath?: string; - record?: Partial; + successMessage?: string; + record?: Partial; redirect: RedirectionSideEffect; + resource: string; version: number; } @@ -79,7 +93,11 @@ export interface CreateControllerProps { * return ; * } */ -const useCreateController = (props: CreateProps): CreateControllerProps => { +export const useCreateController = < + RecordType extends Omit = Record +>( + props: CreateProps +): CreateControllerProps => { useCheckMinimumRequiredProps('Create', ['basePath', 'resource'], props); const { basePath, @@ -211,8 +229,6 @@ const useCreateController = (props: CreateProps): CreateControllerProps => { }; }; -export default useCreateController; - export const getRecord = ({ state, search }, record: any = {}) => { if (state && state.record) { return state.record; diff --git a/packages/ra-core/src/controller/details/useEditContext.tsx b/packages/ra-core/src/controller/details/useEditContext.tsx new file mode 100644 index 00000000000..73493c7c440 --- /dev/null +++ b/packages/ra-core/src/controller/details/useEditContext.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; +import { Record } from '../../types'; +import { EditContext } from './EditContext'; +import { EditControllerProps } from './useEditController'; + +export const useEditContext = ( + props?: Partial> +): Partial> => { + // Can't find a way to specify the RecordType when CreateContext is declared + // @ts-ignore + const context = useContext>(EditContext); + + if (!context.resource) { + /** + * The element isn't inside a + * To avoid breakage in that case, fallback to props + * + * @deprecated - to be removed in 4.0 + */ + if (process.env.NODE_ENV !== 'production') { + console.log( + "Edit components must be used inside a . Relying on props rather than context to get Edit data and callbacks is deprecated and won't be supported in the next major version of react-admin." + ); + } + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + return { + ...props, + record: props.record || props.data, + data: props.record || props.data, + }; + } + + return context; +}; diff --git a/packages/ra-core/src/controller/useEditController.spec.tsx b/packages/ra-core/src/controller/details/useEditController.spec.tsx similarity index 98% rename from packages/ra-core/src/controller/useEditController.spec.tsx rename to packages/ra-core/src/controller/details/useEditController.spec.tsx index 88354f9b78f..ae328c349c4 100644 --- a/packages/ra-core/src/controller/useEditController.spec.tsx +++ b/packages/ra-core/src/controller/details/useEditController.spec.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import expect from 'expect'; import { act, cleanup, wait } from '@testing-library/react'; -import EditController from './EditController'; -import renderWithRedux from '../util/renderWithRedux'; -import { DataProviderContext } from '../dataProvider'; -import { DataProvider } from '../types'; +import { EditController } from './EditController'; +import renderWithRedux from '../../util/renderWithRedux'; +import { DataProviderContext } from '../../dataProvider'; +import { DataProvider } from '../../types'; describe('useEditController', () => { afterEach(cleanup); diff --git a/packages/ra-core/src/controller/useEditController.ts b/packages/ra-core/src/controller/details/useEditController.ts similarity index 88% rename from packages/ra-core/src/controller/useEditController.ts rename to packages/ra-core/src/controller/details/useEditController.ts index f54e2ba94f0..7c0ea914a5d 100644 --- a/packages/ra-core/src/controller/useEditController.ts +++ b/packages/ra-core/src/controller/details/useEditController.ts @@ -1,15 +1,18 @@ import { useCallback } from 'react'; import inflection from 'inflection'; -import useVersion from './useVersion'; -import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; -import { Record, Identifier } from '../types'; +import useVersion from '../useVersion'; +import { useCheckMinimumRequiredProps } from '../checkMinimumRequiredProps'; +import { Record, Identifier } from '../../types'; import { useNotify, useRedirect, useRefresh, RedirectionSideEffect, -} from '../sideEffect'; +} from '../../sideEffect'; +import { useGetOne, useUpdate } from '../../dataProvider'; +import { useTranslate } from '../../i18n'; +import { CRUD_GET_ONE, CRUD_UPDATE } from '../../actions'; import { OnSuccess, SetOnSuccess, @@ -18,10 +21,7 @@ import { TransformData, SetTransformData, useSaveModifiers, -} from './saveModifiers'; -import { useGetOne, useUpdate } from '../dataProvider'; -import { useTranslate } from '../i18n'; -import { CRUD_GET_ONE, CRUD_UPDATE } from '../actions'; +} from '../saveModifiers'; export interface EditProps { basePath?: string; @@ -39,10 +39,17 @@ export interface EditProps { } export interface EditControllerProps { + basePath?: string; + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + data?: RecordType; + defaultTitle: string; + hasCreate?: boolean; + hasEdit?: boolean; + hasShow?: boolean; + hasList?: boolean; loading: boolean; loaded: boolean; - saving: boolean; - defaultTitle: string; save: ( data: Partial, redirect?: RedirectionSideEffect, @@ -52,15 +59,15 @@ export interface EditControllerProps { transform?: TransformData; } ) => void; + saving: boolean; setOnSuccess: SetOnSuccess; setOnFailure: SetOnFailure; setTransform: SetTransformData; - resource: string; - basePath?: string; + successMessage?: string; record?: RecordType; redirect: RedirectionSideEffect; + resource: string; version: number; - successMessage?: string; } /** @@ -80,12 +87,16 @@ export interface EditControllerProps { * return ; * } */ -const useEditController = ( +export const useEditController = ( props: EditProps ): EditControllerProps => { useCheckMinimumRequiredProps('Edit', ['basePath', 'resource'], props); const { basePath, + hasCreate, + hasEdit, + hasList, + hasShow, id, resource, successMessage, @@ -222,6 +233,10 @@ const useEditController = ( loaded, saving, defaultTitle, + hasCreate, + hasEdit, + hasList, + hasShow, save, setOnSuccess, setOnFailure, @@ -234,6 +249,4 @@ const useEditController = ( }; }; -export default useEditController; - const DefaultRedirect = 'list'; diff --git a/packages/ra-core/src/controller/details/useShowContext.tsx b/packages/ra-core/src/controller/details/useShowContext.tsx new file mode 100644 index 00000000000..cbd5badfaef --- /dev/null +++ b/packages/ra-core/src/controller/details/useShowContext.tsx @@ -0,0 +1,35 @@ +import { useContext } from 'react'; +import { Record } from '../../types'; +import { ShowContext } from './ShowContext'; +import { ShowControllerProps } from './useShowController'; + +export const useShowContext = ( + props?: Partial> +): Partial> => { + // Can't find a way to specify the RecordType when CreateContext is declared + // @ts-ignore + const context = useContext>(ShowContext); + + if (!context.resource) { + /** + * The element isn't inside a + * To avoid breakage in that case, fallback to props + * + * @deprecated - to be removed in 4.0 + */ + if (process.env.NODE_ENV !== 'production') { + console.log( + "Show components must be used inside a . Relying on props rather than context to get Show data and callbacks is deprecated and won't be supported in the next major version of react-admin." + ); + } + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + return { + ...props, + record: props.record || props.data, + data: props.record || props.data, + }; + } + + return context; +}; diff --git a/packages/ra-core/src/controller/useShowController.ts b/packages/ra-core/src/controller/details/useShowController.ts similarity index 68% rename from packages/ra-core/src/controller/useShowController.ts rename to packages/ra-core/src/controller/details/useShowController.ts index 5c7912c1a5e..927051fd7e2 100644 --- a/packages/ra-core/src/controller/useShowController.ts +++ b/packages/ra-core/src/controller/details/useShowController.ts @@ -1,17 +1,17 @@ import inflection from 'inflection'; -import useVersion from './useVersion'; -import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; -import { Record, Identifier } from '../types'; -import { useGetOne } from '../dataProvider'; -import { useTranslate } from '../i18n'; -import { useNotify, useRedirect, useRefresh } from '../sideEffect'; -import { CRUD_GET_ONE } from '../actions'; +import useVersion from '../useVersion'; +import { useCheckMinimumRequiredProps } from '../checkMinimumRequiredProps'; +import { Record, Identifier } from '../../types'; +import { useGetOne } from '../../dataProvider'; +import { useTranslate } from '../../i18n'; +import { useNotify, useRedirect, useRefresh } from '../../sideEffect'; +import { CRUD_GET_ONE } from '../../actions'; export interface ShowProps { basePath?: string; hasCreate?: boolean; - hasedit?: boolean; + hasEdit?: boolean; hasShow?: boolean; hasList?: boolean; id?: Identifier; @@ -20,11 +20,18 @@ export interface ShowProps { } export interface ShowControllerProps { + basePath?: string; + defaultTitle: string; + // Necessary for actions (EditActions) which expect a data prop containing the record + // @deprecated - to be removed in 4.0d + data?: RecordType; loading: boolean; loaded: boolean; - defaultTitle: string; + hasCreate?: boolean; + hasEdit?: boolean; + hasList?: boolean; + hasShow?: boolean; resource: string; - basePath?: string; record?: RecordType; version: number; } @@ -46,11 +53,19 @@ export interface ShowControllerProps { * return ; * } */ -const useShowController = ( +export const useShowController = ( props: ShowProps ): ShowControllerProps => { useCheckMinimumRequiredProps('Show', ['basePath', 'resource'], props); - const { basePath, id, resource } = props; + const { + basePath, + hasCreate, + hasEdit, + hasList, + hasShow, + id, + resource, + } = props; const translate = useTranslate(); const notify = useNotify(); const redirect = useRedirect(); @@ -86,8 +101,10 @@ const useShowController = ( resource, basePath, record, + hasCreate, + hasEdit, + hasList, + hasShow, version, }; }; - -export default useShowController; diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts index efd4e8b8ad4..56432180237 100644 --- a/packages/ra-core/src/controller/index.ts +++ b/packages/ra-core/src/controller/index.ts @@ -1,12 +1,9 @@ -import CreateController from './CreateController'; -import EditController from './EditController'; import ListController from './ListController'; import ListContext from './ListContext'; import ListFilterContext from './ListFilterContext'; import ListPaginationContext from './ListPaginationContext'; import ListSortContext from './ListSortContext'; import ListBase from './ListBase'; -import ShowController from './ShowController'; import useRecordSelection from './useRecordSelection'; import useVersion from './useVersion'; import useExpanded from './useExpanded'; @@ -19,11 +16,6 @@ import useListController, { ListControllerProps, } from './useListController'; import useListContext from './useListContext'; -import useEditController, { EditControllerProps } from './useEditController'; -import useCreateController, { - CreateControllerProps, -} from './useCreateController'; -import useShowController, { ShowControllerProps } from './useShowController'; import useReference, { UseReferenceProps } from './useReference'; import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; import useListParams from './useListParams'; @@ -35,19 +27,14 @@ import useListSortContext from './useListSortContext'; export type { ListControllerProps, - EditControllerProps, - CreateControllerProps, - ShowControllerProps, - UseReferenceProps, PaginationHookResult, SortProps, + UseReferenceProps, }; export { getListControllerProps, sanitizeListRestProps, - CreateController, - EditController, ListBase, ListController, ListContext, @@ -55,12 +42,8 @@ export { ListPaginationContext, ListSortContext, ListContextProvider, - ShowController, useCheckMinimumRequiredProps, useListController, - useEditController, - useCreateController, - useShowController, useRecordSelection, useVersion, useExpanded, @@ -79,4 +62,6 @@ export { export * from './field'; export * from './input'; export * from './button'; +export * from './details'; +export * from './RecordContext'; export * from './saveModifiers'; diff --git a/packages/ra-core/src/controller/saveModifiers.tsx b/packages/ra-core/src/controller/saveModifiers.tsx index 7e729ece0d6..6e055313aaa 100644 --- a/packages/ra-core/src/controller/saveModifiers.tsx +++ b/packages/ra-core/src/controller/saveModifiers.tsx @@ -1,6 +1,13 @@ +import * as React from 'react'; import { createContext, useRef } from 'react'; -export const SideEffectContext = createContext({}); +export const SideEffectContext = createContext({}); + +export const SideEffectContextProvider = ({ children, value }) => ( + + {children} + +); /** * Get modifiers for a save() function, and the way to update them. @@ -22,7 +29,7 @@ export const useSaveModifiers = ({ onSuccess, onFailure, transform, -}: SaveModifiers) => { +}: SideEffectContextOptions) => { const onSuccessRef = useRef(onSuccess); const setOnSuccess: SetOnSuccess = onSuccess => { onSuccessRef.current = response => { @@ -67,13 +74,13 @@ export type SetOnFailure = (onFailure: OnFailure) => void; export type TransformData = (data: any) => any | Promise; export type SetTransformData = (transform: TransformData) => void; -export interface SideEffectContextType { +export interface SideEffectContextValue { setOnSuccess?: SetOnSuccess; setOnFailure?: SetOnFailure; setTransform?: SetTransformData; } -export interface SaveModifiers { +export interface SideEffectContextOptions { onSuccess?: OnSuccess; onFailure?: OnFailure; transform?: TransformData; diff --git a/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx index 92e1900bcd7..a7ab42282ff 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx +++ b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx @@ -6,6 +6,7 @@ import { renderWithRedux, DataProviderContext, DataProvider, + SaveContextProvider, } from 'ra-core'; import { ThemeProvider } from '@material-ui/core'; import { createMuiTheme } from '@material-ui/core/styles'; @@ -36,13 +37,17 @@ const invalidButtonDomProps = { describe('', () => { afterEach(cleanup); + const saveContextValue = { save: jest.fn(), saving: false }; + it('should render as submit type with no DOM errors', () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); const { getByLabelText } = render( - + + + ); @@ -59,7 +64,9 @@ describe('', () => { const { getByLabelText } = render( - + + + ); @@ -69,7 +76,9 @@ describe('', () => { it('should render as submit type when submitOnEnter is true', () => { const { getByLabelText } = render( - + + + ); expect(getByLabelText('ra.action.save').getAttribute('type')).toEqual( @@ -80,7 +89,9 @@ describe('', () => { it('should render as button type when submitOnEnter is false', () => { const { getByLabelText } = render( - + + + ); @@ -93,10 +104,12 @@ describe('', () => { const onSubmit = jest.fn(); const { getByLabelText } = render( - + + + ); @@ -109,7 +122,9 @@ describe('', () => { const { getByLabelText } = render( - + + + ); @@ -126,10 +141,12 @@ describe('', () => { {({ store }) => { dispatchSpy = jest.spyOn(store, 'dispatch'); return ( - + + + ); }} @@ -152,8 +169,18 @@ describe('', () => { basePath: '', id: '123', resource: 'posts', - location: {}, - match: {}, + location: { + pathname: '/customers/123', + search: '', + state: {}, + hash: '', + }, + match: { + params: { id: 123 }, + isExact: true, + path: '/customers/123', + url: '/customers/123', + }, undoable: false, }; diff --git a/packages/ra-ui-materialui/src/button/SaveButton.tsx b/packages/ra-ui-materialui/src/button/SaveButton.tsx index 578ba702d51..1dc78c1a8f0 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.tsx +++ b/packages/ra-ui-materialui/src/button/SaveButton.tsx @@ -15,13 +15,13 @@ import { useTranslate, useNotify, RedirectionSideEffect, - SideEffectContext, OnSuccess, OnFailure, TransformData, Record, FormContext, HandleSubmitWithRedirect, + useSaveContext, } from 'ra-core'; import { sanitizeButtonRestProps } from './Button'; @@ -88,9 +88,7 @@ const SaveButton: FC = props => { const notify = useNotify(); const translate = useTranslate(); const { setOnSave } = useContext(FormContext); - const { setOnSuccess, setOnFailure, setTransform } = useContext( - SideEffectContext - ); + const { setOnSuccess, setOnFailure, setTransform } = useSaveContext(props); const handleClick = event => { // deprecated: use onSuccess and transform instead of onSave diff --git a/packages/ra-ui-materialui/src/detail/Create.spec.js b/packages/ra-ui-materialui/src/detail/Create.spec.js index 7f8ea47b963..b923dba0e36 100644 --- a/packages/ra-ui-materialui/src/detail/Create.spec.js +++ b/packages/ra-ui-materialui/src/detail/Create.spec.js @@ -3,7 +3,7 @@ import expect from 'expect'; import { cleanup } from '@testing-library/react'; import { renderWithRedux } from 'ra-core'; -import Create from './Create'; +import { Create } from './Create'; describe('', () => { afterEach(cleanup); diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx index 189b1bc22eb..9b3600bf130 100644 --- a/packages/ra-ui-materialui/src/detail/Create.tsx +++ b/packages/ra-ui-materialui/src/detail/Create.tsx @@ -1,18 +1,13 @@ import * as React from 'react'; -import { Children, cloneElement, useMemo } from 'react'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import { makeStyles } from '@material-ui/core/styles'; -import classnames from 'classnames'; import { + CreateContextProvider, useCheckMinimumRequiredProps, useCreateController, - SideEffectContext, - CreateControllerProps, } from 'ra-core'; -import TitleForRecord from '../layout/TitleForRecord'; import { CreateProps } from '../types'; +import { CreateView } from './CreateView'; /** * Page component for the Create view @@ -57,9 +52,15 @@ import { CreateProps } from '../types'; * ); * export default App; */ -const Create = (props: CreateProps) => ( - -); +export const Create = (props: CreateProps) => { + useCheckMinimumRequiredProps('Create', ['children'], props); + const controllerProps = useCreateController(props); + return ( + + + + ); +}; Create.propTypes = { actions: PropTypes.element, @@ -80,152 +81,4 @@ Create.propTypes = { transform: PropTypes.func, }; -export const CreateView = (props: CreateViewProps) => { - const { - actions, - aside, - basePath, - children, - classes: classesOverride, - className, - component: Content, - defaultTitle, - hasList, - hasShow, - record = {}, - redirect, - resource, - save, - setOnSuccess, - setOnFailure, - setTransform, - saving, - title, - version, - ...rest - } = props; - useCheckMinimumRequiredProps('Create', ['children'], props); - const classes = useStyles(props); - const sideEffectContextValue = useMemo( - () => ({ setOnSuccess, setOnFailure, setTransform }), - [setOnFailure, setOnSuccess, setTransform] - ); - return ( - -
- - {actions && - cloneElement(actions, { - basePath, - resource, - hasList, - // Ensure we don't override any user provided props - ...actions.props, - })} -
- - {cloneElement(Children.only(children), { - basePath, - record, - redirect: - typeof children.props.redirect === 'undefined' - ? redirect - : children.props.redirect, - resource, - save, - saving, - version, - })} - - {aside && - cloneElement(aside, { - basePath, - record, - resource, - save, - saving, - version, - })} -
-
-
- ); -}; - -interface CreateViewProps - extends CreateProps, - Omit {} - -CreateView.propTypes = { - actions: PropTypes.element, - aside: PropTypes.element, - basePath: PropTypes.string, - children: PropTypes.element, - classes: PropTypes.object, - className: PropTypes.string, - defaultTitle: PropTypes.any, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - record: PropTypes.object, - redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - resource: PropTypes.string, - save: PropTypes.func, - title: PropTypes.node, - onSuccess: PropTypes.func, - onFailure: PropTypes.func, - setOnSuccess: PropTypes.func, - setOnFailure: PropTypes.func, - setTransform: PropTypes.func, -}; - -CreateView.defaultProps = { - classes: {}, - component: Card, -}; - -const useStyles = makeStyles( - theme => ({ - root: {}, - main: { - display: 'flex', - }, - noActions: { - [theme.breakpoints.up('sm')]: { - marginTop: '1em', - }, - }, - card: { - flex: '1 1 auto', - }, - }), - { name: 'RaCreate' } -); - -const sanitizeRestProps = ({ - hasCreate = null, - hasEdit = null, - history = null, - loaded = null, - loading = null, - location = null, - match = null, - onFailure = null, - onSuccess = null, - options = null, - permissions = null, - transform = null, - ...rest -}) => rest; - export default Create; diff --git a/packages/ra-ui-materialui/src/detail/CreateActions.js b/packages/ra-ui-materialui/src/detail/CreateActions.js index ea492a0b107..836d31e9232 100644 --- a/packages/ra-ui-materialui/src/detail/CreateActions.js +++ b/packages/ra-ui-materialui/src/detail/CreateActions.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import TopToolbar from '../layout/TopToolbar'; import { ListButton } from '../button'; +import { useCreateContext } from 'ra-core'; const sanitizeRestProps = ({ basePath, @@ -37,11 +38,14 @@ const sanitizeRestProps = ({ *
* ); */ -const CreateActions = ({ basePath, className, hasList, ...rest }) => ( - - {hasList && } - -); +const CreateActions = ({ className, ...rest }) => { + const { basePath, hasList } = useCreateContext(rest); + return ( + + {hasList && } + + ); +}; CreateActions.propTypes = { basePath: PropTypes.string, diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx new file mode 100644 index 00000000000..a57208cfaa2 --- /dev/null +++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx @@ -0,0 +1,160 @@ +import * as React from 'react'; +import { Children, cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import { CreateControllerProps, useCreateContext } from 'ra-core'; +import { Card } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import classnames from 'classnames'; +import { CreateProps } from '../types'; +import { TitleForRecord } from '../layout'; + +export const CreateView = (props: CreateViewProps) => { + const { + actions, + aside, + children, + classes: classesOverride, + className, + component: Content, + title, + ...rest + } = props; + + const classes = useStyles(props); + + const { + basePath, + defaultTitle, + hasList, + record, + redirect, + resource, + save, + saving, + version, + } = useCreateContext(props); + + return ( +
+ + {actions && + cloneElement(actions, { + basePath, + resource, + hasList, + // Ensure we don't override any user provided props + ...actions.props, + })} +
+ + {cloneElement(Children.only(children), { + basePath, + record, + redirect: + typeof children.props.redirect === 'undefined' + ? redirect + : children.props.redirect, + resource, + save, + saving, + version, + })} + + {aside && + cloneElement(aside, { + basePath, + record, + resource, + save, + saving, + version, + })} +
+
+ ); +}; + +interface CreateViewProps + extends CreateProps, + Omit {} + +CreateView.propTypes = { + actions: PropTypes.element, + aside: PropTypes.element, + basePath: PropTypes.string, + children: PropTypes.element, + classes: PropTypes.object, + className: PropTypes.string, + defaultTitle: PropTypes.any, + hasList: PropTypes.bool, + hasShow: PropTypes.bool, + record: PropTypes.object, + redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + resource: PropTypes.string, + save: PropTypes.func, + title: PropTypes.node, + onSuccess: PropTypes.func, + onFailure: PropTypes.func, + setOnSuccess: PropTypes.func, + setOnFailure: PropTypes.func, + setTransform: PropTypes.func, +}; + +CreateView.defaultProps = { + classes: {}, + component: Card, +}; + +const useStyles = makeStyles( + theme => ({ + root: {}, + main: { + display: 'flex', + }, + noActions: { + [theme.breakpoints.up('sm')]: { + marginTop: '1em', + }, + }, + card: { + flex: '1 1 auto', + }, + }), + { name: 'RaCreate' } +); + +const sanitizeRestProps = ({ + basePath = null, + defaultTitle = null, + hasCreate = null, + hasEdit = null, + hasList = null, + hasShow = null, + history = null, + loaded = null, + loading = null, + location = null, + match = null, + onFailure = null, + onSuccess = null, + options = null, + permissions = null, + save = null, + saving = null, + setOnFailure = null, + setOnSuccess = null, + setTransform = null, + transform = null, + ...rest +}) => rest; diff --git a/packages/ra-ui-materialui/src/detail/Edit.spec.js b/packages/ra-ui-materialui/src/detail/Edit.spec.js index 86eaaf24c76..49fe3b8a19a 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.spec.js +++ b/packages/ra-ui-materialui/src/detail/Edit.spec.js @@ -3,7 +3,7 @@ import expect from 'expect'; import { cleanup, wait, fireEvent } from '@testing-library/react'; import { renderWithRedux, DataProviderContext } from 'ra-core'; -import Edit from './Edit'; +import { Edit } from './Edit'; describe('', () => { afterEach(cleanup); diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx index b7285d3850c..3dd9c7a2806 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.tsx +++ b/packages/ra-ui-materialui/src/detail/Edit.tsx @@ -1,20 +1,12 @@ import * as React from 'react'; -import { Children, cloneElement, useMemo } from 'react'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import { makeStyles } from '@material-ui/core/styles'; -import classnames from 'classnames'; import { + EditContextProvider, + useCheckMinimumRequiredProps, useEditController, - EditControllerProps, - ComponentPropType, - SideEffectContext, } from 'ra-core'; - -import DefaultActions from './EditActions'; -import TitleForRecord from '../layout/TitleForRecord'; import { EditProps } from '../types'; +import { EditView } from './EditView'; /** * Page component for the Edit view @@ -61,9 +53,15 @@ import { EditProps } from '../types'; * ); * export default App; */ -const Edit = (props: EditProps): JSX.Element => ( - -); +export const Edit = (props: EditProps): JSX.Element => { + useCheckMinimumRequiredProps('Edit', ['children'], props); + const controllerProps = useEditController(props); + return ( + + + + ); +}; Edit.propTypes = { actions: PropTypes.element, @@ -84,172 +82,3 @@ Edit.propTypes = { transform: PropTypes.func, undoable: PropTypes.bool, }; - -interface EditViewProps - extends EditProps, - Omit {} - -export const EditView = (props: EditViewProps) => { - const { - actions, - aside, - basePath, - children, - classes: classesOverride, - className, - component: Content, - defaultTitle, - hasList, - hasShow, - record, - redirect, - resource, - save, - setOnSuccess, - setOnFailure, - setTransform, - saving, - title, - undoable, - version, - ...rest - } = props; - const classes = useStyles(props); - const finalActions = - typeof actions === 'undefined' && hasShow ? ( - - ) : ( - actions - ); - const sideEffectContextValue = useMemo( - () => ({ setOnSuccess, setOnFailure, setTransform }), - [setOnFailure, setOnSuccess, setTransform] - ); - if (!children) { - return null; - } - return ( - -
- - {finalActions && - cloneElement(finalActions, { - basePath, - data: record, - hasShow, - hasList, - resource, - // Ensure we don't override any user provided props - ...finalActions.props, - })} -
- - {record ? ( - cloneElement(Children.only(children), { - basePath, - record, - redirect: - typeof children.props.redirect === - 'undefined' - ? redirect - : children.props.redirect, - resource, - save, - saving, - undoable, - version, - }) - ) : ( -   - )} - - {aside && - React.cloneElement(aside, { - basePath, - record, - resource, - version, - save, - saving, - })} -
-
-
- ); -}; - -EditView.propTypes = { - actions: PropTypes.element, - aside: PropTypes.element, - basePath: PropTypes.string, - children: PropTypes.element, - classes: PropTypes.object, - className: PropTypes.string, - component: ComponentPropType, - defaultTitle: PropTypes.any, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - record: PropTypes.object, - redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - resource: PropTypes.string, - save: PropTypes.func, - title: PropTypes.node, - version: PropTypes.number, - onSuccess: PropTypes.func, - onFailure: PropTypes.func, - setOnSuccess: PropTypes.func, - setOnFailure: PropTypes.func, - setTransform: PropTypes.func, -}; - -EditView.defaultProps = { - classes: {}, - component: Card, -}; - -const useStyles = makeStyles( - { - root: {}, - main: { - display: 'flex', - }, - noActions: { - marginTop: '1em', - }, - card: { - flex: '1 1 auto', - }, - }, - { name: 'RaEdit' } -); - -const sanitizeRestProps = ({ - hasCreate = null, - hasEdit = null, - history = null, - id = null, - loaded = null, - loading = null, - location = null, - match = null, - onFailure = null, - onSuccess = null, - options = null, - permissions = null, - successMessage = null, - transform = null, - ...rest -}) => rest; - -export default Edit; diff --git a/packages/ra-ui-materialui/src/detail/EditActions.tsx b/packages/ra-ui-materialui/src/detail/EditActions.tsx index bcb157afa1c..95e61800a89 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 { Record } from 'ra-core'; +import { Record, useEditContext } from 'ra-core'; import { ShowButton } from '../button'; import TopToolbar from '../layout/TopToolbar'; @@ -30,18 +30,24 @@ import TopToolbar from '../layout/TopToolbar'; *
* ); */ -const EditActions = ({ - basePath, - className, - data, - hasShow, - hasList, +const EditActions = ({ className, ...rest }: EditActionsProps) => { + const { basePath, hasShow, record } = useEditContext(rest); + + return ( + + {hasShow && } + + ); +}; + +const sanitizeRestProps = ({ + basePath = null, + hasCreate = null, + hasEdit = null, + hasShow = null, + hasList = null, ...rest -}: EditActionsProps) => ( - - {hasShow && } - -); +}) => rest; export interface EditActionsProps { basePath?: string; diff --git a/packages/ra-ui-materialui/src/detail/EditGuesser.js b/packages/ra-ui-materialui/src/detail/EditGuesser.js index c73717c9c80..a137682de2f 100644 --- a/packages/ra-ui-materialui/src/detail/EditGuesser.js +++ b/packages/ra-ui-materialui/src/detail/EditGuesser.js @@ -7,7 +7,7 @@ import { getElementsFromRecords, } from 'ra-core'; -import { EditView } from './Edit'; +import { EditView } from './EditView'; import editFieldTypes from './editFieldTypes'; const EditViewGuesser = props => { diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx new file mode 100644 index 00000000000..8d5b89025be --- /dev/null +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -0,0 +1,187 @@ +import * as React from 'react'; +import { Children, cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { makeStyles } from '@material-ui/core/styles'; +import classnames from 'classnames'; +import { + EditControllerProps, + ComponentPropType, + useEditContext, +} from 'ra-core'; + +import DefaultActions from './EditActions'; +import TitleForRecord from '../layout/TitleForRecord'; +import { EditProps } from '../types'; + +interface EditViewProps + extends EditProps, + Omit {} + +export const EditView = (props: EditViewProps) => { + const { + actions, + aside, + children, + classes: classesOverride, + className, + component: Content, + title, + undoable, + ...rest + } = props; + + const classes = useStyles(props); + + const { + basePath, + defaultTitle, + hasList, + hasShow, + record, + redirect, + resource, + save, + saving, + version, + } = useEditContext(props); + + const finalActions = + typeof actions === 'undefined' && hasShow ? ( + + ) : ( + actions + ); + if (!children) { + return null; + } + return ( +
+ + {finalActions && + cloneElement(finalActions, { + basePath, + data: record, + hasShow, + hasList, + resource, + // Ensure we don't override any user provided props + ...finalActions.props, + })} +
+ + {record ? ( + cloneElement(Children.only(children), { + basePath, + record, + redirect: + typeof children.props.redirect === 'undefined' + ? redirect + : children.props.redirect, + resource, + save, + saving, + undoable, + version, + }) + ) : ( +   + )} + + {aside && + React.cloneElement(aside, { + basePath, + record, + resource, + version, + save, + saving, + })} +
+
+ ); +}; + +EditView.propTypes = { + actions: PropTypes.element, + aside: PropTypes.element, + basePath: PropTypes.string, + children: PropTypes.element, + classes: PropTypes.object, + className: PropTypes.string, + component: ComponentPropType, + defaultTitle: PropTypes.any, + hasList: PropTypes.bool, + hasShow: PropTypes.bool, + record: PropTypes.object, + redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + resource: PropTypes.string, + save: PropTypes.func, + title: PropTypes.node, + version: PropTypes.number, + onSuccess: PropTypes.func, + onFailure: PropTypes.func, + setOnSuccess: PropTypes.func, + setOnFailure: PropTypes.func, + setTransform: PropTypes.func, +}; + +EditView.defaultProps = { + classes: {}, + component: Card, +}; + +const useStyles = makeStyles( + { + root: {}, + main: { + display: 'flex', + }, + noActions: { + marginTop: '1em', + }, + card: { + flex: '1 1 auto', + }, + }, + { name: 'RaEdit' } +); + +const sanitizeRestProps = ({ + basePath = null, + defaultTitle = null, + hasCreate = null, + hasEdit = null, + hasList = null, + hasShow = null, + history = null, + id = null, + loaded = null, + loading = null, + location = null, + match = null, + onFailure = null, + onSuccess = null, + options = null, + permissions = null, + save = null, + saving = null, + setOnFailure = null, + setOnSuccess = null, + setTransform = null, + successMessage = null, + transform = null, + ...rest +}) => rest; diff --git a/packages/ra-ui-materialui/src/detail/Show.spec.js b/packages/ra-ui-materialui/src/detail/Show.spec.js index 5e7051f189a..be715be1f92 100644 --- a/packages/ra-ui-materialui/src/detail/Show.spec.js +++ b/packages/ra-ui-materialui/src/detail/Show.spec.js @@ -3,7 +3,7 @@ import expect from 'expect'; import { cleanup } from '@testing-library/react'; import { renderWithRedux, DataProviderContext } from 'ra-core'; -import Show from './Show'; +import { Show } from './Show'; describe('', () => { afterEach(cleanup); diff --git a/packages/ra-ui-materialui/src/detail/Show.tsx b/packages/ra-ui-materialui/src/detail/Show.tsx index 8b63ae92c45..31e336e0425 100644 --- a/packages/ra-ui-materialui/src/detail/Show.tsx +++ b/packages/ra-ui-materialui/src/detail/Show.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; -import { cloneElement, Children } from 'react'; import PropTypes from 'prop-types'; -import Card from '@material-ui/core/Card'; -import { makeStyles } from '@material-ui/core/styles'; -import classnames from 'classnames'; -import { ShowControllerProps, useShowController } from 'ra-core'; +import { + ShowContextProvider, + useCheckMinimumRequiredProps, + useShowController, +} from 'ra-core'; -import DefaultActions from './ShowActions'; -import TitleForRecord from '../layout/TitleForRecord'; import { ShowProps } from '../types'; +import { ShowView } from './ShowView'; /** * Page component for the Show view @@ -53,9 +52,15 @@ import { ShowProps } from '../types'; * ); * export default App; */ -const Show = (props: ShowProps) => ( - -); +export const Show = (props: ShowProps) => { + useCheckMinimumRequiredProps('Show', ['children'], props); + const controllerProps = useShowController(props); + return ( + + + + ); +}; Show.propTypes = { actions: PropTypes.element, @@ -71,137 +76,3 @@ Show.propTypes = { resource: PropTypes.string.isRequired, title: PropTypes.node, }; - -interface ShowViewProps - extends ShowProps, - Omit {} - -export const ShowView = (props: ShowViewProps) => { - const { - actions, - aside, - basePath, - children, - classes: classesOverride, - className, - component: Content, - defaultTitle, - hasEdit, - hasList, - record, - resource, - title, - version, - ...rest - } = props; - const classes = useStyles(props); - const finalActions = - typeof actions === 'undefined' && hasEdit ? ( - - ) : ( - actions - ); - - if (!children) { - return null; - } - return ( -
- - {finalActions && - cloneElement(finalActions, { - basePath, - data: record, - hasList, - hasEdit, - resource, - // Ensure we don't override any user provided props - ...finalActions.props, - })} -
- - {record && - cloneElement(Children.only(children), { - resource, - basePath, - record, - version, - })} - - {aside && - cloneElement(aside, { - resource, - basePath, - record, - version, - })} -
-
- ); -}; - -ShowView.propTypes = { - actions: PropTypes.element, - aside: PropTypes.element, - basePath: PropTypes.string, - children: PropTypes.element, - classes: PropTypes.object, - className: PropTypes.string, - defaultTitle: PropTypes.any, - hasEdit: PropTypes.bool, - hasList: PropTypes.bool, - loading: PropTypes.bool, - loaded: PropTypes.bool, - record: PropTypes.object, - resource: PropTypes.string, - title: PropTypes.any, - version: PropTypes.node, -}; - -ShowView.defaultProps = { - classes: {}, - component: Card, -}; - -const useStyles = makeStyles( - { - root: {}, - main: { - display: 'flex', - }, - noActions: { - marginTop: '1em', - }, - card: { - flex: '1 1 auto', - }, - }, - { name: 'RaShow' } -); - -const sanitizeRestProps = ({ - hasCreate = null, - hasEdit = null, - history = null, - id = null, - loaded = null, - loading = null, - location = null, - match = null, - options = null, - permissions = null, - ...rest -}) => rest; - -export default Show; diff --git a/packages/ra-ui-materialui/src/detail/ShowActions.tsx b/packages/ra-ui-materialui/src/detail/ShowActions.tsx index 291ed17ae4c..dec37bba556 100644 --- a/packages/ra-ui-materialui/src/detail/ShowActions.tsx +++ b/packages/ra-ui-materialui/src/detail/ShowActions.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { Record } from 'ra-core'; +import { Record, useShowContext } from 'ra-core'; import { EditButton } from '../button'; import TopToolbar from '../layout/TopToolbar'; @@ -8,7 +8,6 @@ import TopToolbar from '../layout/TopToolbar'; const sanitizeRestProps = ({ basePath, className, - record, hasEdit, hasList, resource, @@ -40,17 +39,14 @@ const sanitizeRestProps = ({ *
* ); */ -const ShowActions = ({ - basePath, - className, - data, - hasEdit, - ...rest -}: ShowActionsProps) => ( - - {hasEdit && } - -); +const ShowActions = ({ className, ...rest }: ShowActionsProps) => { + const { basePath, hasEdit, record } = useShowContext(rest); + return ( + + {hasEdit && } + + ); +}; export interface ShowActionsProps { basePath?: string; diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.js b/packages/ra-ui-materialui/src/detail/ShowGuesser.js index c39b755ba44..a3406ea7571 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.js +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.js @@ -5,9 +5,10 @@ import { useShowController, InferredElement, getElementsFromRecords, + ShowContextProvider, } from 'ra-core'; -import { ShowView } from './Show'; +import { ShowView } from './ShowView'; import showFieldTypes from './showFieldTypes'; const ShowViewGuesser = props => { @@ -47,8 +48,13 @@ ${inferredChild.getRepresentation()} ShowViewGuesser.propTypes = ShowView.propTypes; -const ShowGuesser = props => ( - -); +const ShowGuesser = props => { + const controllerProps = useShowController(props); + return ( + + + + ); +}; export default ShowGuesser; diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx new file mode 100644 index 00000000000..5c78a3dfe1d --- /dev/null +++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import { cloneElement, Children } from 'react'; +import PropTypes from 'prop-types'; +import Card from '@material-ui/core/Card'; +import { makeStyles } from '@material-ui/core/styles'; +import classnames from 'classnames'; +import { ShowControllerProps, useShowContext } from 'ra-core'; + +import DefaultActions from './ShowActions'; +import TitleForRecord from '../layout/TitleForRecord'; +import { ShowProps } from '../types'; + +interface ShowViewProps + extends ShowProps, + Omit {} + +export const ShowView = (props: ShowViewProps) => { + const { + actions, + aside, + children, + classes: classesOverride, + className, + component: Content, + title, + ...rest + } = props; + + const classes = useStyles(props); + + const { + basePath, + defaultTitle, + hasEdit, + hasList, + record, + resource, + version, + } = useShowContext(props); + + const finalActions = + typeof actions === 'undefined' && hasEdit ? ( + + ) : ( + actions + ); + + if (!children) { + return null; + } + return ( +
+ + {finalActions && + cloneElement(finalActions, { + basePath, + data: record, + hasList, + hasEdit, + resource, + // Ensure we don't override any user provided props + ...finalActions.props, + })} +
+ + {record && + cloneElement(Children.only(children), { + resource, + basePath, + record, + version, + })} + + {aside && + cloneElement(aside, { + resource, + basePath, + record, + version, + })} +
+
+ ); +}; + +ShowView.propTypes = { + actions: PropTypes.element, + aside: PropTypes.element, + basePath: PropTypes.string, + children: PropTypes.element, + classes: PropTypes.object, + className: PropTypes.string, + defaultTitle: PropTypes.any, + hasEdit: PropTypes.bool, + hasList: PropTypes.bool, + loading: PropTypes.bool, + loaded: PropTypes.bool, + record: PropTypes.object, + resource: PropTypes.string, + title: PropTypes.any, + version: PropTypes.node, +}; + +ShowView.defaultProps = { + classes: {}, + component: Card, +}; + +const useStyles = makeStyles( + { + root: {}, + main: { + display: 'flex', + }, + noActions: { + marginTop: '1em', + }, + card: { + flex: '1 1 auto', + }, + }, + { name: 'RaShow' } +); + +const sanitizeRestProps = ({ + basePath = null, + defaultTitle = null, + hasCreate = null, + hasEdit = null, + hasList = null, + hasShow = null, + history = null, + id = null, + loaded = null, + loading = null, + location = null, + match = null, + options = null, + permissions = null, + ...rest +}) => rest; diff --git a/packages/ra-ui-materialui/src/detail/index.ts b/packages/ra-ui-materialui/src/detail/index.ts index 82bded8ce3b..d5ade4e251d 100644 --- a/packages/ra-ui-materialui/src/detail/index.ts +++ b/packages/ra-ui-materialui/src/detail/index.ts @@ -1,9 +1,12 @@ -import Create, { CreateView } from './Create'; +import { Create } from './Create'; +import { CreateView } from './CreateView'; import CreateActions from './CreateActions'; -import Edit, { EditView } from './Edit'; +import { Edit } from './Edit'; +import { EditView } from './EditView'; import EditActions from './EditActions'; import EditGuesser from './EditGuesser'; -import Show, { ShowView } from './Show'; +import { Show } from './Show'; +import { ShowView } from './ShowView'; import ShowActions, { ShowActionsProps } from './ShowActions'; import ShowGuesser from './ShowGuesser'; import SimpleShowLayout, { SimpleShowLayoutProps } from './SimpleShowLayout'; diff --git a/packages/ra-ui-materialui/src/form/FormTab.spec.tsx b/packages/ra-ui-materialui/src/form/FormTab.spec.tsx index f03fbf98b0c..8a67c9bad86 100644 --- a/packages/ra-ui-materialui/src/form/FormTab.spec.tsx +++ b/packages/ra-ui-materialui/src/form/FormTab.spec.tsx @@ -1,7 +1,11 @@ import { cleanup } from '@testing-library/react'; import * as React from 'react'; import expect from 'expect'; -import { renderWithRedux } from 'ra-core'; +import { + renderWithRedux, + SaveContextProvider, + SideEffectContextProvider, +} from 'ra-core'; import TabbedForm from './TabbedForm'; import FormTab from './FormTab'; @@ -10,25 +14,36 @@ import TextInput from '../input/TextInput'; describe('', () => { afterEach(cleanup); + const saveContextValue = { save: jest.fn(), saving: false }; + const sideEffectValue = {}; + it('should display ', () => { const { queryByLabelText } = renderWithRedux( - - - - - - + + + + + + + + + + ); expect(queryByLabelText('ra.action.save')).not.toBeNull(); }); it('should not alter default margin or variant', () => { const { queryByLabelText } = renderWithRedux( - - - - - + + + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -44,38 +59,42 @@ describe('', () => { const record = { id: 'gazebo', name: 'foo' }; const { container } = renderWithRedux( - - - - - - - - - - - + + + + + + + + + + + + + + + ); expect(spy).not.toHaveBeenCalled(); expect(container).not.toBeNull(); @@ -85,11 +104,15 @@ describe('', () => { it('should pass variant and margin to child inputs', () => { const { queryByLabelText } = renderWithRedux( - - - - - + + + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -102,15 +125,19 @@ describe('', () => { it('should allow input children to override variant and margin', () => { const { queryByLabelText } = renderWithRedux( - - - - - + + + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx index 5cd9f41b24f..a4ea3bb89f2 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx @@ -1,7 +1,11 @@ import { cleanup } from '@testing-library/react'; import * as React from 'react'; import expect from 'expect'; -import { renderWithRedux } from 'ra-core'; +import { + renderWithRedux, + SaveContextProvider, + SideEffectContextProvider, +} from 'ra-core'; import SimpleForm from './SimpleForm'; import TextInput from '../input/TextInput'; @@ -9,12 +13,19 @@ import TextInput from '../input/TextInput'; describe('', () => { afterEach(cleanup); + const saveContextValue = { save: jest.fn(), saving: false }; + const sideEffects = {}; + it('should embed a form with given component children', () => { const { queryByLabelText } = renderWithRedux( - - - - + + + + + + + + ); expect( queryByLabelText('resources.undefined.fields.name') @@ -26,10 +37,14 @@ describe('', () => { it('should display ', () => { const { queryByLabelText } = renderWithRedux( - - - - + + + + + + + + ); expect(queryByLabelText('ra.action.save')).not.toBeNull(); }); @@ -40,21 +55,39 @@ describe('', () => { ); const { queryByText, rerender } = renderWithRedux( - } /> + + + }> +
+ + + ); expect(queryByText('submitOnEnter: false')).not.toBeNull(); - rerender(} />); + rerender( + + + }> +
+ + + + ); expect(queryByText('submitOnEnter: true')).not.toBeNull(); }); it('should not alter default margin or variant', () => { const { queryByLabelText } = renderWithRedux( - - - + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -67,9 +100,13 @@ describe('', () => { it('should pass variant and margin to child inputs', () => { const { queryByLabelText } = renderWithRedux( - - - + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' @@ -82,9 +119,17 @@ describe('', () => { it('should allow input children to override variant and margin', () => { const { queryByLabelText } = renderWithRedux( - - - + + + + + + + ); const inputElement = queryByLabelText( 'resources.undefined.fields.name' diff --git a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx index 34c77764183..40c1033b855 100644 --- a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx @@ -1,6 +1,10 @@ import { cleanup, fireEvent, wait, getByText } from '@testing-library/react'; import * as React from 'react'; -import { renderWithRedux } from 'ra-core'; +import { + renderWithRedux, + SaveContextProvider, + SideEffectContextProvider, +} from 'ra-core'; import { ThemeProvider } from '@material-ui/core'; import { createMuiTheme } from '@material-ui/core/styles'; @@ -22,15 +26,22 @@ describe('', () => { afterEach(cleanup); + const saveContextValue = { save: jest.fn(), saving: false }; + const sideEffectValue = {}; + it('should display an add item button at least', () => { const { getByText } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); expect(getByText('ra.action.add')).toBeDefined(); @@ -38,13 +49,17 @@ describe('', () => { it('should not display add button if disableAdd is truthy', () => { const { queryAllByText } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); expect(queryAllByText('ra.action.add').length).toBe(0); @@ -53,18 +68,22 @@ describe('', () => { it('should not display remove button if disableRemove is truthy', () => { const { queryAllByText } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); @@ -77,13 +96,17 @@ describe('', () => { queryAllByLabelText, queryAllByText, } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); const addItemElement = getByText('ra.action.add').closest('button'); @@ -125,13 +148,17 @@ describe('', () => { queryAllByLabelText, queryAllByText, } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); const addItemElement = getByText('ra.action.add').closest('button'); @@ -160,17 +187,21 @@ describe('', () => { queryAllByLabelText, queryAllByText, } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); const addItemElement = getByText('ra.action.add').closest('button'); @@ -198,13 +229,17 @@ describe('', () => { const { queryAllByLabelText } = renderWithRedux( - - - - - - - + + + + + + + + + + + ); @@ -239,15 +274,19 @@ describe('', () => { it('should not display the default add button if a custom add button is passed', () => { const { queryAllByText } = renderWithRedux( - - - Custom Add Button} - > - - - - + + + + + Custom Add Button} + > + + + + + + ); expect(queryAllByText('ra.action.add').length).toBe(0); }); @@ -255,17 +294,23 @@ describe('', () => { it('should not display the default remove button if a custom remove button is passed', () => { const { queryAllByText } = renderWithRedux( - - - Custom Remove Button} + + + - - - - + + Custom Remove Button + } + > + + + + + + ); @@ -274,15 +319,19 @@ describe('', () => { it('should display the custom add button', () => { const { getByText } = renderWithRedux( - - - Custom Add Button} - > - - - - + + + + + Custom Add Button} + > + + + + + + ); expect(getByText('Custom Add Button')).toBeDefined(); @@ -291,17 +340,23 @@ describe('', () => { it('should display the custom remove button', () => { const { getByText } = renderWithRedux( - - - Custom Remove Button} + + + - - - - + + Custom Remove Button + } + > + + + + + + ); @@ -312,19 +367,23 @@ describe('', () => { const onClick = jest.fn(); const { getByText } = renderWithRedux( - - - - Custom Add Button - - } - > - - - - + + + + + + Custom Add Button + + } + > + + + + + + ); fireEvent.click(getByText('Custom Add Button')); @@ -335,21 +394,25 @@ describe('', () => { const onClick = jest.fn(); const { getByText } = renderWithRedux( - - - - Custom Remove Button - - } + + + - - - - + + + Custom Remove Button + + } + > + + + + + + ); fireEvent.click(getByText('Custom Remove Button')); diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx index 177b0796542..04279094c5e 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.spec.tsx @@ -2,7 +2,11 @@ import { cleanup } from '@testing-library/react'; import * as React from 'react'; import { createElement } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { renderWithRedux } from 'ra-core'; +import { + renderWithRedux, + SaveContextProvider, + SideEffectContextProvider, +} from 'ra-core'; import TabbedForm, { findTabsWithErrors } from './TabbedForm'; import FormTab from './FormTab'; @@ -10,13 +14,20 @@ import FormTab from './FormTab'; describe('', () => { afterEach(cleanup); + const saveContextValue = { save: jest.fn(), saving: false }; + const sideEffectValue = {}; + it('should display the tabs', () => { const { queryAllByRole } = renderWithRedux( - - - - + + + + + + + + ); @@ -31,10 +42,14 @@ describe('', () => { const { queryByText, rerender } = renderWithRedux( - }> - - - + + + }> + + + + + ); @@ -42,10 +57,14 @@ describe('', () => { rerender( - }> - - - + + + }> + + + + + );