Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion packages/ra-core/src/dataProvider/useCreate.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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 { RaRecord } from '../types';
import { testDataProvider } from './testDataProvider';
import { useCreate } from './useCreate';
import { useGetList } from './useGetList';
import { CoreAdminContext } from '../core';
import { Basic } from './useCreate.stories';
import {
ErrorCase as ErrorCasePessimistic,
SuccessCase as SuccessCasePessimistic,
Expand Down Expand Up @@ -334,6 +335,23 @@ describe('useCreate', () => {
expect(screen.queryByText('mutating')).toBeNull();
});
});
it('allows to control the mutation mode', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Basic timeout={10} />);
// Create a post in pessimistic mode
fireEvent.click(await screen.findByText('Create post'));
await screen.findByText('Hello World 2');
fireEvent.click(await screen.findByText('undoable'));
fireEvent.click(await screen.findByText('Increment id'));
await screen.findByText('nothing yet');
// Create a post in undoable mode
fireEvent.click(await screen.findByText('Create post'));
// Check the optimistic result
await screen.findByText('Hello World 3');
// As we haven't confirmed the undoable mutation, refetching the post should return nothing
fireEvent.click(await screen.findByText('Refetch'));
await screen.findByText('nothing yet');
});
});

describe('middlewares', () => {
Expand Down
123 changes: 123 additions & 0 deletions packages/ra-core/src/dataProvider/useCreate.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as React from 'react';
import { useState } from 'react';
import { QueryClient, useIsMutating } from '@tanstack/react-query';

import { CoreAdminContext } from '../core';
import { useCreate } from './useCreate';
import { useGetOne } from './useGetOne';
import { MutationMode } from '../types';

export default { title: 'ra-core/dataProvider/useCreate' };

export const Basic = ({ timeout = 1000 }: { timeout?: number }) => {
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 (
<CoreAdminContext
queryClient={
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
}
dataProvider={dataProvider}
>
<SuccessCore />
</CoreAdminContext>
);
};

Basic.args = {
timeout: 1000,
};

Basic.argTypes = {
timeout: {
control: {
type: 'number',
},
},
};

const SuccessCore = () => {
const isMutating = useIsMutating();
const [success, setSuccess] = useState<string>();
const [id, setId] = useState<number>(2);
const [mutationMode, setMutationMode] =
useState<MutationMode>('pessimistic');
const { data, error, refetch } = useGetOne('posts', { id });
const [create, { isPending }] = useCreate(
'posts',
{},
{
mutationMode,
onSuccess: () => {
setSuccess('success');
},
}
);
const handleClick = () => {
create('posts', {
data: { id, title: `Hello World ${id}` },
});
};
return (
<>
{error ? (
<p>{error.message}</p>
) : (
<dl>
<dt>id</dt>
<dd>{data?.id}</dd>
<dt>title</dt>
<dd>{data?.title}</dd>
<dt>author</dt>
<dd>{data?.author}</dd>
</dl>
)}
<div>
<button onClick={handleClick} disabled={isPending}>
Create post
</button>
&nbsp;
<button onClick={() => refetch()}>Refetch</button>
<button onClick={() => setId(prev => prev + 1)}>
Increment id
</button>
<button onClick={() => setMutationMode('pessimistic')}>
pessimistic
</button>
<button onClick={() => setMutationMode('optimistic')}>
optimistic
</button>
<button onClick={() => setMutationMode('undoable')}>
undoable
</button>
</div>
{success && <div>{success}</div>}
{isMutating !== 0 && <div>mutating</div>}
</>
);
};
6 changes: 5 additions & 1 deletion packages/ra-core/src/dataProvider/useCreate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import {
useMutation,
UseMutationOptions,
Expand Down Expand Up @@ -96,7 +96,11 @@ export const useCreate = <
getMutateWithMiddlewares,
...mutationOptions
} = options;

const mode = useRef<MutationMode>(mutationMode);
useEffect(() => {
mode.current = mutationMode;
}, [mutationMode]);

const paramsRef =
useRef<Partial<CreateParams<Partial<RecordType>>>>(params);
Expand Down
28 changes: 27 additions & 1 deletion packages/ra-core/src/dataProvider/useDelete.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 { CoreAdminContext } from '../core';
Expand All @@ -20,6 +20,7 @@ import {
SuccessCase as SuccessCaseUndoable,
} from './useDelete.undoable.stories';
import { QueryClient } from '@tanstack/react-query';
import { Basic } from './useDelete.stories';

describe('useDelete', () => {
it('returns a callback that can be used with deleteOne arguments', async () => {
Expand Down Expand Up @@ -449,6 +450,31 @@ describe('useDelete', () => {
{ timeout: 4000 }
);
});
it('allows to control the mutation mode', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Basic timeout={10} />);
// Delete the first post in pessimistic mode
await screen.findByText('Hello');
await screen.findByText('World');
fireEvent.click(await screen.findByText('Delete post'));
await screen.findByText('World');
// Wait for the post to be deleted
await waitFor(() => {
expect(screen.queryByText('Hello')).toBeNull();
});

fireEvent.click(await screen.findByText('undoable'));
fireEvent.click(await screen.findByText('Increment id'));
// Delete the second post in undoable mode
fireEvent.click(await screen.findByText('Delete post'));
// Check the optimistic result
await waitFor(() => {
expect(screen.queryByText('World')).toBeNull();
});
// As we haven't confirmed the undoable mutation, refetching the post should return nothing
fireEvent.click(await screen.findByText('Refetch'));
await screen.findByText('World');
});
});

describe('query cache', () => {
Expand Down
93 changes: 93 additions & 0 deletions packages/ra-core/src/dataProvider/useDelete.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 { MutationMode } from '../types';

export default { title: 'ra-core/dataProvider/useDelete' };

export const Basic = ({ timeout = 1000 }: { timeout?: number }) => {
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);
const deletedPost = posts.splice(index, 1);
resolve({ data: deletedPost });
}, timeout);
});
},
} as any;
return (
<CoreAdminContext
queryClient={new QueryClient()}
dataProvider={dataProvider}
>
<SuccessCore />
</CoreAdminContext>
);
};

