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',
+ }}
+ >
+
+
+
+
+
+
+);