Skip to content

Commit 7cf8f78

Browse files
authored
Merge pull request #8746 from marmelab/fix-global-validation-translation
Fix form `validate` function no longer applies translation
2 parents 7cae09a + 6756019 commit 7cf8f78

File tree

4 files changed

+199
-22
lines changed

4 files changed

+199
-22
lines changed

packages/ra-core/src/form/getSimpleValidationResolver.spec.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,29 @@ describe('getSimpleValidationResolver', () => {
1515
expect(result).toEqual({
1616
values: {},
1717
errors: {
18-
title: { type: 'manual', message: 'title too short' },
18+
title: {
19+
type: 'manual',
20+
message: { message: 'title too short' },
21+
},
1922
backlinks: [
2023
{
2124
url: {
2225
type: 'manual',
23-
message: 'url too short',
26+
message: { message: 'url too short' },
2427
},
2528
id: {
2629
type: 'manual',
27-
message: 'missing id',
30+
message: { message: 'missing id' },
2831
},
2932
},
3033
{
3134
url: {
3235
type: 'manual',
33-
message: 'url too short',
36+
message: { message: 'url too short' },
3437
},
3538
id: {
3639
type: 'manual',
37-
message: 'missing id',
40+
message: { message: 'missing id' },
3841
},
3942
},
4043
],
@@ -51,7 +54,10 @@ describe('getSimpleValidationResolver', () => {
5154
expect(result).toEqual({
5255
values: {},
5356
errors: {
54-
title: { type: 'manual', message: 'title too short' },
57+
title: {
58+
type: 'manual',
59+
message: { message: 'title too short' },
60+
},
5561
},
5662
});
5763
});
@@ -65,7 +71,10 @@ describe('getSimpleValidationResolver', () => {
6571
expect(result).toEqual({
6672
values: {},
6773
errors: {
68-
title: { type: 'manual', message: 'title too short' },
74+
title: {
75+
type: 'manual',
76+
message: { message: 'title too short' },
77+
},
6978
},
7079
});
7180
});
@@ -79,7 +88,10 @@ describe('getSimpleValidationResolver', () => {
7988
expect(result).toEqual({
8089
values: {},
8190
errors: {
82-
title: { type: 'manual', message: 'title too short' },
91+
title: {
92+
type: 'manual',
93+
message: { message: 'title too short' },
94+
},
8395
},
8496
});
8597
});
@@ -95,11 +107,14 @@ describe('getSimpleValidationResolver', () => {
95107
expect(result).toEqual({
96108
values: {},
97109
errors: {
98-
title: { type: 'manual', message: 'title too short' },
110+
title: {
111+
type: 'manual',
112+
message: { message: 'title too short' },
113+
},
99114
comment: {
100115
author: {
101116
type: 'manual',
102-
message: 'author is required',
117+
message: { message: 'author is required' },
103118
},
104119
},
105120
},
@@ -118,7 +133,10 @@ describe('getSimpleValidationResolver', () => {
118133
expect(result).toEqual({
119134
values: {},
120135
errors: {
121-
title: { type: 'manual', message: 'title too short' },
136+
title: {
137+
type: 'manual',
138+
message: { message: 'title too short' },
139+
},
122140
average_note: {
123141
type: 'manual',
124142
message: {
@@ -147,7 +165,10 @@ describe('getSimpleValidationResolver', () => {
147165
expect(result).toEqual({
148166
values: {},
149167
errors: {
150-
title: { type: 'manual', message: 'title too short' },
168+
title: {
169+
type: 'manual',
170+
message: { message: 'title too short' },
171+
},
151172
backlinks: [
152173
{
153174
average_note: {
@@ -161,7 +182,7 @@ describe('getSimpleValidationResolver', () => {
161182
{
162183
id: {
163184
type: 'manual',
164-
message: 'missing id',
185+
message: { message: 'missing id' },
165186
},
166187
},
167188
],

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const transformErrorFields = (error: object) => {
9595

9696
const addTypeAndMessage = (error: object) => ({
9797
type: 'manual',
98-
message: error,
98+
message: isRaTranslationObj(error) ? error : { message: error },
9999
});
100100

101101
const isRaTranslationObj = (obj: object) =>

packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx

+64-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, screen } from '@testing-library/react';
3+
import { fireEvent, render, screen } from '@testing-library/react';
44
import { testDataProvider } from 'ra-core';
55

66
import { AdminContext } from '../AdminContext';
77
import { SimpleForm } from './SimpleForm';
88
import { TextInput } from '../input';
9+
import { GlobalValidation, InputBasedValidation } from './SimpleForm.stories';
910

1011
describe('<SimpleForm />', () => {
1112
it('should embed a form with given component children', () => {
@@ -36,4 +37,66 @@ describe('<SimpleForm />', () => {
3637
);
3738
expect(screen.queryByLabelText('ra.action.save')).not.toBeNull();
3839
});
40+
41+
describe('validation', () => {
42+
it('should support translations with global validation', async () => {
43+
const mock = jest
44+
.spyOn(console, 'warn')
45+
.mockImplementation(() => {});
46+
render(<GlobalValidation />);
47+
fireEvent.change(await screen.findByLabelText('Title'), {
48+
target: { value: '' },
49+
});
50+
fireEvent.change(await screen.findByLabelText('Author'), {
51+
target: { value: '' },
52+
});
53+
fireEvent.change(await screen.findByLabelText('Year'), {
54+
target: { value: '2003' },
55+
});
56+
fireEvent.click(await screen.findByLabelText('Save'));
57+
await screen.findByText('The title is required');
58+
await screen.findByText('The author is required');
59+
await screen.findByText('The year must be less than 2000');
60+
expect(mock).toHaveBeenCalledWith(
61+
"Missing translation for key 'The title is required'"
62+
);
63+
expect(mock).not.toHaveBeenCalledWith(
64+
"Missing translation for key 'The author is required'"
65+
);
66+
expect(mock).not.toHaveBeenCalledWith(
67+
"Missing translation for key 'The year must be less than 2000'"
68+
);
69+
mock.mockRestore();
70+
});
71+
72+
it('should support translations with per input validation', async () => {
73+
const mock = jest
74+
.spyOn(console, 'warn')
75+
.mockImplementation(() => {});
76+
render(<InputBasedValidation />);
77+
fireEvent.change(await screen.findByLabelText('Title *'), {
78+
target: { value: '' },
79+
});
80+
fireEvent.change(await screen.findByLabelText('Author *'), {
81+
target: { value: '' },
82+
});
83+
fireEvent.change(await screen.findByLabelText('Year'), {
84+
target: { value: '2003' },
85+
});
86+
fireEvent.click(await screen.findByLabelText('Save'));
87+
await screen.findByText('The title is required');
88+
await screen.findByText('The author is required');
89+
await screen.findByText('The year must be less than 2000');
90+
expect(mock).toHaveBeenCalledWith(
91+
"Missing translation for key 'The title is required'"
92+
);
93+
expect(mock).not.toHaveBeenCalledWith(
94+
"Missing translation for key 'The author is required'"
95+
);
96+
expect(mock).not.toHaveBeenCalledWith(
97+
"Missing translation for key 'The year must be less than 2000'"
98+
);
99+
mock.mockRestore();
100+
});
101+
});
39102
});

packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx

+100-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as React from 'react';
2-
import { ResourceContextProvider, testDataProvider } from 'ra-core';
2+
import {
3+
maxValue,
4+
required,
5+
ResourceContextProvider,
6+
testDataProvider,
7+
} from 'ra-core';
38

49
import { AdminContext } from '../AdminContext';
510
import { Edit } from '../detail';
@@ -16,13 +21,16 @@ const data = {
1621
year: 1869,
1722
};
1823

19-
const Wrapper = ({ children }) => (
24+
const Wrapper = ({
25+
children,
26+
i18nProvider = {
27+
translate: (x, options) => options?._ ?? x,
28+
changeLocale: () => Promise.resolve(),
29+
getLocale: () => 'en',
30+
},
31+
}) => (
2032
<AdminContext
21-
i18nProvider={{
22-
translate: (x, options) => options?._ ?? x,
23-
changeLocale: () => Promise.resolve(),
24-
getLocale: () => 'en',
25-
}}
33+
i18nProvider={i18nProvider}
2634
dataProvider={testDataProvider({
2735
getOne: () => Promise.resolve({ data }),
2836
})}
@@ -76,3 +84,88 @@ export const NoToolbar = () => (
7684
</SimpleForm>
7785
</Wrapper>
7886
);
87+
88+
const translate = (x, options) => {
89+
switch (x) {
90+
case 'resources.books.name':
91+
return 'Books';
92+
case 'ra.page.edit':
93+
return 'Edit';
94+
case 'resources.books.fields.title':
95+
return 'Title';
96+
case 'resources.books.fields.author':
97+
return 'Author';
98+
case 'resources.books.fields.year':
99+
return 'Year';
100+
case 'ra.action.save':
101+
return 'Save';
102+
case 'ra.action.delete':
103+
return 'Delete';
104+
case 'ra.validation.required.author':
105+
return 'The author is required';
106+
case 'ra.validation.maxValue':
107+
return `The year must be less than ${options.max}`;
108+
default:
109+
console.warn(`Missing translation for key '${x}'`);
110+
return options?._ ?? x;
111+
}
112+
};
113+
114+
const validate = values => {
115+
const errors = {} as any;
116+
if (!values.title) {
117+
errors.title = 'The title is required';
118+
}
119+
if (!values.author) {
120+
errors.author = 'ra.validation.required.author';
121+
}
122+
if (values.year > 2000) {
123+
errors.year = {
124+
message: 'ra.validation.maxValue',
125+
args: { max: 2000 },
126+
};
127+
}
128+
return errors;
129+
};
130+
131+
export const GlobalValidation = () => (
132+
<Wrapper
133+
i18nProvider={{
134+
translate,
135+
changeLocale: () => Promise.resolve(),
136+
getLocale: () => 'en',
137+
}}
138+
>
139+
<SimpleForm validate={validate}>
140+
<TextInput source="title" fullWidth />
141+
<TextInput source="author" />
142+
<NumberInput source="year" />
143+
</SimpleForm>
144+
</Wrapper>
145+
);
146+
147+
export const InputBasedValidation = () => (
148+
<Wrapper
149+
i18nProvider={{
150+
translate,
151+
changeLocale: () => Promise.resolve(),
152+
getLocale: () => 'en',
153+
}}
154+
>
155+
<SimpleForm>
156+
<TextInput
157+
source="title"
158+
fullWidth
159+
validate={required('The title is required')}
160+
/>
161+
<TextInput
162+
source="author"
163+
validate={required('ra.validation.required.author')}
164+
/>
165+
<NumberInput
166+
source="year"
167+
validate={maxValue(2000, 'ra.validation.maxValue')}
168+
/>
169+
</SimpleForm>
170+
</Wrapper>
171+
);

0 commit comments

Comments
 (0)