Skip to content

Commit 257ab0b

Browse files
authored
Merge pull request #8882 from marmelab/warnWhenUnsavedChanges-isSubmitting-fix
Fix custom redirect in pessimistic `Edit` or `Create` when using `warnWhenUnsavedChanges` (new approach)
2 parents f8e0e9e + 011c76f commit 257ab0b

3 files changed

Lines changed: 157 additions & 15 deletions

File tree

packages/ra-core/src/controller/create/useCreateController.spec.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1-
import React from 'react';
1+
import { act, fireEvent, render, screen } from '@testing-library/react';
22
import expect from 'expect';
3-
import { render, act } from '@testing-library/react';
4-
import { Location } from 'react-router-dom';
3+
import React from 'react';
4+
import { Location, MemoryRouter, Route, Routes } from 'react-router-dom';
55

6-
import { getRecordFromLocation } from './useCreateController';
7-
import { CreateController } from './CreateController';
6+
import {
7+
CreateContextProvider,
8+
DataProvider,
9+
Form,
10+
InputProps,
11+
useCreateController,
12+
useInput,
13+
} from '../..';
14+
import { CoreAdminContext } from '../../core';
815
import { testDataProvider, useCreate } from '../../dataProvider';
916
import { useNotificationContext } from '../../notification';
10-
import { CoreAdminContext } from '../../core';
1117
import {
1218
Middleware,
1319
SaveContextProvider,
1420
useRegisterMutationMiddleware,
1521
} from '../saveContext';
16-
import { DataProvider } from '../..';
22+
import { CreateController } from './CreateController';
23+
import { getRecordFromLocation } from './useCreateController';
1724

1825
describe('useCreateController', () => {
1926
describe('getRecordFromLocation', () => {
@@ -532,4 +539,61 @@ describe('useCreateController', () => {
532539
data: { foo: 'bar' },
533540
});
534541
});
542+
543+
it('should allow custom redirect with warnWhenUnsavedChanges', async () => {
544+
const dataProvider = testDataProvider({
545+
getOne: () => Promise.resolve({ data: { id: 123 } } as any),
546+
create: (_, { data }) =>
547+
new Promise(resolve =>
548+
setTimeout(
549+
() => resolve({ data: { id: 123, ...data } }),
550+
300
551+
)
552+
),
553+
});
554+
const Input = (props: InputProps) => {
555+
const name = props.source;
556+
const { field } = useInput(props);
557+
return (
558+
<>
559+
<label htmlFor={name}>{name}</label>
560+
<input id={name} type="text" {...field} />
561+
</>
562+
);
563+
};
564+
const CreateView = () => {
565+
const controllerProps = useCreateController({
566+
...defaultProps,
567+
redirect: 'show',
568+
});
569+
return (
570+
<CreateContextProvider value={controllerProps}>
571+
<Form warnWhenUnsavedChanges>
572+
<>
573+
<div>Create</div>
574+
<Input source="foo" />
575+
<input type="submit" value="Submit" />
576+
</>
577+
</Form>
578+
</CreateContextProvider>
579+
);
580+
};
581+
const ShowView = () => <div>Show</div>;
582+
render(
583+
<MemoryRouter initialEntries={['/posts/create']}>
584+
<CoreAdminContext dataProvider={dataProvider}>
585+
<Routes>
586+
<Route path="/posts/create" element={<CreateView />} />
587+
<Route path="/posts/123/show" element={<ShowView />} />
588+
</Routes>
589+
</CoreAdminContext>
590+
</MemoryRouter>
591+
);
592+
await screen.findByText('Create');
593+
fireEvent.change(screen.getByLabelText('foo'), {
594+
target: { value: 'bar' },
595+
});
596+
fireEvent.click(screen.getByText('Submit'));
597+
expect(await screen.findByText('Show')).not.toBeNull();
598+
});
535599
});

