Skip to content

Commit 3cc6dd0

Browse files
authored
Merge pull request #9118 from marmelab/fix-use-unique
Fix useUnique should allow value if the only matching record is the current one
2 parents 656f1ba + 5c652f4 commit 3cc6dd0

File tree

3 files changed

+127
-19
lines changed

3 files changed

+127
-19
lines changed

packages/ra-core/src/form/useUnique.spec.tsx

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as React from 'react';
22
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
33
import {
4-
Basic,
4+
Create,
55
DataProviderErrorOnValidation,
66
DeepField,
7+
Edit,
78
WithAdditionalFilters,
89
WithMessage,
910
} from './useUnique.stories';
@@ -27,7 +28,7 @@ describe('useUnique', () => {
2728

2829
it('should show the default error when the field value already exists', async () => {
2930
const dataProvider = baseDataProvider();
30-
render(<Basic dataProvider={dataProvider} />);
31+
render(<Create dataProvider={dataProvider} />);
3132

3233
await screen.findByDisplayValue('John Doe');
3334

@@ -49,6 +50,66 @@ describe('useUnique', () => {
4950
expect(dataProvider.create).not.toHaveBeenCalled();
5051
});
5152

53+
it('should not show the error when the field value already exists but only for the current record', async () => {
54+
const dataProvider = baseDataProvider({
55+
// @ts-ignore
56+
getList: jest.fn((resource, params) =>
57+
params.filter.name === 'John Doe'
58+
? Promise.resolve({
59+
data: [{ id: 1, name: 'John Doe' }],
60+
total: 1,
61+
})
62+
: Promise.resolve({
63+
data: [{ id: 2, name: 'Jane Doe' }],
64+
total: 1,
65+
})
66+
),
67+
// @ts-ignore
68+
getOne: jest.fn(() =>
69+
Promise.resolve({
70+
data: { id: 1, name: 'John Doe' },
71+
})
72+
),
73+
// @ts-ignore
74+
update: jest.fn(() => Promise.resolve({ data: { id: 1 } })),
75+
});
76+
render(<Edit dataProvider={dataProvider} id={1} />);
77+
78+
await waitFor(() =>
79+
expect(dataProvider.getOne).toHaveBeenCalledWith('users', {
80+
id: 1,
81+
})
82+
);
83+
fireEvent.change(screen.getByDisplayValue('John Doe'), {
84+
target: { value: 'Jane Doe' },
85+
});
86+
fireEvent.blur(screen.getByDisplayValue('Jane Doe'));
87+
fireEvent.click(screen.getByText('Submit'));
88+
89+
await waitFor(() =>
90+
expect(dataProvider.getList).toHaveBeenCalledWith('users', {
91+
filter: {
92+
name: 'Jane Doe',
93+
},
94+
pagination: {
95+
page: 1,
96+
perPage: 1,
97+
},
98+
sort: {
99+
field: 'id',
100+
order: 'ASC',
101+
},
102+
})
103+
);
104+
await screen.findByText('Must be unique');
105+
fireEvent.change(screen.getByDisplayValue('Jane Doe'), {
106+
target: { value: 'John Doe' },
107+
});
108+
await waitFor(() =>
109+
expect(screen.queryByText('Must be unique')).toBeNull()
110+
);
111+
});
112+
52113
it('should not show the default error when the field value does not already exist', async () => {
53114
const dataProvider = baseDataProvider({
54115
// @ts-ignore
@@ -60,7 +121,7 @@ describe('useUnique', () => {
60121
),
61122
});
62123

63-
render(<Basic dataProvider={dataProvider} />);
124+
render(<Create dataProvider={dataProvider} />);
64125

65126
await screen.findByDisplayValue('John Doe');
66127
fireEvent.change(screen.getByDisplayValue('John Doe'), {

packages/ra-core/src/form/useUnique.stories.tsx

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CoreAdminContext,
99
CreateBase,
1010
DataProvider,
11+
EditBase,
1112
FormDataConsumer,
1213
mergeTranslations,
1314
useUnique,
@@ -84,7 +85,7 @@ const Wrapper = ({ children, dataProvider = defaultDataProvider }) => {
8485
history={createMemoryHistory()}
8586
queryClient={new QueryClient()}
8687
>
87-
<CreateBase resource="users">{children}</CreateBase>
88+
{children}
8889
</CoreAdminContext>
8990
);
9091
};
@@ -103,10 +104,41 @@ const BasicForm = () => {
103104
);
104105
};
105106

106-
export const Basic = ({ dataProvider }: { dataProvider?: DataProvider }) => {
107+
export const Create = ({ dataProvider }: { dataProvider?: DataProvider }) => {
107108
return (
108109
<Wrapper dataProvider={dataProvider}>
109-
<BasicForm />
110+
<CreateBase resource="users">
111+
<BasicForm />
112+
</CreateBase>
113+
</Wrapper>
114+
);
115+
};
116+
117+
const EditForm = () => {
118+
const unique = useUnique();
119+
return (
120+
<Form defaultValues={{ name: 'John Doe' }}>
121+
<p>
122+
The name field should be unique. Try to enter "John Doe". Jane
123+
Doe should work as this is the current record value
124+
</p>
125+
<Input source="name" defaultValue="" validate={unique()} />
126+
<button type="submit">Submit</button>
127+
</Form>
128+
);
129+
};
130+
export const Edit = ({
131+
dataProvider,
132+
id = 2,
133+
}: {
134+
dataProvider?: DataProvider;
135+
id?: number;
136+
}) => {
137+
return (
138+
<Wrapper dataProvider={dataProvider}>
139+
<EditBase resource="users" id={id}>
140+
<EditForm />
141+
</EditBase>
110142
</Wrapper>
111143
);
112144
};
@@ -142,7 +174,9 @@ export const DeepField = ({
142174
}) => {
143175
return (
144176
<Wrapper dataProvider={dataProvider}>
145-
<DeepFieldForm />
177+
<CreateBase resource="users">
178+
<DeepFieldForm />
179+
</CreateBase>
146180
</Wrapper>
147181
);
148182
};
@@ -176,7 +210,9 @@ export const WithMessage = ({
176210
}) => {
177211
return (
178212
<Wrapper dataProvider={dataProvider}>
179-
<WithMessageForm />
213+
<CreateBase resource="users">
214+
<WithMessageForm />
215+
</CreateBase>
180216
</Wrapper>
181217
);
182218
};
@@ -211,7 +247,9 @@ const WithTranslatedMessageForm = () => {
211247
export const WithTranslatedMessage = () => {
212248
return (
213249
<Wrapper>
214-
<WithTranslatedMessageForm />
250+
<CreateBase resource="users">
251+
<WithTranslatedMessageForm />
252+
</CreateBase>
215253
</Wrapper>
216254
);
217255
};
@@ -251,7 +289,9 @@ export const WithAdditionalFilters = ({
251289
}) => {
252290
return (
253291
<Wrapper dataProvider={dataProvider}>
254-
<WithAdditionalFiltersForm />
292+
<CreateBase resource="users">
293+
<WithAdditionalFiltersForm />
294+
</CreateBase>
255295
</Wrapper>
256296
);
257297
};
@@ -272,8 +312,10 @@ export const DataProviderErrorOnValidation = () => {
272312

273313
return (
274314
<Wrapper dataProvider={errorDataProvider}>
275-
<p>The validation will fail one time over two</p>
276-
<BasicForm />
315+
<CreateBase resource="users">
316+
<p>The validation will fail one time over two</p>
317+
<BasicForm />
318+
</CreateBase>
277319
</Wrapper>
278320
);
279321
};

packages/ra-core/src/form/useUnique.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { InputProps } from './useInput';
66
import { useCallback, useRef } from 'react';
77
import set from 'lodash/set';
88
import { asyncDebounce } from '../util';
9+
import { useRecordContext } from '../controller';
910

1011
/**
1112
* A hook that returns a validation function checking for a record field uniqueness
@@ -54,6 +55,7 @@ export const useUnique = (options?: UseUniqueOptions) => {
5455
const translateLabel = useTranslateLabel();
5556
const resource = useResourceContext(options);
5657
const translate = useTranslate();
58+
const record = useRecordContext();
5759

5860
const debouncedGetList = useRef(
5961
// The initial value is here to set the correct type on useRef
@@ -91,13 +93,16 @@ export const useUnique = (options?: UseUniqueOptions) => {
9193
props.source,
9294
value
9395
);
94-
const { total } = await debouncedGetList.current(resource, {
95-
filter: finalFilter,
96-
pagination: { page: 1, perPage: 1 },
97-
sort: { field: 'id', order: 'ASC' },
98-
});
96+
const { data, total } = await debouncedGetList.current(
97+
resource,
98+
{
99+
filter: finalFilter,
100+
pagination: { page: 1, perPage: 1 },
101+
sort: { field: 'id', order: 'ASC' },
102+
}
103+
);
99104

100-
if (total > 0) {
105+
if (total > 0 && !data.some(r => r.id === record?.id)) {
101106
return translate(message, {
102107
_: message,
103108
source: props.source,
@@ -116,7 +121,7 @@ export const useUnique = (options?: UseUniqueOptions) => {
116121
return undefined;
117122
};
118123
},
119-
[dataProvider, options, resource, translate, translateLabel]
124+
[dataProvider, options, record, resource, translate, translateLabel]
120125
);
121126

122127
return validateUnique;

0 commit comments

Comments
 (0)