Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix form validate function no longer applies translation #8746

Merged
merged 2 commits into from
Mar 24, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions packages/ra-core/src/form/getSimpleValidationResolver.spec.ts
Original file line number Diff line number Diff line change
@@ -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' },
},
},
],
2 changes: 1 addition & 1 deletion packages/ra-core/src/form/getSimpleValidationResolver.ts
Original file line number Diff line number Diff line change
@@ -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) =>
65 changes: 64 additions & 1 deletion packages/ra-ui-materialui/src/form/SimpleForm.spec.tsx
Original file line number Diff line number Diff line change
@@ -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('<SimpleForm />', () => {
it('should embed a form with given component children', () => {
@@ -36,4 +37,66 @@ describe('<SimpleForm />', () => {
);
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(<GlobalValidation />);
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(<InputBasedValidation />);
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();
});
});
});
107 changes: 100 additions & 7 deletions packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -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',
},
}) => (
<AdminContext
i18nProvider={{
translate: (x, options) => options?._ ?? x,
changeLocale: () => Promise.resolve(),
getLocale: () => 'en',
}}
i18nProvider={i18nProvider}
dataProvider={testDataProvider({
getOne: () => Promise.resolve({ data }),
})}
@@ -76,3 +84,88 @@ export const NoToolbar = () => (
</SimpleForm>
</Wrapper>
);

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 = () => (
<Wrapper
i18nProvider={{
translate,
changeLocale: () => Promise.resolve(),
getLocale: () => 'en',
}}
>
<SimpleForm validate={validate}>
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleForm>
</Wrapper>
);

export const InputBasedValidation = () => (
<Wrapper
i18nProvider={{
translate,
changeLocale: () => Promise.resolve(),
getLocale: () => 'en',
}}
>
<SimpleForm>
<TextInput
source="title"
fullWidth
validate={required('The title is required')}
/>
<TextInput
source="author"
validate={required('ra.validation.required.author')}
/>
<NumberInput
source="year"
validate={maxValue(2000, 'ra.validation.maxValue')}
/>
</SimpleForm>
</Wrapper>
);