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 validation errors from resolvers are not translated #9191

Merged
merged 9 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"watch": "tsc --outDir dist/esm --module es2015 --watch"
},
"devDependencies": {
"@hookform/resolvers": "^2.8.8",
"@hookform/resolvers": "^3.2.0",
"@testing-library/react": "^11.2.3",
"@testing-library/react-hooks": "^7.0.2",
"@types/jest": "^29.5.2",
Expand All @@ -46,7 +46,8 @@
"recharts": "^2.1.15",
"rimraf": "^3.0.2",
"typescript": "^5.1.3",
"yup": "^0.32.11"
"yup": "^0.32.11",
"zod": "^3.22.1"
},
"peerDependencies": {
"history": "^5.1.0",
Expand Down
35 changes: 35 additions & 0 deletions packages/ra-core/src/form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
UseControllerProps,
useFormState,
} from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

import { CoreAdminContext } from '../core';
import { Form } from './Form';
Expand Down Expand Up @@ -164,3 +166,36 @@ export const UndefinedValue = () => {
</CoreAdminContext>
);
};

const zodSchema = z.object({
preTranslated: z.string().min(5, { message: 'This field is required' }),
translationKey: z.string().min(5, { message: 'ra.validation.required' }),
});
export const ZodResolver = () => {
const [result, setResult] = React.useState<any>();
return (
<CoreAdminContext>
<Form
record={{}}
onSubmit={data => setResult(data)}
resolver={zodResolver(zodSchema)}
>
<p>
This field has "This field is required" as its error message
in the zod schema. We shouldn't see a missing translation
error:
</p>
<Input source="preTranslated" />
<br />
<br />
<p>
This field has "ra.validation.required" as its error message
in the zod schema:
</p>
<Input source="translationKey" />
<button type="submit">Submit</button>
</Form>
<pre>{JSON.stringify(result, null, 2)}</pre>
</CoreAdminContext>
);
};
24 changes: 21 additions & 3 deletions packages/ra-core/src/form/ValidationError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,33 @@ export interface ValidationErrorProps {
error: ValidationErrorMessage;
}

