Skip to content

Commit 3ee2989

Browse files
committed
add useUpdateMany middleware support
1 parent 2bd2320 commit 3ee2989

File tree

3 files changed

+380
-7
lines changed

3 files changed

+380
-7
lines changed

packages/ra-core/src/dataProvider/useUpdateMany.spec.tsx

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as React from 'react';
2-
import { screen, render, waitFor } from '@testing-library/react';
2+
import { screen, render, waitFor, act } from '@testing-library/react';
33
import { QueryClient } from '@tanstack/react-query';
44
import expect from 'expect';
55

66
import { testDataProvider } from './testDataProvider';
77
import { CoreAdminContext } from '../core';
88
import { useUpdateMany } from './useUpdateMany';
9-
import { UndefinedValues } from './useUpdateMany.stories';
9+
import { UndefinedValues, WithMiddlewares } from './useUpdateMany.stories';
1010

1111
describe('useUpdateMany', () => {
1212
it('returns a callback that can be used with update arguments', async () => {
@@ -436,4 +436,198 @@ describe('useUpdateMany', () => {
436436
); // and not [{"title":"world"},{"title":"world"}]
437437
});
438438
});
439+
440+
describe('middlewares', () => {
441+
it('when pessimistic, it accepts middlewares and displays result and success side effects when dataProvider promise resolves', async () => {
442+
render(<WithMiddlewares mutationMode="pessimistic" timeout={10} />);
443+
screen.getByText('Update title').click();
444+
await waitFor(() => {
445+
expect(screen.queryByText('success')).toBeNull();
446+
expect(
447+
screen.queryByText('Hello World from middleware')
448+
).toBeNull();
449+
expect(screen.queryByText('mutating')).not.toBeNull();
450+
});
451+
await waitFor(() => {
452+
expect(screen.queryByText('success')).not.toBeNull();
453+
expect(
454+
// We could expect 'Hello World from middleware' here, but
455+
// updateMany's result only contains the ids, not the updated data
456+
// so the cache can only be updated with the call-time params,
457+
// which do not include the middleware's result.
458+
// I guess it's OK for most cases though...
459+
screen.queryByText('Hello World')
460+
).not.toBeNull();
461+
expect(screen.queryByText('mutating')).toBeNull();
462+
});
463+
screen.getByText('Refetch').click();
464+
await waitFor(() => {
465+
expect(screen.queryByText('success')).not.toBeNull();
466+
expect(
467+
screen.queryByText('Hello World from middleware')
468+
).not.toBeNull();
469+
expect(screen.queryByText('mutating')).toBeNull();
470+
});
471+
});
472+
473+
it('when pessimistic, it accepts middlewares and displays error and error side effects when dataProvider promise rejects', async () => {
474+
jest.spyOn(console, 'error').mockImplementation(() => {});
475+
render(
476+
<WithMiddlewares
477+
mutationMode="pessimistic"
478+
shouldError
479+
timeout={10}
480+
/>
481+
);
482+
screen.getByText('Update title').click();
483+
await waitFor(() => {
484+
expect(screen.queryByText('success')).toBeNull();
485+
expect(screen.queryByText('something went wrong')).toBeNull();
486+
expect(
487+
screen.queryByText('Hello World from middleware')
488+
).toBeNull();
489+
expect(screen.queryByText('mutating')).not.toBeNull();
490+
});
491+
await waitFor(() => {
492+
expect(screen.queryByText('success')).toBeNull();
493+
expect(
494+
screen.queryByText('something went wrong')
495+
).not.toBeNull();
496+
expect(
497+
screen.queryByText('Hello World from middleware')
498+
).toBeNull();
499+
expect(screen.queryByText('mutating')).toBeNull();
500+
});
501+
});
502+
503+
it('when optimistic, it accepts middlewares and displays result and success side effects right away', async () => {
504+
render(<WithMiddlewares mutationMode="optimistic" timeout={10} />);
505+
screen.getByText('Update title').click();
506+
await waitFor(() => {
507+
expect(screen.queryByText('success')).not.toBeNull();
508+
expect(
509+
screen.queryByText('Hello World from middleware')
510+
).not.toBeNull();
511+
});
512+
await waitFor(() => {
513+
expect(screen.queryByText('success')).not.toBeNull();
514+
expect(
515+
screen.queryByText('Hello World from middleware')
516+
).not.toBeNull();
517+
expect(screen.queryByText('mutating')).toBeNull();
518+
});
519+
});
520+
it('when optimistic, it accepts middlewares and displays error and error side effects when dataProvider promise rejects', async () => {
521+
jest.spyOn(console, 'error').mockImplementation(() => {});
522+
render(
523+
<WithMiddlewares
524+
mutationMode="optimistic"
525+
shouldError
526+
timeout={10}
527+
/>
528+
);
529+
screen.getByText('Update title').click();
530+
await waitFor(() => {
531+
expect(screen.queryByText('success')).not.toBeNull();
532+
expect(screen.queryByText('Hello World')).not.toBeNull();
533+
expect(screen.queryByText('mutating')).not.toBeNull();
534+
});
535+
await waitFor(() => {
536+
expect(screen.queryByText('success')).toBeNull();
537+
expect(
538+
screen.queryByText('something went wrong')
539+
).not.toBeNull();
540+
expect(
541+
screen.queryByText('Hello World from middleware')
542+
).toBeNull();
543+
expect(screen.queryByText('mutating')).toBeNull();
544+
});
545+
await screen.findByText('Hello');
546+
});
547+
548+
it('when undoable, it accepts middlewares and displays result and success side effects right away and fetched on confirm', async () => {
549+
render(<WithMiddlewares mutationMode="undoable" timeout={10} />);
550+
act(() => {
551+
screen.getByText('Update title').click();
552+
});
553+
await waitFor(() => {
554+
expect(screen.queryByText('success')).not.toBeNull();
555+
expect(screen.queryByText('Hello World')).not.toBeNull();
556+
expect(screen.queryByText('mutating')).toBeNull();
557+
});
558+
act(() => {
559+
screen.getByText('Confirm').click();
560+
});
561+
await waitFor(() => {
562+
expect(screen.queryByText('success')).not.toBeNull();
563+
expect(screen.queryByText('Hello World')).not.toBeNull();
564+
expect(screen.queryByText('mutating')).not.toBeNull();
565+
});
566+
await waitFor(
567+
() => {
568+
expect(screen.queryByText('mutating')).toBeNull();
569+
},
570+
{ timeout: 4000 }
571+
);
572+
expect(screen.queryByText('success')).not.toBeNull();
573+
expect(
574+
screen.queryByText('Hello World from middleware')
575+
).not.toBeNull();
576+
});
577+
it('when undoable, it accepts middlewares and displays result and success side effects right away and reverts on cancel', async () => {
578+
render(<WithMiddlewares mutationMode="undoable" timeout={10} />);
579+
await screen.findByText('Hello');
580+
act(() => {
581+
screen.getByText('Update title').click();
582+
});
583+
await waitFor(() => {
584+
expect(screen.queryByText('success')).not.toBeNull();
585+
expect(screen.queryByText('Hello World')).not.toBeNull();
586+
expect(screen.queryByText('mutating')).toBeNull();
587+
});
588+
act(() => {
589+
screen.getByText('Cancel').click();
590+
});
591+
await waitFor(() => {
592+
expect(screen.queryByText('Hello World')).toBeNull();
593+
});
594+
expect(screen.queryByText('mutating')).toBeNull();
595+
await screen.findByText('Hello');
596+
});
597+
it('when undoable, it accepts middlewares and displays result and success side effects right away and reverts on error', async () => {
598+
jest.spyOn(console, 'error').mockImplementation(() => {});
599+
render(
600+
<WithMiddlewares
601+
mutationMode="undoable"
602+
shouldError
603+
timeout={10}
604+
/>
605+
);
606+
await screen.findByText('Hello');
607+
screen.getByText('Update title').click();
608+
await waitFor(() => {
609+
expect(screen.queryByText('success')).not.toBeNull();
610+
expect(screen.queryByText('Hello World')).not.toBeNull();
611+
expect(screen.queryByText('mutating')).toBeNull();
612+
});
613+
screen.getByText('Confirm').click();
614+
await waitFor(() => {
615+
expect(screen.queryByText('success')).not.toBeNull();
616+
expect(screen.queryByText('Hello World')).not.toBeNull();
617+
expect(screen.queryByText('mutating')).not.toBeNull();
618+
});
619+
await screen.findByText('Hello', undefined, { timeout: 4000 });
620+
await waitFor(() => {
621+
expect(screen.queryByText('success')).toBeNull();
622+
expect(
623+
screen.queryByText('Hello World from middleware')
624+
).toBeNull();
625+
expect(screen.queryByText('mutating')).toBeNull();
626+
});
627+
});
628+
});
629+
});
630+
631+
afterEach(() => {
632+
jest.restoreAllMocks();
439633
});

