diff --git a/packages/ra-core/src/form/getSimpleValidationResolver.spec.ts b/packages/ra-core/src/form/getSimpleValidationResolver.spec.ts index 1840458451a..12baecf82a3 100644 --- a/packages/ra-core/src/form/getSimpleValidationResolver.spec.ts +++ b/packages/ra-core/src/form/getSimpleValidationResolver.spec.ts @@ -15,26 +15,29 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, backlinks: [ { url: { type: 'manual', - message: 'url too short', + message: { message: 'url too short' }, }, id: { type: 'manual', - message: 'missing id', + message: { message: 'missing id' }, }, }, { url: { type: 'manual', - message: 'url too short', + message: { message: 'url too short' }, }, id: { type: 'manual', - message: 'missing id', + message: { message: 'missing id' }, }, }, ], @@ -51,7 +54,10 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, }, }); }); @@ -65,7 +71,10 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, }, }); }); @@ -79,7 +88,10 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, }, }); }); @@ -95,11 +107,14 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, comment: { author: { type: 'manual', - message: 'author is required', + message: { message: 'author is required' }, }, }, }, @@ -118,7 +133,10 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, average_note: { type: 'manual', message: { @@ -147,7 +165,10 @@ describe('getSimpleValidationResolver', () => { expect(result).toEqual({ values: {}, errors: { - title: { type: 'manual', message: 'title too short' }, + title: { + type: 'manual', + message: { message: 'title too short' }, + }, backlinks: [ { average_note: { @@ -161,7 +182,7 @@ describe('getSimpleValidationResolver', () => { { id: { type: 'manual', - message: 'missing id', + message: { message: 'missing id' }, }, }, ], diff --git a/packages/ra-core/src/form/getSimpleValidationResolver.ts b/packages/ra-core/src/form/getSimpleValidationResolver.ts index f1041307228..542f21d7609 100644 --- a/packages/ra-core/src/form/getSimpleValidationResolver.ts +++ b/packages/ra-core/src/form/getSimpleValidationResolver.ts @@ -95,7 +95,7 @@ const transformErrorFields = (error: object) => { const addTypeAndMessage = (error: object) => ({ type: 'manual', - message: error, + message: isRaTranslationObj(error) ? error : { message: error }, }); const isRaTranslationObj = (obj: object) => diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx index 6c4b9cb4ff6..e5908499b52 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import expect from 'expect'; -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { testDataProvider } from 'ra-core'; import { AdminContext } from '../AdminContext'; import { SimpleForm } from './SimpleForm'; import { TextInput } from '../input'; +import { GlobalValidation, InputBasedValidation } from './SimpleForm.stories'; describe('', () => { it('should embed a form with given component children', () => { @@ -36,4 +37,66 @@ describe('', () => { ); expect(screen.queryByLabelText('ra.action.save')).not.toBeNull(); }); + + describe('validation', () => { + it('should support translations with global validation', async () => { + const mock = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + render(); + fireEvent.change(await screen.findByLabelText('Title'), { + target: { value: '' }, + }); + fireEvent.change(await screen.findByLabelText('Author'), { + target: { value: '' }, + }); + fireEvent.change(await screen.findByLabelText('Year'), { + target: { value: '2003' }, + }); + fireEvent.click(await screen.findByLabelText('Save')); + await screen.findByText('The title is required'); + await screen.findByText('The author is required'); + await screen.findByText('The year must be less than 2000'); + expect(mock).toHaveBeenCalledWith( + "Missing translation for key 'The title is required'" + ); + expect(mock).not.toHaveBeenCalledWith( + "Missing translation for key 'The author is required'" + ); + expect(mock).not.toHaveBeenCalledWith( + "Missing translation for key 'The year must be less than 2000'" + ); + mock.mockRestore(); + }); + + it('should support translations with per input validation', async () => { + const mock = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + render(); + fireEvent.change(await screen.findByLabelText('Title *'), { + target: { value: '' }, + }); + fireEvent.change(await screen.findByLabelText('Author *'), { + target: { value: '' }, + }); + fireEvent.change(await screen.findByLabelText('Year'), { + target: { value: '2003' }, + }); + fireEvent.click(await screen.findByLabelText('Save')); + await screen.findByText('The title is required'); + await screen.findByText('The author is required'); + await screen.findByText('The year must be less than 2000'); + expect(mock).toHaveBeenCalledWith( + "Missing translation for key 'The title is required'" + ); + expect(mock).not.toHaveBeenCalledWith( + "Missing translation for key 'The author is required'" + ); + expect(mock).not.toHaveBeenCalledWith( + "Missing translation for key 'The year must be less than 2000'" + ); + mock.mockRestore(); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx index 0f2343596cb..d5905a8971d 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx @@ -1,5 +1,10 @@ import * as React from 'react'; -import { ResourceContextProvider, testDataProvider } from 'ra-core'; +import { + maxValue, + required, + ResourceContextProvider, + testDataProvider, +} from 'ra-core'; import { AdminContext } from '../AdminContext'; import { Edit } from '../detail'; @@ -16,13 +21,16 @@ const data = { year: 1869, }; -const Wrapper = ({ children }) => ( +const Wrapper = ({ + children, + i18nProvider = { + translate: (x, options) => options?._ ?? x, + changeLocale: () => Promise.resolve(), + getLocale: () => 'en', + }, +}) => ( options?._ ?? x, - changeLocale: () => Promise.resolve(), - getLocale: () => 'en', - }} + i18nProvider={i18nProvider} dataProvider={testDataProvider({ getOne: () => Promise.resolve({ data }), })} @@ -76,3 +84,88 @@ export const NoToolbar = () => ( ); + +const translate = (x, options) => { + switch (x) { + case 'resources.books.name': + return 'Books'; + case 'ra.page.edit': + return 'Edit'; + case 'resources.books.fields.title': + return 'Title'; + case 'resources.books.fields.author': + return 'Author'; + case 'resources.books.fields.year': + return 'Year'; + case 'ra.action.save': + return 'Save'; + case 'ra.action.delete': + return 'Delete'; + case 'ra.validation.required.author': + return 'The author is required'; + case 'ra.validation.maxValue': + return `The year must be less than ${options.max}`; + default: + console.warn(`Missing translation for key '${x}'`); + return options?._ ?? x; + } +}; + +const validate = values => { + const errors = {} as any; + if (!values.title) { + errors.title = 'The title is required'; + } + if (!values.author) { + errors.author = 'ra.validation.required.author'; + } + if (values.year > 2000) { + errors.year = { + message: 'ra.validation.maxValue', + args: { max: 2000 }, + }; + } + return errors; +}; + +export const GlobalValidation = () => ( + Promise.resolve(), + getLocale: () => 'en', + }} + > + + + + + + +); + +export const InputBasedValidation = () => ( + Promise.resolve(), + getLocale: () => 'en', + }} + > + + + + + + +);