diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 9714f0700b2..e6e69920989 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -38,7 +38,7 @@ "build": "zshy --silent" }, "devDependencies": { - "@hookform/resolvers": "^3.2.0", + "@hookform/resolvers": "^5.2.2", "@tanstack/react-query": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2", "@testing-library/react": "^15.0.7", @@ -58,7 +58,7 @@ "react-router-dom": "^6.28.1", "typescript": "^5.1.3", "yup": "^0.32.11", - "zod": "^3.22.1", + "zod": "^4.3.6", "zshy": "^0.5.0" }, "peerDependencies": { diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index f815a4ca61d..325c09df18e 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -871,7 +871,9 @@ describe('Form', () => { const translate = jest.spyOn(i18nProvider, 'translate'); render(); fireEvent.click(screen.getByText('Submit')); - await screen.findByText('Required'); + await screen.findByText( + 'Invalid input: expected string, received undefined' + ); await screen.findByText('This field is required'); await screen.findByText('This field must be provided'); await screen.findByText('app.validation.missing'); diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index a08bda197b6..46e79f87c6c 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -6,6 +6,8 @@ import { } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; import { @@ -273,14 +275,10 @@ export const InputLevelValidation = ({ const zodSchema = z.object({ defaultMessage: z.string(), //.min(1), - customMessage: z.string({ - required_error: 'This field is required', - }), - customMessageTranslationKey: z.string({ - required_error: 'app.validation.required', - }), + customMessage: z.string({ error: 'This field is required' }), + customMessageTranslationKey: z.string({ error: 'app.validation.required' }), missingCustomMessageTranslationKey: z.string({ - required_error: 'app.validation.missing', + error: 'app.validation.missing', }), }); @@ -308,6 +306,67 @@ export const ZodResolver = ({ ); }; +type Schema = z.infer; + +export const ZodResolverWithSchema = ({ + i18nProvider = defaultI18nProvider, +}: { + i18nProvider?: I18nProvider; +}) => { + const [result, setResult] = React.useState(); + return ( + + + record={{}} + onSubmit={data => setResult(data)} + resolver={zodResolver(zodSchema)} + > + + + + + + +
{JSON.stringify(result, null, 2)}
+
+ ); +}; + +const yupSchema = yup.object({ + defaultMessage: yup.string().required(), + customMessage: yup.string().required('This field is required'), + customMessageTranslationKey: yup + .string() + .required('app.validation.required'), + missingCustomMessageTranslationKey: yup + .string() + .required('app.validation.missing'), +}); + +export const YupResolver = ({ + i18nProvider = defaultI18nProvider, +}: { + i18nProvider?: I18nProvider; +}) => { + const [result, setResult] = React.useState(); + return ( + +
setResult(data)} + resolver={yupResolver(yupSchema)} + > + + + + + +
+
{JSON.stringify(result, null, 2)}
+
+ ); +}; + const FormUnderTest = () => { const navigate = useNavigate(); return ( diff --git a/packages/ra-core/src/form/Form.tsx b/packages/ra-core/src/form/Form.tsx index cf487140b46..0d5a4cc2ab3 100644 --- a/packages/ra-core/src/form/Form.tsx +++ b/packages/ra-core/src/form/Form.tsx @@ -63,7 +63,7 @@ export function Form(props: FormProps) { } = props; const record = useRecordContext(props); const resource = useResourceContext(props); - const { form, formHandleSubmit } = useAugmentedForm(props); + const { form, formHandleSubmit } = useAugmentedForm(props); const sourceContext = React.useMemo( () => ({ getSource: (source: string) => source, @@ -113,7 +113,10 @@ export function Form(props: FormProps) { } export type FormProps = FormOwnProps & - Omit & { + Omit< + UseFormProps, + 'onSubmit' + > & { validate?: ValidateForm; noValidate?: boolean; WarnWhenUnsavedChangesComponent?: React.ComponentType<{ diff --git a/packages/ra-core/src/form/useAugmentedForm.ts b/packages/ra-core/src/form/useAugmentedForm.ts index 8f84c2929fe..ec20a2ebea2 100644 --- a/packages/ra-core/src/form/useAugmentedForm.ts +++ b/packages/ra-core/src/form/useAugmentedForm.ts @@ -155,7 +155,12 @@ export const useAugmentedForm = ( export type UseAugmentedFormProps = UseFormOwnProps & - Omit & { + Omit< + UseFormProps< + RecordType extends FieldValues ? RecordType : FieldValues + >, + 'onSubmit' + > & { validate?: ValidateForm; }; diff --git a/packages/ra-core/src/form/validation/getSimpleValidationResolver.ts b/packages/ra-core/src/form/validation/getSimpleValidationResolver.ts index eadc5fc4a7c..a6b593c7bfa 100644 --- a/packages/ra-core/src/form/validation/getSimpleValidationResolver.ts +++ b/packages/ra-core/src/form/validation/getSimpleValidationResolver.ts @@ -1,4 +1,4 @@ -import { FieldValues } from 'react-hook-form'; +import { FieldValues, Resolver } from 'react-hook-form'; /** * Convert a simple validation function that returns an object matching the form shape with errors @@ -25,7 +25,7 @@ import { FieldValues } from 'react-hook-form'; export const getSimpleValidationResolver = ( validate: ValidateForm - ) => + ): Resolver => async (data: TFieldValues) => { const errors = await validate(data); diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.tsx b/packages/ra-ui-materialui/src/form/SimpleForm.tsx index 1da1d10c29b..1cb3270ec36 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleForm.tsx @@ -76,8 +76,8 @@ export const SimpleForm = (inProps: SimpleFormProps) => { ); }; -export interface SimpleFormProps - extends Omit, +export interface SimpleFormProps + extends Omit, 'render'>, Omit { children: ReactNode; className?: string; diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.tsx index c2e939addf5..547b185e36e 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.tsx @@ -110,8 +110,8 @@ const sanitizeRestProps = ({ ...rest }: TabbedFormProps) => rest; -export interface TabbedFormProps - extends Omit, +export interface TabbedFormProps + extends Omit, 'render'>, Omit< HtmlHTMLAttributes, 'defaultValue' | 'onSubmit' | 'children' diff --git a/yarn.lock b/yarn.lock index a15ce2bf2fc..53cc2ba875d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2627,12 +2627,14 @@ __metadata: languageName: node linkType: hard -"@hookform/resolvers@npm:^3.2.0": - version: 3.2.0 - resolution: "@hookform/resolvers@npm:3.2.0" +"@hookform/resolvers@npm:^5.2.2": + version: 5.2.2 + resolution: "@hookform/resolvers@npm:5.2.2" + dependencies: + "@standard-schema/utils": "npm:^0.3.0" peerDependencies: - react-hook-form: ^7.0.0 - checksum: 7eb79c480e006f08fcfe803e70b7b67eda03cc5c5bb8ce68a5399a0c6fdc34ee0fcc677fed9bea4a0baaa455ba39b15f86c8d2e3a702acdf762d6667988085b6 + react-hook-form: ^7.55.0 + checksum: 0692cd61dcc2a70cbb27b88a37f733c39e97f555c036ba04a81bd42b0467461cfb6bafacb46c16f173672f9c8a216bd7928a2330d4e49c700d130622bf1defaf languageName: node linkType: hard @@ -5655,6 +5657,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/utils@npm:^0.3.0": + version: 0.3.0 + resolution: "@standard-schema/utils@npm:0.3.0" + checksum: 6eb74cd13e52d5fc74054df51e37d947ef53f3ab9e02c085665dcca3c38c60ece8d735cebbdf18fbb13c775fbcb9becb3f53109b0e092a63f0f7389ce0993fd0 + languageName: node + linkType: hard + "@storybook/addon-actions@npm:^8.6.11": version: 8.6.11 resolution: "@storybook/addon-actions@npm:8.6.11" @@ -20735,7 +20744,7 @@ __metadata: version: 0.0.0-use.local resolution: "ra-core@workspace:packages/ra-core" dependencies: - "@hookform/resolvers": "npm:^3.2.0" + "@hookform/resolvers": "npm:^5.2.2" "@tanstack/react-query": "npm:^5.90.2" "@tanstack/react-query-devtools": "npm:^5.90.2" "@testing-library/react": "npm:^15.0.7" @@ -20763,7 +20772,7 @@ __metadata: react-router-dom: "npm:^6.28.1" typescript: "npm:^5.1.3" yup: "npm:^0.32.11" - zod: "npm:^3.22.1" + zod: "npm:^4.3.6" zshy: "npm:^0.5.0" peerDependencies: "@tanstack/react-query": ^5.83.0 @@ -26394,13 +26403,20 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.1, zod@npm:^3.23.8, zod@npm:^3.24.2, zod@npm:^3.25.76": +"zod@npm:^3.23.8, zod@npm:^3.24.2, zod@npm:^3.25.76": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: 5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c languageName: node linkType: hard +"zod@npm:^4.3.6": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307 + languageName: node + linkType: hard + "zrender@npm:5.6.1": version: 5.6.1 resolution: "zrender@npm:5.6.1"