From f58fbbc4e0814937da4c34f8d29b5db40573188d Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:58:23 +0200 Subject: [PATCH 1/3] Make all mutations react to their declaration time options changes --- .../src/dataProvider/useCreate.spec.tsx | 43 +++- .../src/dataProvider/useCreate.stories.tsx | 210 ++++++++++++++++++ .../ra-core/src/dataProvider/useCreate.ts | 9 +- .../src/dataProvider/useDelete.spec.tsx | 78 ++++++- .../src/dataProvider/useDelete.stories.tsx | 168 ++++++++++++++ .../ra-core/src/dataProvider/useDelete.ts | 11 +- .../src/dataProvider/useDeleteMany.spec.tsx | 78 ++++++- .../dataProvider/useDeleteMany.stories.tsx | 164 ++++++++++++++ .../ra-core/src/dataProvider/useDeleteMany.ts | 11 +- .../src/dataProvider/useUpdate.spec.tsx | 50 ++++- .../src/dataProvider/useUpdate.stories.tsx | 166 ++++++++++++++ .../ra-core/src/dataProvider/useUpdate.ts | 11 +- .../src/dataProvider/useUpdateMany.spec.tsx | 54 ++++- .../dataProvider/useUpdateMany.stories.tsx | 107 +++++++++ .../ra-core/src/dataProvider/useUpdateMany.ts | 11 +- 15 files changed, 1156 insertions(+), 15 deletions(-) create mode 100644 packages/ra-core/src/dataProvider/useCreate.stories.tsx create mode 100644 packages/ra-core/src/dataProvider/useDelete.stories.tsx create mode 100644 packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx create mode 100644 packages/ra-core/src/dataProvider/useUpdate.stories.tsx diff --git a/packages/ra-core/src/dataProvider/useCreate.spec.tsx b/packages/ra-core/src/dataProvider/useCreate.spec.tsx index d4746892a86..44937381fa3 100644 --- a/packages/ra-core/src/dataProvider/useCreate.spec.tsx +++ b/packages/ra-core/src/dataProvider/useCreate.spec.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { render, waitFor, screen } from '@testing-library/react'; +import { render, waitFor, screen, fireEvent } from '@testing-library/react'; import expect from 'expect'; +import { QueryClient, useMutationState } from '@tanstack/react-query'; import { RaRecord } from '../types'; import { testDataProvider } from './testDataProvider'; @@ -25,7 +26,7 @@ import { WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable, WithMiddlewaresError as WithMiddlewaresErrorUndoable, } from './useCreate.undoable.stories'; -import { QueryClient, useMutationState } from '@tanstack/react-query'; +import { MutationMode, Params } from './useCreate.stories'; describe('useCreate', () => { it('returns a callback that can be used with create arguments', async () => { @@ -76,6 +77,44 @@ describe('useCreate', () => { }); }); + it('uses the latest declaration time mutationMode', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + // This story uses the pessimistic mode by default + render(); + fireEvent.click(screen.getByText('Change mutation mode to optimistic')); + fireEvent.click(screen.getByText('Create post')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('mutating')).toBeNull(); + }); + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello World')).not.toBeNull(); + }); + + it('uses the latest declaration time params', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + // This story sends the Hello World title by default + render(); + fireEvent.click(screen.getByText('Change params')); + fireEvent.click(screen.getByText('Create post')); + // Should have changed the title to Goodbye World + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Goodbye World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('mutating')).toBeNull(); + }); + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Goodbye World')).not.toBeNull(); + }); + it('uses call time params over hook time params', async () => { const dataProvider = testDataProvider({ create: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)), diff --git a/packages/ra-core/src/dataProvider/useCreate.stories.tsx b/packages/ra-core/src/dataProvider/useCreate.stories.tsx new file mode 100644 index 00000000000..9601b11e4ab --- /dev/null +++ b/packages/ra-core/src/dataProvider/useCreate.stories.tsx @@ -0,0 +1,210 @@ +import * as React from 'react'; +import { QueryClient, useIsMutating } from '@tanstack/react-query'; + +import { CoreAdminContext } from '../core'; +import { useCreate } from './useCreate'; +import { useGetOne } from './useGetOne'; +import type { MutationMode as MutationModeType } from '../types'; + +export default { title: 'ra-core/dataProvider/useCreate' }; + +export const MutationMode = ({ timeout = 1000 }) => { + const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }]; + const dataProvider = { + getOne: (resource, params) => { + return new Promise((resolve, reject) => { + const data = posts.find(p => p.id === params.id); + setTimeout(() => { + if (!data) { + reject(new Error('nothing yet')); + } + resolve({ data }); + }, timeout); + }); + }, + create: (resource, params) => { + return new Promise(resolve => { + setTimeout(() => { + posts.push(params.data); + resolve({ data: params.data }); + }, timeout); + }); + }, + } as any; + return ( + + + + ); +}; + +const MutationModeCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = React.useState(); + const [mutationMode, setMutationMode] = + React.useState('pessimistic'); + + const { + isPending: isPendingGetOne, + data, + error, + refetch, + } = useGetOne('posts', { id: 2 }); + const [create, { isPending }] = useCreate( + 'posts', + { + data: { id: 2, title: 'Hello World' }, + }, + { + mutationMode, + onSuccess: () => setSuccess('success'), + } + ); + const handleClick = () => { + create(); + }; + return ( + <> + {isPendingGetOne ? ( +

Loading...

+ ) : error ? ( +

{error.message}

+ ) : ( +
+
id
+
{data?.id}
+
title
+
{data?.title}
+
author
+
{data?.author}
+
+ )} +
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; + +export const Params = ({ timeout = 1000 }) => { + const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }]; + const dataProvider = { + getOne: (resource, params) => { + return new Promise((resolve, reject) => { + const data = posts.find(p => p.id === params.id); + setTimeout(() => { + if (!data) { + reject(new Error('nothing yet')); + } + resolve({ data }); + }, timeout); + }); + }, + create: (resource, params) => { + return new Promise(resolve => { + setTimeout(() => { + posts.push(params.data); + resolve({ data: params.data }); + }, timeout); + }); + }, + } as any; + return ( + + + + ); +}; + +const ParamsCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = React.useState(); + const [params, setParams] = React.useState({ title: 'Hello World' }); + + const { + isPending: isPendingGetOne, + data, + error, + refetch, + } = useGetOne('posts', { id: 2 }); + const [create, { isPending }] = useCreate( + 'posts', + { + data: { id: 2, ...params }, + }, + { + mutationMode: 'optimistic', + onSuccess: () => setSuccess('success'), + } + ); + const handleClick = () => { + create(); + }; + return ( + <> + {isPendingGetOne ? ( +

Loading...

+ ) : error ? ( +

{error.message}

+ ) : ( +
+
id
+
{data?.id}
+
title
+
{data?.title}
+
author
+
{data?.author}
+
+ )} +
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; diff --git a/packages/ra-core/src/dataProvider/useCreate.ts b/packages/ra-core/src/dataProvider/useCreate.ts index aa23b526861..0ea0282e3a1 100644 --- a/packages/ra-core/src/dataProvider/useCreate.ts +++ b/packages/ra-core/src/dataProvider/useCreate.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useMutation, UseMutationOptions, @@ -97,9 +97,16 @@ export const useCreate = < ...mutationOptions } = options; const mode = useRef(mutationMode); + useEffect(() => { + mode.current = mutationMode; + }, [mutationMode]); const paramsRef = useRef>>>(params); + useEffect(() => { + paramsRef.current = params; + }, [params]); + const snapshot = useRef([]); // Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted diff --git a/packages/ra-core/src/dataProvider/useDelete.spec.tsx b/packages/ra-core/src/dataProvider/useDelete.spec.tsx index 27ef7583e53..7a1ccbfb5e9 100644 --- a/packages/ra-core/src/dataProvider/useDelete.spec.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.spec.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { screen, render, waitFor } from '@testing-library/react'; +import { screen, render, waitFor, fireEvent } from '@testing-library/react'; import expect from 'expect'; +import { QueryClient, useMutationState } from '@tanstack/react-query'; import { CoreAdminContext } from '../core'; import { RaRecord } from '../types'; @@ -19,7 +20,7 @@ import { ErrorCase as ErrorCaseUndoable, SuccessCase as SuccessCaseUndoable, } from './useDelete.undoable.stories'; -import { QueryClient, useMutationState } from '@tanstack/react-query'; +import { MutationMode, Params } from './useDelete.stories'; describe('useDelete', () => { it('returns a callback that can be used with deleteOne arguments', async () => { @@ -75,6 +76,79 @@ describe('useDelete', () => { }); }); + it('uses the latest declaration time mutationMode', async () => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + // This story uses the pessimistic mode by default + render(); + await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); + fireEvent.click(screen.getByText('Change mutation mode to optimistic')); + fireEvent.click(screen.getByText('Delete first post')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }); + }); + + it('uses the latest declaration time params', async () => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + const posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const dataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + delete: jest.fn((resource, params) => { + console.log('delete', resource, params); + return new Promise(resolve => { + setTimeout(() => { + const index = posts.findIndex(p => p.id === params.id); + posts.splice(index, 1); + resolve({ data: params.previousData }); + }, 1000); + }); + }), + } as any; + // This story has no meta by default + render(); + await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); + fireEvent.click(screen.getByText('Change params')); + fireEvent.click(screen.getByText('Delete first post')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }); + + expect(dataProvider.delete).toHaveBeenCalledWith('posts', { + id: 1, + previousData: { id: 1, title: 'Hello' }, + meta: 'test', + }); + }); + it('uses call time params over hook time params', async () => { const dataProvider = testDataProvider({ delete: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)), diff --git a/packages/ra-core/src/dataProvider/useDelete.stories.tsx b/packages/ra-core/src/dataProvider/useDelete.stories.tsx new file mode 100644 index 00000000000..0cf86f7dc72 --- /dev/null +++ b/packages/ra-core/src/dataProvider/useDelete.stories.tsx @@ -0,0 +1,168 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { QueryClient, useIsMutating } from '@tanstack/react-query'; + +import { CoreAdminContext } from '../core'; +import { useDelete } from './useDelete'; +import { useGetList } from './useGetList'; +import type { DataProvider, MutationMode as MutationModeType } from '../types'; + +export default { title: 'ra-core/dataProvider/useDelete' }; + +export const MutationMode = () => { + const posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const dataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + delete: (resource, params) => { + console.log('delete', resource, params); + return new Promise(resolve => { + setTimeout(() => { + const index = posts.findIndex(p => p.id === params.id); + posts.splice(index, 1); + resolve({ data: params.previousData }); + }, 1000); + }); + }, + } as any; + return ( + + + + ); +}; + +const MutationModeCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetList('posts'); + const [mutationMode, setMutationMode] = + React.useState('pessimistic'); + + const [deleteOne, { isPending }] = useDelete( + 'posts', + { + id: 1, + previousData: { id: 1, title: 'Hello' }, + }, + { + mutationMode, + onSuccess: () => setSuccess('success'), + } + ); + + const handleClick = () => { + deleteOne(); + }; + return ( + <> +
    {data?.map(post =>
  • {post.title}
  • )}
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; + +export const Params = ({ dataProvider }: { dataProvider?: DataProvider }) => { + const posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const defaultDataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + delete: (resource, params) => { + console.log('delete', resource, params); + return new Promise(resolve => { + setTimeout(() => { + const index = posts.findIndex(p => p.id === params.id); + posts.splice(index, 1); + resolve({ data: params.previousData }); + }, 1000); + }); + }, + } as any; + return ( + + + + ); +}; + +const ParamsCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetList('posts'); + const [params, setParams] = React.useState({}); + + const [deleteOne, { isPending }] = useDelete( + 'posts', + { + id: 1, + previousData: { id: 1, title: 'Hello' }, + meta: params.meta, + }, + { + mutationMode: 'optimistic', + onSuccess: () => setSuccess('success'), + } + ); + + const handleClick = () => { + deleteOne(); + }; + return ( + <> +
    {data?.map(post =>
  • {post.title}
  • )}
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; diff --git a/packages/ra-core/src/dataProvider/useDelete.ts b/packages/ra-core/src/dataProvider/useDelete.ts index 586f25a0558..3847b696366 100644 --- a/packages/ra-core/src/dataProvider/useDelete.ts +++ b/packages/ra-core/src/dataProvider/useDelete.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useMutation, useQueryClient, @@ -92,8 +92,17 @@ export const useDelete = < const addUndoableMutation = useAddUndoableMutation(); const { id, previousData } = params; const { mutationMode = 'pessimistic', ...mutationOptions } = options; + const mode = useRef(mutationMode); + useEffect(() => { + mode.current = mutationMode; + }, [mutationMode]); + const paramsRef = useRef>>(params); + useEffect(() => { + paramsRef.current = params; + }, [params]); + const snapshot = useRef([]); const hasCallTimeOnError = useRef(false); const hasCallTimeOnSuccess = useRef(false); diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx b/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx index f586befc369..de1209bd8c8 100644 --- a/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx +++ b/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; -import { waitFor, render, screen } from '@testing-library/react'; +import { waitFor, render, screen, fireEvent } from '@testing-library/react'; import expect from 'expect'; +import { QueryClient, useMutationState } from '@tanstack/react-query'; import { CoreAdminContext } from '../core'; import { testDataProvider } from './testDataProvider'; import { useDeleteMany } from './useDeleteMany'; -import { QueryClient, useMutationState } from '@tanstack/react-query'; +import { MutationMode, Params } from './useDeleteMany.stories'; describe('useDeleteMany', () => { it('returns a callback that can be used with update arguments', async () => { @@ -56,6 +57,79 @@ describe('useDeleteMany', () => { }); }); + it('uses the latest declaration time mutationMode', async () => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + // This story uses the pessimistic mode by default + render(); + await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); + fireEvent.click(screen.getByText('Change mutation mode to optimistic')); + fireEvent.click(screen.getByText('Delete posts')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }); + }); + + it('uses the latest declaration time params', async () => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + let posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const dataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + deleteMany: jest.fn((resource, params) => { + console.log('deleteMany', resource, params); + return new Promise(resolve => { + setTimeout(() => { + posts = posts.filter( + post => !params.ids.includes(post.id) + ); + resolve({ data: params.previousData }); + }, 1000); + }); + }), + } as any; + // This story has no meta by default + render(); + await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); + fireEvent.click(screen.getByText('Change params')); + fireEvent.click(screen.getByText('Delete posts')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello')).toBeNull(); + expect(screen.queryByText('World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }); + + expect(dataProvider.deleteMany).toHaveBeenCalledWith('posts', { + ids: [1], + meta: 'test', + }); + }); + it('uses call time params over hook time params', async () => { const dataProvider = testDataProvider({ deleteMany: jest.fn(() => Promise.resolve({ data: [1, 2] } as any)), diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx b/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx new file mode 100644 index 00000000000..a350d7cb6f2 --- /dev/null +++ b/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { QueryClient, useIsMutating } from '@tanstack/react-query'; + +import { CoreAdminContext } from '../core'; +import { useDeleteMany } from './useDeleteMany'; +import { useGetList } from './useGetList'; +import type { DataProvider, MutationMode as MutationModeType } from '../types'; + +export default { title: 'ra-core/dataProvider/useDeleteMany' }; + +export const MutationMode = () => { + let posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const dataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + deleteMany: (resource, params) => { + console.log('delete', resource, params); + return new Promise(resolve => { + setTimeout(() => { + posts = posts.filter(post => !params.ids.includes(post.id)); + resolve({ data: params.previousData }); + }, 1000); + }); + }, + } as any; + return ( + + + + ); +}; + +const MutationModeCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetList('posts'); + const [mutationMode, setMutationMode] = + React.useState('pessimistic'); + + const [deleteMany, { isPending }] = useDeleteMany( + 'posts', + { + ids: [1], + }, + { + mutationMode, + onSuccess: () => setSuccess('success'), + } + ); + + const handleClick = () => { + deleteMany(); + }; + return ( + <> +
    {data?.map(post =>
  • {post.title}
  • )}
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; + +export const Params = ({ dataProvider }: { dataProvider?: DataProvider }) => { + let posts = [ + { id: 1, title: 'Hello' }, + { id: 2, title: 'World' }, + ]; + const defaultDataProvider = { + getList: (resource, params) => { + console.log('getList', resource, params); + return Promise.resolve({ + data: posts, + total: posts.length, + }); + }, + deleteMany: (resource, params) => { + console.log('deleteMany', resource, params); + return new Promise(resolve => { + setTimeout(() => { + posts = posts.filter(post => !params.ids.includes(post.id)); + resolve({ data: params.previousData }); + }, 1000); + }); + }, + } as any; + return ( + + + + ); +}; + +const ParamsCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetList('posts'); + const [params, setParams] = React.useState({}); + + const [deleteMany, { isPending }] = useDeleteMany( + 'posts', + { + ids: [1], + meta: params.meta, + }, + { + mutationMode: 'optimistic', + onSuccess: () => setSuccess('success'), + } + ); + + const handleClick = () => { + deleteMany(); + }; + return ( + <> +
    {data?.map(post =>
  • {post.title}
  • )}
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.ts b/packages/ra-core/src/dataProvider/useDeleteMany.ts index 92031824e6d..4b7d4080132 100644 --- a/packages/ra-core/src/dataProvider/useDeleteMany.ts +++ b/packages/ra-core/src/dataProvider/useDeleteMany.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useMutation, useQueryClient, @@ -92,8 +92,17 @@ export const useDeleteMany = < const addUndoableMutation = useAddUndoableMutation(); const { ids } = params; const { mutationMode = 'pessimistic', ...mutationOptions } = options; + const mode = useRef(mutationMode); + useEffect(() => { + mode.current = mutationMode; + }, [mutationMode]); + const paramsRef = useRef>>({}); + useEffect(() => { + paramsRef.current = params; + }, [params]); + const snapshot = useRef([]); const hasCallTimeOnError = useRef(false); const hasCallTimeOnSuccess = useRef(false); diff --git a/packages/ra-core/src/dataProvider/useUpdate.spec.tsx b/packages/ra-core/src/dataProvider/useUpdate.spec.tsx index d43d033bd07..72063daf2a9 100644 --- a/packages/ra-core/src/dataProvider/useUpdate.spec.tsx +++ b/packages/ra-core/src/dataProvider/useUpdate.spec.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; -import { act, render, screen, waitFor } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import expect from 'expect'; +import { QueryClient, useMutationState } from '@tanstack/react-query'; import { CoreAdminContext } from '../core'; import { RaRecord } from '../types'; @@ -24,7 +31,7 @@ import { WithMiddlewaresSuccess as WithMiddlewaresSuccessUndoable, WithMiddlewaresError as WithMiddlewaresErrorUndoable, } from './useUpdate.undoable.stories'; -import { QueryClient, useMutationState } from '@tanstack/react-query'; +import { MutationMode, Params } from './useUpdate.stories'; describe('useUpdate', () => { describe('mutate', () => { @@ -92,6 +99,45 @@ describe('useUpdate', () => { }); }); + it('uses the latest declaration time mutationMode', async () => { + // This story uses the pessimistic mode by default + render(); + fireEvent.click( + screen.getByText('Change mutation mode to optimistic') + ); + fireEvent.click(screen.getByText('Update title')); + // Should display the optimistic result right away if the change was handled + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Hello World')).not.toBeNull(); + expect(screen.queryByText('mutating')).toBeNull(); + }); + }); + + it('uses the latest declaration time params', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + // This story sends the Hello World title by default + render(); + fireEvent.click(screen.getByText('Change params')); + fireEvent.click(screen.getByText('Update title')); + // Should have changed the title to Goodbye World + await waitFor(() => { + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Goodbye World')).not.toBeNull(); + expect(screen.queryByText('mutating')).not.toBeNull(); + }); + await waitFor(() => { + expect(screen.queryByText('mutating')).toBeNull(); + }); + expect(screen.queryByText('success')).not.toBeNull(); + expect(screen.queryByText('Goodbye World')).not.toBeNull(); + }); + it('accepts falsy value that are not null nor undefined as the record id', async () => { const dataProvider = { update: jest.fn(() => diff --git a/packages/ra-core/src/dataProvider/useUpdate.stories.tsx b/packages/ra-core/src/dataProvider/useUpdate.stories.tsx new file mode 100644 index 00000000000..7241c180b12 --- /dev/null +++ b/packages/ra-core/src/dataProvider/useUpdate.stories.tsx @@ -0,0 +1,166 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { QueryClient, useIsMutating } from '@tanstack/react-query'; + +import { CoreAdminContext } from '../core'; +import { useUpdate } from './useUpdate'; +import { useGetOne } from './useGetOne'; +import type { MutationMode as MutationModeType } from '../types'; + +export default { title: 'ra-core/dataProvider/useUpdate' }; + +export const MutationMode = ({ timeout = 1000 }) => { + const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }]; + const dataProvider = { + getOne: (resource, params) => { + return Promise.resolve({ + data: posts.find(p => p.id === params.id), + }); + }, + update: (resource, params) => { + return new Promise(resolve => { + setTimeout(() => { + const post = posts.find(p => p.id === params.id); + if (post) { + post.title = params.data.title; + } + resolve({ data: post }); + }, timeout); + }); + }, + } as any; + return ( + + + + ); +}; + +const MutationModeCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetOne('posts', { id: 1 }); + const [mutationMode, setMutationMode] = + React.useState('pessimistic'); + const [update, { isPending }] = useUpdate( + 'posts', + { + id: 1, + data: { title: 'Hello World' }, + }, + { + mutationMode, + onSuccess: () => setSuccess('success'), + } + ); + const handleClick = () => { + update(); + }; + return ( + <> +
+
title
+
{data?.title}
+
author
+
{data?.author}
+
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; + +export const Params = ({ timeout = 1000 }) => { + const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }]; + const dataProvider = { + getOne: (resource, params) => { + return Promise.resolve({ + data: posts.find(p => p.id === params.id), + }); + }, + update: (resource, params) => { + return new Promise(resolve => { + setTimeout(() => { + const post = posts.find(p => p.id === params.id); + if (post) { + post.title = params.data.title; + } + resolve({ data: post }); + }, timeout); + }); + }, + } as any; + return ( + + + + ); +}; + +const ParamsCore = () => { + const isMutating = useIsMutating(); + const [success, setSuccess] = useState(); + const { data, refetch } = useGetOne('posts', { id: 1 }); + const [params, setParams] = React.useState({ title: 'Hello World' }); + + const [update, { isPending }] = useUpdate( + 'posts', + { + id: 1, + data: params, + }, + { + mutationMode: 'optimistic', + onSuccess: () => setSuccess('success'), + } + ); + const handleClick = () => { + update(); + }; + return ( + <> +
+
title
+
{data?.title}
+
author
+
{data?.author}
+
+
+ +   + +   + +
+ {success &&
{success}
} + {isMutating !== 0 &&
mutating
} + + ); +}; diff --git a/packages/ra-core/src/dataProvider/useUpdate.ts b/packages/ra-core/src/dataProvider/useUpdate.ts index b4cd0a055da..1e7388fe53f 100644 --- a/packages/ra-core/src/dataProvider/useUpdate.ts +++ b/packages/ra-core/src/dataProvider/useUpdate.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useMutation, useQueryClient, @@ -98,8 +98,17 @@ export const useUpdate = ( getMutateWithMiddlewares, ...mutationOptions } = options; + const mode = useRef(mutationMode); + useEffect(() => { + mode.current = mutationMode; + }, [mutationMode]); + const paramsRef = useRef>>(params); + useEffect(() => { + paramsRef.current = params; + }, [params]); + const snapshot = useRef([]); // Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted const mutateWithMiddlewares = useRef(dataProvider.update); diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.spec.tsx b/packages/ra-core/src/dataProvider/useUpdateMany.spec.tsx index fa722a9bac8..800c86d6244 100644 --- a/packages/ra-core/src/dataProvider/useUpdateMany.spec.tsx +++ b/packages/ra-core/src/dataProvider/useUpdateMany.spec.tsx @@ -1,12 +1,23 @@ import * as React from 'react'; -import { screen, render, waitFor, act } from '@testing-library/react'; +import { + screen, + render, + waitFor, + act, + fireEvent, +} from '@testing-library/react'; import { QueryClient, useMutationState } from '@tanstack/react-query'; import expect from 'expect'; import { testDataProvider } from './testDataProvider'; import { CoreAdminContext } from '../core'; import { useUpdateMany } from './useUpdateMany'; -import { UndefinedValues, WithMiddlewares } from './useUpdateMany.stories'; +import { + MutationMode, + Params, + UndefinedValues, + WithMiddlewares, +} from './useUpdateMany.stories'; describe('useUpdateMany', () => { it('returns a callback that can be used with update arguments', async () => { @@ -60,6 +71,45 @@ describe('useUpdateMany', () => { }); }); + it('uses the latest declaration time mutationMode', async () => { + // This story uses the pessimistic mode by default + render(); + await screen.findByText( + '[{"id":1,"title":"foo"},{"id":2,"title":"bar"}]' + ); + fireEvent.click(screen.getByText('Change mutation mode to optimistic')); + fireEvent.click(screen.getByText('Update title')); + await screen.findByText( + '[{"id":1,"title":"world"},{"id":2,"title":"world"}]' + ); // and not [{"title":"world"},{"title":"world"}] + }); + + it('uses the latest declaration time params', async () => { + const data = [ + { id: 1, title: 'foo' }, + { id: 2, title: 'bar' }, + ]; + const dataProvider = { + getList: async () => ({ data, total: 2 }), + updateMany: jest.fn(() => new Promise(() => {})), // never resolve to see only optimistic update + } as any; + // This story sends no meta by default + render(); + await screen.findByText( + '[{"id":1,"title":"foo"},{"id":2,"title":"bar"}]' + ); + fireEvent.click(screen.getByText('Change params')); + fireEvent.click(screen.getByText('Update title')); + await screen.findByText( + '[{"id":1,"title":"world"},{"id":2,"title":"world"}]' + ); // and not [{"title":"world"},{"title":"world"}] + expect(dataProvider.updateMany).toHaveBeenCalledWith('posts', { + ids: [1, 2], + data: { title: 'world' }, + meta: 'test', + }); + }); + it('uses callback call time params rather than hook call time params', async () => { const dataProvider = testDataProvider({ updateMany: jest.fn(() => Promise.resolve({ data: [1, 2] } as any)), diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.stories.tsx b/packages/ra-core/src/dataProvider/useUpdateMany.stories.tsx index 4e9ff5ff4f6..1c5b819dc5d 100644 --- a/packages/ra-core/src/dataProvider/useUpdateMany.stories.tsx +++ b/packages/ra-core/src/dataProvider/useUpdateMany.stories.tsx @@ -7,6 +7,7 @@ import { useGetList } from './useGetList'; import { useState } from 'react'; import { useGetOne } from './useGetOne'; import { useTakeUndoableMutation } from './undo'; +import type { DataProvider, MutationMode as MutationModeType } from '../types'; export default { title: 'ra-core/dataProvider/useUpdateMany' }; @@ -202,3 +203,109 @@ const WithMiddlewaresCore = ({ ); }; + +export const MutationMode = () => { + const data = [ + { id: 1, title: 'foo' }, + { id: 2, title: 'bar' }, + ]; + const dataProvider = { + getList: async () => ({ data, total: 2 }), + updateMany: () => new Promise(() => {}), // never resolve to see only optimistic update + } as any; + return ( + + + + ); +}; + +const MutationModeCore = () => { + const { data } = useGetList('posts'); + const [mutationMode, setMutationMode] = + React.useState('pessimistic'); + const [updateMany, { isPending }] = useUpdateMany( + 'posts', + { + ids: [1, 2], + data: { id: undefined, title: 'world' }, + }, + { mutationMode } + ); + const handleClick = () => { + updateMany(); + }; + return ( + <> +
{JSON.stringify(data)}
+
+ +   + +
+ + ); +}; + +export const Params = ({ dataProvider }: { dataProvider?: DataProvider }) => { + const data = [ + { id: 1, title: 'foo' }, + { id: 2, title: 'bar' }, + ]; + const defaultDataProvider = { + getList: async () => ({ data, total: 2 }), + updateMany: () => new Promise(() => {}), // never resolve to see only optimistic update + } as any; + return ( + + + + ); +}; + +const ParamsCore = () => { + const { data } = useGetList('posts'); + const [params, setParams] = React.useState({}); + const [updateMany, { isPending }] = useUpdateMany( + 'posts', + { + ids: [1, 2], + data: { id: undefined, title: 'world' }, + meta: params.meta, + }, + { mutationMode: 'optimistic' } + ); + const handleClick = () => { + updateMany(); + }; + return ( + <> +
{JSON.stringify(data)}
+
+ +   + +
+ + ); +}; diff --git a/packages/ra-core/src/dataProvider/useUpdateMany.ts b/packages/ra-core/src/dataProvider/useUpdateMany.ts index 1f261467d25..0051e37fbc0 100644 --- a/packages/ra-core/src/dataProvider/useUpdateMany.ts +++ b/packages/ra-core/src/dataProvider/useUpdateMany.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useMutation, useQueryClient, @@ -95,9 +95,18 @@ export const useUpdateMany = < getMutateWithMiddlewares, ...mutationOptions } = options; + const mode = useRef(mutationMode); + useEffect(() => { + mode.current = mutationMode; + }, [mutationMode]); + const paramsRef = useRef>>>(params); + useEffect(() => { + paramsRef.current = params; + }, [params]); + const snapshot = useRef([]); // Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted const mutateWithMiddlewares = useRef(dataProvider.updateMany); From 2797c03c1f634e43b0c9f1b35a80d6a5d9e067d5 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:39:54 +0200 Subject: [PATCH 2/3] Remove logs from useDelete stories and tests --- .../dataProvider/useDelete.optimistic.stories.tsx | 12 ++++-------- .../useDelete.pessimistic.stories.tsx | 12 ++++-------- .../ra-core/src/dataProvider/useDelete.spec.tsx | 15 ++------------- .../src/dataProvider/useDelete.stories.tsx | 12 ++++-------- .../dataProvider/useDelete.undoable.stories.tsx | 12 ++++-------- 5 files changed, 18 insertions(+), 45 deletions(-) diff --git a/packages/ra-core/src/dataProvider/useDelete.optimistic.stories.tsx b/packages/ra-core/src/dataProvider/useDelete.optimistic.stories.tsx index 5e6f26d417a..06f16745f53 100644 --- a/packages/ra-core/src/dataProvider/useDelete.optimistic.stories.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.optimistic.stories.tsx @@ -14,15 +14,13 @@ export const SuccessCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: (_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); @@ -82,15 +80,13 @@ export const ErrorCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('something went wrong')); diff --git a/packages/ra-core/src/dataProvider/useDelete.pessimistic.stories.tsx b/packages/ra-core/src/dataProvider/useDelete.pessimistic.stories.tsx index aff6bb90689..29dd18d1d89 100644 --- a/packages/ra-core/src/dataProvider/useDelete.pessimistic.stories.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.pessimistic.stories.tsx @@ -16,15 +16,13 @@ export const SuccessCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: (_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); @@ -181,15 +179,13 @@ export const ErrorCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('something went wrong')); diff --git a/packages/ra-core/src/dataProvider/useDelete.spec.tsx b/packages/ra-core/src/dataProvider/useDelete.spec.tsx index 7a1ccbfb5e9..3e07f66091f 100644 --- a/packages/ra-core/src/dataProvider/useDelete.spec.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.spec.tsx @@ -77,7 +77,6 @@ describe('useDelete', () => { }); it('uses the latest declaration time mutationMode', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); // This story uses the pessimistic mode by default render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); @@ -99,21 +98,18 @@ describe('useDelete', () => { }); it('uses the latest declaration time params', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); const posts = [ { id: 1, title: 'Hello' }, { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: jest.fn((resource, params) => { - console.log('delete', resource, params); + delete: jest.fn((_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); @@ -384,7 +380,6 @@ describe('useDelete', () => { describe('mutationMode', () => { it('when pessimistic, displays result and success side effects when dataProvider promise resolves', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); render(); screen.getByText('Delete first post').click(); await waitFor(() => { @@ -401,7 +396,6 @@ describe('useDelete', () => { expect(screen.queryByText('World')).not.toBeNull(); }); it('when pessimistic, displays error and error side effects when dataProvider promise rejects', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); render(); screen.getByText('Delete first post').click(); @@ -450,7 +444,6 @@ describe('useDelete', () => { ).not.toBeNull(); }); it('when optimistic, displays result and success side effects right away', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); screen.getByText('Delete first post').click(); @@ -468,7 +461,6 @@ describe('useDelete', () => { }); }); it('when optimistic, displays error and error side effects when dataProvider promise rejects', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); @@ -490,7 +482,6 @@ describe('useDelete', () => { }); }); it('when undoable, displays result and success side effects right away and fetched on confirm', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); screen.getByText('Delete first post').click(); @@ -518,7 +509,6 @@ describe('useDelete', () => { ); }); it('when undoable, displays result and success side effects right away and reverts on cancel', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); screen.getByText('Delete first post').click(); @@ -536,7 +526,6 @@ describe('useDelete', () => { }); }); it('when undoable, displays result and success side effects right away and reverts on error', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); diff --git a/packages/ra-core/src/dataProvider/useDelete.stories.tsx b/packages/ra-core/src/dataProvider/useDelete.stories.tsx index 0cf86f7dc72..f52ee316a66 100644 --- a/packages/ra-core/src/dataProvider/useDelete.stories.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.stories.tsx @@ -15,15 +15,13 @@ export const MutationMode = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: (_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); @@ -94,15 +92,13 @@ export const Params = ({ dataProvider }: { dataProvider?: DataProvider }) => { { id: 2, title: 'World' }, ]; const defaultDataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: (_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); diff --git a/packages/ra-core/src/dataProvider/useDelete.undoable.stories.tsx b/packages/ra-core/src/dataProvider/useDelete.undoable.stories.tsx index 6dfbdd72083..e99e9b52999 100644 --- a/packages/ra-core/src/dataProvider/useDelete.undoable.stories.tsx +++ b/packages/ra-core/src/dataProvider/useDelete.undoable.stories.tsx @@ -15,15 +15,13 @@ export const SuccessCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: (_, params) => { return new Promise(resolve => { setTimeout(() => { const index = posts.findIndex(p => p.id === params.id); @@ -112,15 +110,13 @@ export const ErrorCase = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - delete: (resource, params) => { - console.log('delete', resource, params); + delete: () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('something went wrong')); From 0bab27aa9b5b944af8e27425b04daaccc2d90fd8 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:41:11 +0200 Subject: [PATCH 3/3] Remove logs from useDeleteMany stories and tests --- .../ra-core/src/dataProvider/useDeleteMany.spec.tsx | 8 ++------ .../src/dataProvider/useDeleteMany.stories.tsx | 12 ++++-------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx b/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx index de1209bd8c8..ef4f5309022 100644 --- a/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx +++ b/packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx @@ -58,7 +58,6 @@ describe('useDeleteMany', () => { }); it('uses the latest declaration time mutationMode', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); // This story uses the pessimistic mode by default render(); await waitFor(() => new Promise(resolve => setTimeout(resolve, 0))); @@ -80,21 +79,18 @@ describe('useDeleteMany', () => { }); it('uses the latest declaration time params', async () => { - jest.spyOn(console, 'log').mockImplementation(() => {}); let posts = [ { id: 1, title: 'Hello' }, { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - deleteMany: jest.fn((resource, params) => { - console.log('deleteMany', resource, params); + deleteMany: jest.fn((_, params) => { return new Promise(resolve => { setTimeout(() => { posts = posts.filter( diff --git a/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx b/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx index a350d7cb6f2..5bba02a1d57 100644 --- a/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx +++ b/packages/ra-core/src/dataProvider/useDeleteMany.stories.tsx @@ -15,15 +15,13 @@ export const MutationMode = () => { { id: 2, title: 'World' }, ]; const dataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - deleteMany: (resource, params) => { - console.log('delete', resource, params); + deleteMany: (_, params) => { return new Promise(resolve => { setTimeout(() => { posts = posts.filter(post => !params.ids.includes(post.id)); @@ -92,15 +90,13 @@ export const Params = ({ dataProvider }: { dataProvider?: DataProvider }) => { { id: 2, title: 'World' }, ]; const defaultDataProvider = { - getList: (resource, params) => { - console.log('getList', resource, params); + getList: () => { return Promise.resolve({ data: posts, total: posts.length, }); }, - deleteMany: (resource, params) => { - console.log('deleteMany', resource, params); + deleteMany: (_, params) => { return new Promise(resolve => { setTimeout(() => { posts = posts.filter(post => !params.ids.includes(post.id));