diff --git a/src/create/CreateGuesser.tsx b/src/create/CreateGuesser.tsx index 9780bd0d..92bccc87 100644 --- a/src/create/CreateGuesser.tsx +++ b/src/create/CreateGuesser.tsx @@ -52,6 +52,7 @@ export const IntrospectedCreateGuesser = ({ schema, schemaAnalyzer, resource, + mutationMode = 'pessimistic', mutationOptions, redirect: redirectTo = 'list', mode, @@ -70,6 +71,7 @@ export const IntrospectedCreateGuesser = ({ resource, schemaAnalyzer, fields, + mutationMode, mutationOptions, transform, redirectTo, diff --git a/src/edit/EditGuesser.tsx b/src/edit/EditGuesser.tsx index cd3618b3..038d234a 100644 --- a/src/edit/EditGuesser.tsx +++ b/src/edit/EditGuesser.tsx @@ -76,6 +76,7 @@ export const IntrospectedEditGuesser = ({ schemaAnalyzer, fields, mutationOptions, + mutationMode, transform, redirectTo, children: [], diff --git a/src/types.ts b/src/types.ts index b410c6b1..2a67f5e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -525,4 +525,5 @@ export type UseOnSubmitProps = Pick< 'schemaAnalyzer' | 'resource' | 'fields' > & Pick & - PickRename; + PickRename & + Partial>; diff --git a/src/useOnSubmit.test.tsx b/src/useOnSubmit.test.tsx index 6f8d4462..525ffbf5 100644 --- a/src/useOnSubmit.test.tsx +++ b/src/useOnSubmit.test.tsx @@ -2,11 +2,16 @@ import * as React from 'react'; import { jest } from '@jest/globals'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, waitFor } from '@testing-library/react'; -import type { CreateResult, RaRecord, UpdateResult } from 'react-admin'; -import { DataProviderContext, testDataProvider } from 'react-admin'; +import { + type CreateResult, + DataProviderContext, + type MutationMode, + type RaRecord, + type UpdateResult, + testDataProvider, +} from 'react-admin'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import useOnSubmit from './useOnSubmit.js'; import schemaAnalyzer from './hydra/schemaAnalyzer.js'; import { API_FIELDS_DATA } from './__fixtures__/parsedData.js'; @@ -23,6 +28,16 @@ const onSubmitProps = { }; jest.mock('./getIdentifierValue.js'); +const notify = jest.fn(); +const reactAdminActual = jest.requireActual('react-admin') as Record< + string, + unknown +>; +jest.mock('react-admin', () => ({ + __esModule: true, + ...reactAdminActual, + useNotify: () => notify, +})); test.each([ { @@ -41,6 +56,7 @@ test.each([ ])( 'Call create with file input ($name)', async (values: Omit) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); let save; const Dummy = () => { const onSubmit = useOnSubmit(onSubmitProps); @@ -93,6 +109,7 @@ test.each([ cover: null, }, ])('Call update without file inputs ($name)', async (values: RaRecord) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); let save; const Dummy = () => { const onSubmit = useOnSubmit(onSubmitProps); @@ -125,3 +142,55 @@ test.each([ }); }); }); + +test.each` + submissionErrors | mutationMode | shouldNotify + ${{ name: 'Required' }} | ${'pessimistic'} | ${false} + ${{ name: 'Required' }} | ${'optimistic'} | ${true} + ${{ name: 'Required' }} | ${'undoable'} | ${true} + ${null} | ${'pessimistic'} | ${true} +`( + 'notification handling on validation errors ($submissionErrors, $mutationMode)', + async ({ submissionErrors, mutationMode, shouldNotify }) => { + const { default: useOnSubmit } = await import('./useOnSubmit.js'); + notify.mockClear(); + dataProvider.create = jest.fn(() => + Promise.reject(new Error('Service Unavailable')), + ); + + let save; + const Dummy = () => { + const onSubmit = useOnSubmit({ + ...onSubmitProps, + mutationMode: mutationMode as MutationMode, + schemaAnalyzer: { + ...onSubmitProps.schemaAnalyzer, + getSubmissionErrors: () => submissionErrors, + }, + }); + save = onSubmit; + return ; + }; + + render( + + + + + } /> + + + + , + ); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const errors = await save({ author: 'Author 1' }); + + await waitFor(() => { + expect(dataProvider.create).toHaveBeenCalled(); + }); + (shouldNotify ? expect(notify) : expect(notify).not).toHaveBeenCalled(); + expect(errors).toEqual(submissionErrors ?? {}); + }, +); diff --git a/src/useOnSubmit.ts b/src/useOnSubmit.ts index dbc2a8ef..9f6b5982 100644 --- a/src/useOnSubmit.ts +++ b/src/useOnSubmit.ts @@ -21,6 +21,7 @@ const useOnSubmit = ({ schemaAnalyzer, fields, mutationOptions, + mutationMode = 'pessimistic', transform, redirectTo = 'list', }: UseOnSubmitProps): (( @@ -92,11 +93,14 @@ const useOnSubmit = ({ const failure = mutationOptions?.onError ?? ((error: string | Error) => { - let message = 'ra.notification.http_error'; - if (!submissionErrors) { - message = - typeof error === 'string' ? error : error.message || message; + // Notification will be handled by the useNotifyIsFormInvalid hook. + if (submissionErrors && mutationMode === 'pessimistic') { + return; } + const message = + typeof error === 'string' + ? error + : error.message || 'ra.notification.http_error'; let errorMessage; if (typeof error === 'string') { errorMessage = error; @@ -116,15 +120,13 @@ const useOnSubmit = ({ }, {}, ); - if (submissionErrors) { - return submissionErrors; - } - return {}; + return submissionErrors ?? {}; } }, [ fields, id, + mutationMode, mutationOptions, notify, redirect,