const SuccessCore = () => {
const isMutating = useIsMutating();
const [success, setSuccess] = useState<string>();
const { data, refetch } = useGetList('posts');
const [id, setId] = useState<number>(1);
const [mutationMode, setMutationMode] =
useState<MutationMode>('pessimistic');
const [deleteOne, { isPending }] = useDelete(
'posts',
{},
{
mutationMode,
onSuccess: () => setSuccess('success'),
}
);
const handleClick = () => {
deleteOne('posts', {
id,
previousData: { id, title: 'Hello' },
});
};
return (
<>
<ul>{data?.map(post => <li key={post.id}>{post.title}</li>)}</ul>
<div>
<button onClick={handleClick} disabled={isPending}>
Delete post
</button>
&nbsp;
<button onClick={() => refetch()}>Refetch</button>
<button onClick={() => setId(prev => prev + 1)}>
Increment id
</button>
<button onClick={() => setMutationMode('pessimistic')}>
pessimistic
</button>
<button onClick={() => setMutationMode('optimistic')}>
optimistic
</button>
<button onClick={() => setMutationMode('undoable')}>
undoable
</button>
</div>
{success && <div>{success}</div>}
{isMutating !== 0 && <div>mutating</div>}
</>
);
};
5 changes: 4 additions & 1 deletion packages/ra-core/src/dataProvider/useDelete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import {
useMutation,
useQueryClient,
Expand Down Expand Up @@ -93,6 +93,9 @@ export const useDelete = <
const { id, previousData } = params;
const { mutationMode = 'pessimistic', ...mutationOptions } = options;
const mode = useRef<MutationMode>(mutationMode);
useEffect(() => {
mode.current = mutationMode;
}, [mutationMode]);
const paramsRef = useRef<Partial<DeleteParams<RecordType>>>(params);
const snapshot = useRef<Snapshot>([]);
const hasCallTimeOnError = useRef(false);
Expand Down
35 changes: 34 additions & 1 deletion packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import { waitFor, render } from '@testing-library/react';
import { waitFor, render, screen, fireEvent } from '@testing-library/react';
import expect from 'expect';

import { CoreAdminContext } from '../core';
import { testDataProvider } from './testDataProvider';
import { useDeleteMany } from './useDeleteMany';
import { QueryClient } from '@tanstack/react-query';
import { Basic } from './useDeleteMany.stories';

describe('useDeleteMany', () => {
it('returns a callback that can be used with update arguments', async () => {
Expand Down Expand Up @@ -263,4 +264,36 @@ describe('useDeleteMany', () => {
});
});
});

it('allows to control the mutation mode', async () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Basic timeout={10} />);
await screen.findByText('Hello World 1');
await screen.findByText('Hello World 2');
await screen.findByText('Hello World 3');
await screen.findByText('Hello World 4');

// Delete the first 2 posts in pessimistic mode
fireEvent.click(await screen.findByText('Delete posts'));
// Wait for the post to be deleted
await waitFor(() => {
expect(screen.queryByText('Hello World 1')).toBeNull();
expect(screen.queryByText('Hello World 2')).toBeNull();
});

fireEvent.click(await screen.findByText('undoable'));
fireEvent.click(await screen.findByText('Increment id'));
// Delete the 2 next posts in undoable mode
fireEvent.click(await screen.findByText('Delete posts'));
// Check the optimistic result
await waitFor(() => {
expect(screen.queryByText('Hello World 3')).toBeNull();
expect(screen.queryByText('Hello World 4')).toBeNull();
});
// As we haven't confirmed the undoable mutation, refetching the post should return nothing
fireEvent.click(await screen.findByText('Refetch'));
await screen.findByText('Hello World 3');
await screen.findByText('Hello World 4');
});
});
Loading
Loading