packages/ra-core/src/controller/edit/useEditController.spec.tsx

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
import * as React from 'react';
1+
import {
2+
act,
3+
fireEvent,
4+
render,
5+
screen,
6+
waitFor,
7+
} from '@testing-library/react';
28
import expect from 'expect';
3-
import { act, render, screen, waitFor } from '@testing-library/react';
4-
import { Routes, Route } from 'react-router';
59
import { createMemoryHistory } from 'history';
10+
import * as React from 'react';
11+
import { MemoryRouter, Route, Routes } from 'react-router';
612

7-
import { EditController } from './EditController';
8-
import { DataProvider } from '../../types';
13+
import {
14+
EditContextProvider,
15+
SaveContextProvider,
16+
useEditController,
17+
} from '..';
918
import { CoreAdminContext } from '../../core';
10-
import { useNotificationContext } from '../../notification';
11-
import { SaveContextProvider } from '..';
19+
import { testDataProvider, useUpdate } from '../../dataProvider';
1220
import undoableEventEmitter from '../../dataProvider/undoableEventEmitter';
21+
import { Form, InputProps, useInput } from '../../form';
22+
import { useNotificationContext } from '../../notification';
23+
import { DataProvider } from '../../types';
1324
import { Middleware, useRegisterMutationMiddleware } from '../saveContext';
14-
import { testDataProvider, useUpdate } from '../../dataProvider';
25+
import { EditController } from './EditController';
1526

1627
describe('useEditController', () => {
1728
const defaultProps = {
@@ -940,4 +951,63 @@ describe('useEditController', () => {
940951
previousData: { id: 12 },
941952
});
942953
});
954+
955+
it('should allow custom redirect with warnWhenUnsavedChanges in pessimistic mode', async () => {
956+
const dataProvider = testDataProvider({
957+
getOne: () => Promise.resolve({ data: { id: 123 } } as any),
958+
update: (_, { id, data }) =>
959+
new Promise(resolve =>
960+
setTimeout(
961+
() => resolve({ data: { id, ...data } } as any),
962+
300
963+
)
964+
),
965+
});
966+
const Input = (props: InputProps) => {
967+
const name = props.source;
968+
const { field } = useInput(props);
969+
return (
970+
<>
971+
<label htmlFor={name}>{name}</label>
972+
<input id={name} type="text" {...field} />
973+
</>
974+
);
975+
};
976+
const EditView = () => {
977+
const controllerProps = useEditController({
978+
...defaultProps,
979+
id: 123,
980+
redirect: 'show',
981+
mutationMode: 'pessimistic',
982+
});
983+
return (
984+
<EditContextProvider value={controllerProps}>
985+
<Form warnWhenUnsavedChanges>
986+
<>
987+
<div>Edit</div>
988+
<Input source="foo" />
989+
<input type="submit" value="Submit" />
990+
</>
991+
</Form>
992+
</EditContextProvider>
993+
);
994+
};
995+
const ShowView = () => <div>Show</div>;
996+
render(
997+
<MemoryRouter initialEntries={['/posts/123']}>
998+
<CoreAdminContext dataProvider={dataProvider}>
999+
<Routes>
1000+
<Route path="/posts/123" element={<EditView />} />
1001+
<Route path="/posts/123/show" element={<ShowView />} />
1002+
</Routes>
1003+
</CoreAdminContext>
1004+
</MemoryRouter>
1005+
);
1006+
await screen.findByText('Edit');
1007+
fireEvent.change(await screen.findByLabelText('foo'), {
1008+
target: { value: 'bar' },
1009+
});
1010+
fireEvent.click(screen.getByText('Submit'));
1011+
expect(await screen.findByText('Show')).not.toBeNull();
1012+
});
9431013
});

packages/ra-core/src/form/useWarnWhenUnsavedChanges.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export const useWarnWhenUnsavedChanges = (
5555
) {
5656
unblock();
5757
tx.retry();
58+
} else {
59+
if (isSubmitting) {
60+
// Retry the transition (possibly several times) until the form is no longer submitting.
61+
// The value of 100ms is arbitrary, it allows to give some time between retries.
62+
setTimeout(() => {
63+
tx.retry();
64+
}, 100);
65+
}
5866
}
5967
});
6068

0 commit comments

Comments
 (0)