packages/ra-core/src/dataProvider/useUpdateMany.stories.tsx

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as React from 'react';
2-
import { QueryClient } from '@tanstack/react-query';
2+
import { QueryClient, useIsMutating } from '@tanstack/react-query';
33

44
import { CoreAdminContext } from '../core';
55
import { useUpdateMany } from './useUpdateMany';
66
import { useGetList } from './useGetList';
7+
import { useState } from 'react';
8+
import { useGetOne } from './useGetOne';
9+
import { useTakeUndoableMutation } from './undo';
710

811
export default { title: 'ra-core/dataProvider/useUpdateMany' };
912

@@ -50,3 +53,152 @@ const UndefinedValuesCore = () => {
5053
</>
5154
);
5255
};
56+
57+
export const WithMiddlewares = ({
58+
timeout = 1000,
59+
mutationMode,
60+
shouldError,
61+
}: {
62+
timeout?: number;
63+
mutationMode: 'optimistic' | 'pessimistic' | 'undoable';
64+
shouldError?: boolean;
65+
}) => {
66+
const posts = [{ id: 1, title: 'Hello', author: 'John Doe' }];
67+
const dataProvider = {
68+
getOne: () => {
69+
return Promise.resolve({
70+
data: posts[0],
71+
});
72+
},
73+
updateMany: (resource, params) => {
74+
return new Promise((resolve, reject) => {
75+
setTimeout(() => {
76+
if (shouldError) {
77+
return reject(new Error('something went wrong'));
78+
}
79+
posts[0].title = params.data.title;
80+
resolve({ data: [1] });
81+
}, timeout);
82+
});
83+
},
84+
} as any;
85+
return (
86+
<CoreAdminContext dataProvider={dataProvider}>
87+
<WithMiddlewaresCore mutationMode={mutationMode} />
88+
</CoreAdminContext>
89+
);
90+
};
91+
WithMiddlewares.args = {
92+
timeout: 1000,
93+
mutationMode: 'optimistic',
94+
shouldError: false,
95+
};
96+
WithMiddlewares.argTypes = {
97+
timeout: {
98+
control: { type: 'number' },
99+
},
100+
mutationMode: {
101+
options: ['optimistic', 'pessimistic', 'undoable'],
102+
control: { type: 'select' },
103+
},
104+
shouldError: {
105+
control: { type: 'boolean' },
106+
},
107+
};
108+
109+
const WithMiddlewaresCore = ({
110+
mutationMode,
111+
}: {
112+
mutationMode: 'optimistic' | 'pessimistic' | 'undoable';
113+
}) => {
114+
const isMutating = useIsMutating();
115+
const [notification, setNotification] = useState<boolean>(false);
116+
const [success, setSuccess] = useState<string>();
117+
const [error, setError] = useState<any>();
118+
119+
const takeMutation = useTakeUndoableMutation();
120+
121+
const { data, refetch } = useGetOne('posts', { id: 1 });
122+
const [updateMany, { isPending }] = useUpdateMany(
123+
'posts',
124+
{
125+
ids: [1],
126+
data: { title: 'Hello World' },
127+
},
128+
{
129+
mutationMode,
130+
// @ts-ignore
131+
getMutateWithMiddlewares: mutate => async (resource, params) => {
132+
return mutate(resource, {
133+
...params,
134+
data: { title: `${params.data.title} from middleware` },
135+
});
136+
},
137+
}
138+
);
139+
const handleClick = () => {
140+
updateMany(
141+
'posts',
142+
{
143+
ids: [1],
144+
data: { title: 'Hello World' },
145+
},
146+
{
147+
onSuccess: () => setSuccess('success'),
148+
onError: e => {
149+
setError(e);
150+
setSuccess('');
151+
},
152+
}
153+
);
154+
if (mutationMode === 'undoable') {
155+
setNotification(true);
156+
}
157+
};
158+
return (
159+
<>
160+
<dl>
161+
<dt>title</dt>
162+
<dd>{data?.title}</dd>
163+
<dt>author</dt>
164+
<dd>{data?.author}</dd>
165+
</dl>
166+
<div>
167+
{notification ? (
168+
<>
169+
<button
170+
onClick={() => {
171+
setNotification(false);
172+
const mutation = takeMutation();
173+
if (!mutation) return;
174+
mutation({ isUndo: false });
175+
}}
176+
>
177+
Confirm
178+
</button>
179+
&nbsp;
180+
<button
181+
onClick={() => {
182+
setNotification(false);
183+
const mutation = takeMutation();
184+
if (!mutation) return;
185+
mutation({ isUndo: true });
186+
}}
187+
>
188+
Cancel
189+
</button>
190+
</>
191+
) : (
192+
<button onClick={handleClick} disabled={isPending}>
193+
Update title
194+
</button>
195+
)}
196+
&nbsp;
197+
<button onClick={() => refetch()}>Refetch</button>
198+
</div>
199+
{success && <div>{success}</div>}
200+
{error && <div>{error.message}</div>}
201+
{isMutating !== 0 && <div>mutating</div>}
202+
</>
203+
);
204+
};

0 commit comments

Comments
 (0)