diff --git a/packages/ra-core/src/form/useUnique.spec.tsx b/packages/ra-core/src/form/useUnique.spec.tsx index a307807d881..c30f4544aa2 100644 --- a/packages/ra-core/src/form/useUnique.spec.tsx +++ b/packages/ra-core/src/form/useUnique.spec.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { - Basic, + Create, DataProviderErrorOnValidation, DeepField, + Edit, WithAdditionalFilters, WithMessage, } from './useUnique.stories'; @@ -27,7 +28,7 @@ describe('useUnique', () => { it('should show the default error when the field value already exists', async () => { const dataProvider = baseDataProvider(); - render(<Basic dataProvider={dataProvider} />); + render(<Create dataProvider={dataProvider} />); await screen.findByDisplayValue('John Doe'); @@ -49,6 +50,66 @@ describe('useUnique', () => { expect(dataProvider.create).not.toHaveBeenCalled(); }); + it('should not show the error when the field value already exists but only for the current record', async () => { + const dataProvider = baseDataProvider({ + // @ts-ignore + getList: jest.fn((resource, params) => + params.filter.name === 'John Doe' + ? Promise.resolve({ + data: [{ id: 1, name: 'John Doe' }], + total: 1, + }) + : Promise.resolve({ + data: [{ id: 2, name: 'Jane Doe' }], + total: 1, + }) + ), + // @ts-ignore + getOne: jest.fn(() => + Promise.resolve({ + data: { id: 1, name: 'John Doe' }, + }) + ), + // @ts-ignore + update: jest.fn(() => Promise.resolve({ data: { id: 1 } })), + }); + render(<Edit dataProvider={dataProvider} id={1} />); + + await waitFor(() => + expect(dataProvider.getOne).toHaveBeenCalledWith('users', { + id: 1, + }) + ); + fireEvent.change(screen.getByDisplayValue('John Doe'), { + target: { value: 'Jane Doe' }, + }); + fireEvent.blur(screen.getByDisplayValue('Jane Doe')); + fireEvent.click(screen.getByText('Submit')); + + await waitFor(() => + expect(dataProvider.getList).toHaveBeenCalledWith('users', { + filter: { + name: 'Jane Doe', + }, + pagination: { + page: 1, + perPage: 1, + }, + sort: { + field: 'id', + order: 'ASC', + }, + }) + ); + await screen.findByText('Must be unique'); + fireEvent.change(screen.getByDisplayValue('Jane Doe'), { + target: { value: 'John Doe' }, + }); + await waitFor(() => + expect(screen.queryByText('Must be unique')).toBeNull() + ); + }); + it('should not show the default error when the field value does not already exist', async () => { const dataProvider = baseDataProvider({ // @ts-ignore @@ -60,7 +121,7 @@ describe('useUnique', () => { ), }); - render(<Basic dataProvider={dataProvider} />); + render(<Create dataProvider={dataProvider} />); await screen.findByDisplayValue('John Doe'); fireEvent.change(screen.getByDisplayValue('John Doe'), { diff --git a/packages/ra-core/src/form/useUnique.stories.tsx b/packages/ra-core/src/form/useUnique.stories.tsx index 87d884be106..182edefe9f1 100644 --- a/packages/ra-core/src/form/useUnique.stories.tsx +++ b/packages/ra-core/src/form/useUnique.stories.tsx @@ -8,6 +8,7 @@ import { CoreAdminContext, CreateBase, DataProvider, + EditBase, FormDataConsumer, mergeTranslations, useUnique, @@ -84,7 +85,7 @@ const Wrapper = ({ children, dataProvider = defaultDataProvider }) => { history={createMemoryHistory()} queryClient={new QueryClient()} > - <CreateBase resource="users">{children}</CreateBase> + {children} </CoreAdminContext> ); }; @@ -103,10 +104,41 @@ const BasicForm = () => { ); }; -export const Basic = ({ dataProvider }: { dataProvider?: DataProvider }) => { +export const Create = ({ dataProvider }: { dataProvider?: DataProvider }) => { return ( <Wrapper dataProvider={dataProvider}> - <BasicForm /> + <CreateBase resource="users"> + <BasicForm /> + </CreateBase> + </Wrapper> + ); +}; + +const EditForm = () => { + const unique = useUnique(); + return ( + <Form defaultValues={{ name: 'John Doe' }}> + <p> + The name field should be unique. Try to enter "John Doe". Jane + Doe should work as this is the current record value + </p> + <Input source="name" defaultValue="" validate={unique()} /> + <button type="submit">Submit</button> + </Form> + ); +}; +export const Edit = ({ + dataProvider, + id = 2, +}: { + dataProvider?: DataProvider; + id?: number; +}) => { + return ( + <Wrapper dataProvider={dataProvider}> + <EditBase resource="users" id={id}> + <EditForm /> + </EditBase> </Wrapper> ); }; @@ -142,7 +174,9 @@ export const DeepField = ({ }) => { return ( <Wrapper dataProvider={dataProvider}> - <DeepFieldForm /> + <CreateBase resource="users"> + <DeepFieldForm /> + </CreateBase> </Wrapper> ); }; @@ -176,7 +210,9 @@ export const WithMessage = ({ }) => { return ( <Wrapper dataProvider={dataProvider}> - <WithMessageForm /> + <CreateBase resource="users"> + <WithMessageForm /> + </CreateBase> </Wrapper> ); }; @@ -211,7 +247,9 @@ const WithTranslatedMessageForm = () => { export const WithTranslatedMessage = () => { return ( <Wrapper> - <WithTranslatedMessageForm /> + <CreateBase resource="users"> + <WithTranslatedMessageForm /> + </CreateBase> </Wrapper> ); }; @@ -251,7 +289,9 @@ export const WithAdditionalFilters = ({ }) => { return ( <Wrapper dataProvider={dataProvider}> - <WithAdditionalFiltersForm /> + <CreateBase resource="users"> + <WithAdditionalFiltersForm /> + </CreateBase> </Wrapper> ); }; @@ -272,8 +312,10 @@ export const DataProviderErrorOnValidation = () => { return ( <Wrapper dataProvider={errorDataProvider}> - <p>The validation will fail one time over two</p> - <BasicForm /> + <CreateBase resource="users"> + <p>The validation will fail one time over two</p> + <BasicForm /> + </CreateBase> </Wrapper> ); }; diff --git a/packages/ra-core/src/form/useUnique.ts b/packages/ra-core/src/form/useUnique.ts index 421674814d9..58de868c031 100644 --- a/packages/ra-core/src/form/useUnique.ts +++ b/packages/ra-core/src/form/useUnique.ts @@ -6,6 +6,7 @@ import { InputProps } from './useInput'; import { useCallback, useRef } from 'react'; import set from 'lodash/set'; import { asyncDebounce } from '../util'; +import { useRecordContext } from '../controller'; /** * A hook that returns a validation function checking for a record field uniqueness @@ -54,6 +55,7 @@ export const useUnique = (options?: UseUniqueOptions) => { const translateLabel = useTranslateLabel(); const resource = useResourceContext(options); const translate = useTranslate(); + const record = useRecordContext(); const debouncedGetList = useRef( // The initial value is here to set the correct type on useRef @@ -91,13 +93,16 @@ export const useUnique = (options?: UseUniqueOptions) => { props.source, value ); - const { total } = await debouncedGetList.current(resource, { - filter: finalFilter, - pagination: { page: 1, perPage: 1 }, - sort: { field: 'id', order: 'ASC' }, - }); + const { data, total } = await debouncedGetList.current( + resource, + { + filter: finalFilter, + pagination: { page: 1, perPage: 1 }, + sort: { field: 'id', order: 'ASC' }, + } + ); - if (total > 0) { + if (total > 0 && !data.some(r => r.id === record?.id)) { return translate(message, { _: message, source: props.source, @@ -116,7 +121,7 @@ export const useUnique = (options?: UseUniqueOptions) => { return undefined; }; }, - [dataProvider, options, resource, translate, translateLabel] + [dataProvider, options, record, resource, translate, translateLabel] ); return validateUnique;