Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
4 changes: 3 additions & 1 deletion packages/ra-core/src/form/Form.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,9 @@ describe('Form', () => {
const translate = jest.spyOn(i18nProvider, 'translate');
render(<ZodResolver i18nProvider={i18nProvider} />);
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');
Expand Down
73 changes: 66 additions & 7 deletions packages/ra-core/src/form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
}),
});

Expand Down Expand Up @@ -308,6 +306,67 @@ export const ZodResolver = ({
);
};

type Schema = z.infer<typeof zodSchema>;

export const ZodResolverWithSchema = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [result, setResult] = React.useState<any>();
return (
<CoreAdminContext i18nProvider={i18nProvider}>
<Form<Schema>
record={{}}
onSubmit={data => setResult(data)}
resolver={zodResolver(zodSchema)}
>
<Input source="defaultMessage" />
<Input source="customMessage" />
<Input source="customMessageTranslationKey" />
<Input source="missingCustomMessageTranslationKey" />
<button type="submit">Submit</button>
</Form>
<pre>{JSON.stringify(result, null, 2)}</pre>
</CoreAdminContext>
);
};

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<any>();
return (
<CoreAdminContext i18nProvider={i18nProvider}>
<Form
record={{}}
onSubmit={data => setResult(data)}
resolver={yupResolver(yupSchema)}
>
<Input source="defaultMessage" />
<Input source="customMessage" />
<Input source="customMessageTranslationKey" />
<Input source="missingCustomMessageTranslationKey" />
<button type="submit">Submit</button>
</Form>
<pre>{JSON.stringify(result, null, 2)}</pre>
</CoreAdminContext>
);
};

const FormUnderTest = () => {
const navigate = useNavigate();
return (
Expand Down
7 changes: 5 additions & 2 deletions packages/ra-core/src/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function Form<RecordType = any>(props: FormProps<RecordType>) {
} = props;
const record = useRecordContext(props);
const resource = useResourceContext(props);
const { form, formHandleSubmit } = useAugmentedForm(props);
const { form, formHandleSubmit } = useAugmentedForm<RecordType>(props);
const sourceContext = React.useMemo<SourceContextValue>(
() => ({
getSource: (source: string) => source,
Expand Down Expand Up @@ -113,7 +113,10 @@ export function Form<RecordType = any>(props: FormProps<RecordType>) {
}

export type FormProps<RecordType = any> = FormOwnProps<RecordType> &
Omit<UseFormProps, 'onSubmit'> & {
Omit<
UseFormProps<RecordType extends FieldValues ? RecordType : FieldValues>,
'onSubmit'
> & {
validate?: ValidateForm;
noValidate?: boolean;
WarnWhenUnsavedChangesComponent?: React.ComponentType<{
Expand Down
7 changes: 6 additions & 1 deletion packages/ra-core/src/form/useAugmentedForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,12 @@ export const useAugmentedForm = <RecordType = any>(

export type UseAugmentedFormProps<RecordType = any> =
UseFormOwnProps<RecordType> &
Omit<UseFormProps, 'onSubmit'> & {
Omit<
UseFormProps<
RecordType extends FieldValues ? RecordType : FieldValues
>,
'onSubmit'
> & {
validate?: ValidateForm;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,7 +25,7 @@ import { FieldValues } from 'react-hook-form';
export const getSimpleValidationResolver =
<TFieldValues extends FieldValues = FieldValues>(
validate: ValidateForm<TFieldValues>
) =>
): Resolver<TFieldValues> =>
async (data: TFieldValues) => {
const errors = await validate(data);

Expand Down
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/form/SimpleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export const SimpleForm = (inProps: SimpleFormProps) => {
);
};

export interface SimpleFormProps
extends Omit<FormProps, 'render'>,
export interface SimpleFormProps<RecordType = any>
extends Omit<FormProps<RecordType>, 'render'>,
Omit<StackProps, 'onSubmit'> {
children: ReactNode;
className?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/form/TabbedForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ const sanitizeRestProps = ({
...rest
}: TabbedFormProps) => rest;

export interface TabbedFormProps
extends Omit<FormProps, 'render'>,
export interface TabbedFormProps<RecordType = any>
extends Omit<FormProps<RecordType>, 'render'>,
Omit<
HtmlHTMLAttributes<HTMLFormElement>,
'defaultValue' | 'onSubmit' | 'children'
Expand Down
32 changes: 24 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading