From 0b8fe6d1501391aaada90f21134cb2cdabbcd75a Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 30 Nov 2020 10:43:21 +0100 Subject: [PATCH 1/5] Make SaveContext provides access to side effects --- .../ra-core/src/controller/details/SaveContext.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/details/SaveContext.tsx b/packages/ra-core/src/controller/details/SaveContext.tsx index e6064f1ac32..82625c3da83 100644 --- a/packages/ra-core/src/controller/details/SaveContext.tsx +++ b/packages/ra-core/src/controller/details/SaveContext.tsx @@ -8,10 +8,13 @@ import { OnFailure, OnSuccess, SideEffectContextValue, + SideEffectContextOptions, TransformData, } from '../saveModifiers'; -interface SaveContextValue extends SideEffectContextValue { +interface SaveContextValue + extends SideEffectContextValue, + SideEffectContextOptions { save?: ( record: Partial, redirect: RedirectionSideEffect, @@ -81,6 +84,9 @@ export const usePickSaveContext = < 'setOnFailure', 'setOnSuccess', 'setTransform', + 'onSuccess', + 'onFailure', + 'transorm', ]), /* eslint-disable react-hooks/exhaustive-deps */ [ @@ -89,6 +95,10 @@ export const usePickSaveContext = < context.setOnFailure, context.setOnSuccess, context.setTransform, + context.setTransform, + context.onFailure, + context.onSuccess, + context.transform, ] /* eslint-enable react-hooks/exhaustive-deps */ ); From 1bd5086b84d3fbaa6536ea903a79d8dc2b4be6b4 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 30 Nov 2020 12:39:20 +0100 Subject: [PATCH 2/5] Review & Tests --- .../controller/details/CreateBase.spec.tsx | 122 ++++++++++++++++++ .../src/controller/details/CreateContext.tsx | 3 + .../src/controller/details/EditBase.spec.tsx | 122 ++++++++++++++++++ .../src/controller/details/EditContext.tsx | 3 + .../src/controller/details/SaveContext.tsx | 2 +- .../controller/details/useCreateController.ts | 6 + .../controller/details/useEditController.ts | 6 + 7 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 packages/ra-core/src/controller/details/CreateBase.spec.tsx create mode 100644 packages/ra-core/src/controller/details/EditBase.spec.tsx diff --git a/packages/ra-core/src/controller/details/CreateBase.spec.tsx b/packages/ra-core/src/controller/details/CreateBase.spec.tsx new file mode 100644 index 00000000000..78e3c09c694 --- /dev/null +++ b/packages/ra-core/src/controller/details/CreateBase.spec.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; +import { useEffect } from 'react'; +import { cleanup } from '@testing-library/react'; +import { CreateBase } from './CreateBase'; +import { useSaveContext } from './SaveContext'; +import { DataProviderContext } from '../../dataProvider'; +import { DataProvider } from '../../types'; +import { renderWithRedux } from '../../util'; + +describe('CreateBase', () => { + afterEach(cleanup); + + const defaultProps = { + basePath: '', + hasCreate: true, + hasEdit: true, + hasList: true, + hasShow: true, + id: 12, + resource: 'posts', + debounce: 200, + }; + + it('should give access to the current onSuccess function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const onSuccess = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.onSuccess('test'); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(onSuccess).toHaveBeenCalledWith('test'); + }); + + it('should give access to the current onFailure function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const onFailure = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.onFailure({ message: 'test' }); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(onFailure).toHaveBeenCalledWith({ message: 'test' }); + }); + + it('should give access to the current transform function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const transform = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.transform({ message: 'test' }); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(transform).toHaveBeenCalledWith({ message: 'test' }); + }); +}); diff --git a/packages/ra-core/src/controller/details/CreateContext.tsx b/packages/ra-core/src/controller/details/CreateContext.tsx index 56578ebf8d6..bd46f9c78f6 100644 --- a/packages/ra-core/src/controller/details/CreateContext.tsx +++ b/packages/ra-core/src/controller/details/CreateContext.tsx @@ -23,6 +23,9 @@ export const CreateContext = createContext({ basePath: null, record: null, defaultTitle: null, + onFailure: null, + onSuccess: null, + transform: null, loaded: null, loading: null, redirect: null, diff --git a/packages/ra-core/src/controller/details/EditBase.spec.tsx b/packages/ra-core/src/controller/details/EditBase.spec.tsx new file mode 100644 index 00000000000..f78b708ac08 --- /dev/null +++ b/packages/ra-core/src/controller/details/EditBase.spec.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; +import { useEffect } from 'react'; +import { cleanup } from '@testing-library/react'; +import { EditBase } from './EditBase'; +import { useSaveContext } from './SaveContext'; +import { DataProviderContext } from '../../dataProvider'; +import { DataProvider } from '../../types'; +import { renderWithRedux } from '../../util'; + +describe('EditBase', () => { + afterEach(cleanup); + + const defaultProps = { + basePath: '', + hasCreate: true, + hasEdit: true, + hasList: true, + hasShow: true, + id: 12, + resource: 'posts', + debounce: 200, + }; + + it('should give access to the current onSuccess function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const onSuccess = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.onSuccess('test'); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(onSuccess).toHaveBeenCalledWith('test'); + }); + + it('should give access to the current onFailure function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const onFailure = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.onFailure({ message: 'test' }); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(onFailure).toHaveBeenCalledWith({ message: 'test' }); + }); + + it('should give access to the current transform function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const transform = jest.fn(); + + const Child = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.transform({ message: 'test' }); + }, []); + + return null; + }; + renderWithRedux( + + + + + , + { admin: { resources: { posts: { data: {} } } } } + ); + + expect(transform).toHaveBeenCalledWith({ message: 'test' }); + }); +}); diff --git a/packages/ra-core/src/controller/details/EditContext.tsx b/packages/ra-core/src/controller/details/EditContext.tsx index ede20ef1306..9632057248f 100644 --- a/packages/ra-core/src/controller/details/EditContext.tsx +++ b/packages/ra-core/src/controller/details/EditContext.tsx @@ -25,6 +25,9 @@ export const EditContext = createContext({ defaultTitle: null, loaded: null, loading: null, + onFailure: null, + onSuccess: null, + transform: null, redirect: null, setOnFailure: null, setOnSuccess: null, diff --git a/packages/ra-core/src/controller/details/SaveContext.tsx b/packages/ra-core/src/controller/details/SaveContext.tsx index 82625c3da83..2f5b5efb3fe 100644 --- a/packages/ra-core/src/controller/details/SaveContext.tsx +++ b/packages/ra-core/src/controller/details/SaveContext.tsx @@ -86,7 +86,7 @@ export const usePickSaveContext = < 'setTransform', 'onSuccess', 'onFailure', - 'transorm', + 'transform', ]), /* eslint-disable react-hooks/exhaustive-deps */ [ diff --git a/packages/ra-core/src/controller/details/useCreateController.ts b/packages/ra-core/src/controller/details/useCreateController.ts index a89ecfd6950..b9cf9bc00ec 100644 --- a/packages/ra-core/src/controller/details/useCreateController.ts +++ b/packages/ra-core/src/controller/details/useCreateController.ts @@ -57,6 +57,9 @@ export interface CreateControllerProps< hasEdit?: boolean; hasList?: boolean; hasShow?: boolean; + onSuccess: OnSuccess; + onFailure: OnFailure; + transform: TransformData; save: ( record: Partial, redirect: RedirectionSideEffect, @@ -218,6 +221,9 @@ export const useCreateController = < loaded: true, saving, defaultTitle, + onFailure: onFailureRef.current, + onSuccess: onSuccessRef.current, + transform: transformRef.current, save, setOnSuccess, setOnFailure, diff --git a/packages/ra-core/src/controller/details/useEditController.ts b/packages/ra-core/src/controller/details/useEditController.ts index a90b86a1762..a22648a576f 100644 --- a/packages/ra-core/src/controller/details/useEditController.ts +++ b/packages/ra-core/src/controller/details/useEditController.ts @@ -51,6 +51,9 @@ export interface EditControllerProps { hasList?: boolean; loading: boolean; loaded: boolean; + onSuccess: OnSuccess; + onFailure: OnFailure; + transform: TransformData; save: ( data: Partial, redirect?: RedirectionSideEffect, @@ -238,6 +241,9 @@ export const useEditController = ( hasEdit, hasList, hasShow, + onSuccess: onSuccessRef.current, + onFailure: onFailureRef.current, + transform: transformRef.current, save, setOnSuccess, setOnFailure, From d46b3a3af5fcf18c298f5ab4f080fbcb82ae2f8d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 30 Nov 2020 13:50:49 +0100 Subject: [PATCH 3/5] Fix ra-ui-materialui --- packages/ra-ui-materialui/src/detail/CreateView.tsx | 9 +++++++-- packages/ra-ui-materialui/src/detail/EditView.tsx | 11 ++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx index a57208cfaa2..7767a6d0d67 100644 --- a/packages/ra-ui-materialui/src/detail/CreateView.tsx +++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Children, cloneElement } from 'react'; +import { Children, cloneElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { CreateControllerProps, useCreateContext } from 'ra-core'; import { Card } from '@material-ui/core'; @@ -87,7 +87,12 @@ export const CreateView = (props: CreateViewProps) => { interface CreateViewProps extends CreateProps, - Omit {} + Omit< + CreateControllerProps, + 'resource' | 'onSuccess' | 'onFailure' | 'transform' + > { + children: ReactElement; +} CreateView.propTypes = { actions: PropTypes.element, diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx index 8d5b89025be..647c70664f1 100644 --- a/packages/ra-ui-materialui/src/detail/EditView.tsx +++ b/packages/ra-ui-materialui/src/detail/EditView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Children, cloneElement } from 'react'; +import { Children, cloneElement, ReactElement } from 'react'; import PropTypes from 'prop-types'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; @@ -114,6 +114,15 @@ export const EditView = (props: EditViewProps) => { ); }; +interface EditViewProps + extends EditProps, + Omit< + EditControllerProps, + 'resource' | 'onSuccess' | 'onFailure' | 'transform' + > { + children: ReactElement; +} + EditView.propTypes = { actions: PropTypes.element, aside: PropTypes.element, From 9513ef1ba106865b0f01cf64a88190b3e733f516 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 30 Nov 2020 15:32:40 +0100 Subject: [PATCH 4/5] Pass refs instead of their values --- .../controller/details/CreateBase.spec.tsx | 150 +++++++++++++++++- .../src/controller/details/CreateContext.tsx | 6 +- .../src/controller/details/EditBase.spec.tsx | 150 +++++++++++++++++- .../src/controller/details/EditContext.tsx | 6 +- .../src/controller/details/SaveContext.tsx | 26 +-- .../controller/details/useCreateController.ts | 14 +- .../controller/details/useEditController.ts | 14 +- .../src/detail/CreateView.tsx | 11 +- .../ra-ui-materialui/src/detail/EditView.tsx | 11 +- 9 files changed, 335 insertions(+), 53 deletions(-) diff --git a/packages/ra-core/src/controller/details/CreateBase.spec.tsx b/packages/ra-core/src/controller/details/CreateBase.spec.tsx index 78e3c09c694..41968e69fb9 100644 --- a/packages/ra-core/src/controller/details/CreateBase.spec.tsx +++ b/packages/ra-core/src/controller/details/CreateBase.spec.tsx @@ -33,7 +33,7 @@ describe('CreateBase', () => { const saveContext = useSaveContext(); useEffect(() => { - saveContext.onSuccess('test'); + saveContext.onSuccessRef.current('test'); }, []); return null; @@ -54,6 +54,54 @@ describe('CreateBase', () => { expect(onSuccess).toHaveBeenCalledWith('test'); }); + it('should allow to override the onSuccess function', () => { + const dataProvider = ({ + getOne: () => Promise.resolve({ data: { id: 12 } }), + update: (_, { id, data, previousData }) => + Promise.resolve({ data: { id, ...previousData, ...data } }), + } as unknown) as DataProvider; + const onSuccess = jest.fn(); + const onSuccessOverride = jest.fn(); + + const SetOnSuccess = () => { + const saveContext = useSaveContext(); + + useEffect(() => { + saveContext.setOnSuccess(onSuccessOverride); + }, []); + + return null; + }; + const Child = () => { + const saveContext = useSaveContext(); + + const handleClick = () => { + saveContext.onSuccessRef.current('test'); + }; + + return