const ValidationErrorSpecialFormatPrefix = '@@react-admin@@';
const ValidationError = (props: ValidationErrorProps) => {
const { error } = props;
let errorMessage = error;
const translate = useTranslate();
if ((error as ValidationErrorMessageWithArgs).message) {
const { message, args } = error as ValidationErrorMessageWithArgs;
// react-hook-form expects errors to be plain strings but our validators can return objects
// that have message and args.
// To avoid double translation for users that validate with a schema instead of our validators
// we use a special format for our validators errors.
// The useInput hook handle the special formatting
if (
typeof error === 'string' &&
error.startsWith(ValidationErrorSpecialFormatPrefix)
) {
errorMessage = JSON.parse(
error.substring(ValidationErrorSpecialFormatPrefix.length)
);
}
if ((errorMessage as ValidationErrorMessageWithArgs).message) {
const {
message,
args,
} = errorMessage as ValidationErrorMessageWithArgs;
return <>{translate(message, { _: message, ...args })}</>;
}

return <>{translate(error as string, { _: error })}</>;
return <>{translate(errorMessage as string, { _: errorMessage })}</>;
};

export default ValidationError;
1 change: 1 addition & 0 deletions packages/ra-core/src/form/useGetValidationErrorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { useTranslate } from '../i18n';

/**
* @deprecated
* This internal hook returns a function that can translate an error message.
* It handles simple string errors and those which have a message and args.
* Only useful if you are implementing custom inputs without leveraging our useInput hook.
Expand Down
10 changes: 7 additions & 3 deletions packages/ra-core/src/form/useInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { useRecordContext } from '../controller';
import { composeValidators, Validator } from './validate';
import isRequired from './isRequired';
import { useFormGroupContext } from './useFormGroupContext';
import { useGetValidationErrorMessage } from './useGetValidationErrorMessage';
import { useFormGroups } from './useFormGroups';
import { useApplyInputDefaultValues } from './useApplyInputDefaultValues';
import { useEvent } from '../util';
Expand Down Expand Up @@ -43,7 +42,6 @@ export const useInput = <ValueType = any>(
const formGroupName = useFormGroupContext();
const formGroups = useFormGroups();
const record = useRecordContext();
const getValidationErrorMessage = useGetValidationErrorMessage();

useEffect(() => {
if (!formGroups || formGroupName == null) {
Expand Down Expand Up @@ -74,7 +72,13 @@ export const useInput = <ValueType = any>(
const error = await sanitizedValidate(value, values, props);

if (!error) return true;
return getValidationErrorMessage(error);
// react-hook-form expects errors to be plain strings but our validators can return objects
// that have message and args.
// To avoid double translation for users that validate with a schema instead of our validators
// we use a special format for our validators errors.
// The ValidationError component will check for this format and extract the message and args
// to translate.
return `@@react-admin@@${JSON.stringify(error)}`;
},
},
...options,
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-core/src/form/useUnique.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DataProvider,
EditBase,
FormDataConsumer,
ValidationError,
mergeTranslations,
useUnique,
} from '..';
Expand All @@ -30,7 +31,9 @@ const Input = props => {
aria-invalid={fieldState.invalid}
{...field}
/>
<p>{fieldState.error?.message}</p>
{fieldState.error && fieldState.error?.message ? (
<ValidationError error={fieldState.error?.message} />
) : null}
</>
);
};
Expand Down
20 changes: 11 additions & 9 deletions packages/ra-core/src/form/useUnique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,18 @@ export const useUnique = (options?: UseUniqueOptions) => {
);

if (total > 0 && !data.some(r => r.id === record?.id)) {
return translate(message, {
_: message,
source: props.source,
value,
field: translateLabel({
label: props.label,
return {
message,
args: {
source: props.source,
resource,
}),
});
value,
field: translateLabel({
label: props.label,
source: props.source,
resource,
}),
},
};
}
} catch (error) {
return translate('ra.notification.http_error');
Expand Down
12 changes: 2 additions & 10 deletions packages/ra-ui-materialui/src/input/InputHelperText.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import * as React from 'react';
import { isValidElement, ReactElement } from 'react';
import {
useTranslate,
ValidationError,
ValidationErrorMessage,
ValidationErrorMessageWithArgs,
} from 'ra-core';
import { useTranslate, ValidationError, ValidationErrorMessage } from 'ra-core';

export const InputHelperText = (props: InputHelperTextProps) => {
const { helperText, touched, error } = props;
const translate = useTranslate();

if (touched && error) {
if ((error as ValidationErrorMessageWithArgs).message) {
return <ValidationError error={error} />;
}
return <>{error}</>;
return <ValidationError error={error} />;
}

if (helperText === false) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-ui-materialui/src/input/NumberInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ describe('<NumberInput />', () => {
fireEvent.blur(input);
fireEvent.click(screen.getByText('ra.action.save'));
await screen.findByText(
'views:{"invalid":true,"isDirty":true,"isTouched":true,"error":{"type":"validate","message":"error","ref":{}}}'
'views:{"invalid":true,"isDirty":true,"isTouched":true,"error":{"type":"validate","message":"@@react-admin@@\\"error\\"","ref":{}}}'
);
});
});
Expand Down
18 changes: 13 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2433,12 +2433,12 @@ __metadata:
languageName: node
linkType: hard

"@hookform/resolvers@npm:^2.8.8":
version: 2.8.8
resolution: "@hookform/resolvers@npm:2.8.8"
"@hookform/resolvers@npm:^3.2.0":
version: 3.2.0
resolution: "@hookform/resolvers@npm:3.2.0"
peerDependencies:
react-hook-form: ^7.0.0
checksum: 0c8814f1116a145c433300f079c3289d5ece71f36c6b782fb0f5b2388c766140cadd72bd2f7b77bc570988359d686e0c88ba513ab0ed79e70073747b7a19e8f8
checksum: 7eb79c480e006f08fcfe803e70b7b67eda03cc5c5bb8ce68a5399a0c6fdc34ee0fcc677fed9bea4a0baaa455ba39b15f86c8d2e3a702acdf762d6667988085b6
languageName: node
linkType: hard

Expand Down Expand Up @@ -18212,7 +18212,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "ra-core@workspace:packages/ra-core"
dependencies:
"@hookform/resolvers": ^2.8.8
"@hookform/resolvers": ^3.2.0
"@testing-library/react": ^11.2.3
"@testing-library/react-hooks": ^7.0.2
"@types/jest": ^29.5.2
Expand Down Expand Up @@ -18243,6 +18243,7 @@ __metadata:
rimraf: ^3.0.2
typescript: ^5.1.3
yup: ^0.32.11
zod: ^3.22.1
peerDependencies:
history: ^5.1.0
react: ^16.9.0 || ^17.0.0 || ^18.0.0
Expand Down Expand Up @@ -22827,3 +22828,10 @@ __metadata:
checksum: 71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8
languageName: node
linkType: hard

"zod@npm:^3.22.1":
version: 3.22.1
resolution: "zod@npm:3.22.1"
checksum: fe7112dd8080136652f0be10670a2a44868b097198f3be6264294a62d6c6b280099db5e1bc4a327ec4f738f58bc600445d373ecadf5d51fb5585fa0ab76ee67a
languageName: node
linkType: hard