-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Unable to Show Server Side Errors in Form #7581
Comments
Hi, If I'm missing your point, feel free to correct me and/or complete my sandbox to reproduce your actual issue. |
Hello, So you saying that 'Create' component can save the form, but can't handle server side errors itself? 'Create' uses 'useCreateController', that uses 'useCreate'. Why do I need to use second 'useCreate' but can't work directly with 'Create ' ?
|
Since there is no standard format defining how your backend will answer with the validation errors, you need to write that logic manually at some point. |
My current workaround for this issue (based on useCreateController.ts and useEditController.ts) until #7938 is merged: IExceptionWrapper.ts (see ValidationProblemDetails class which is an extension of rfc7807) export interface IExceptionWrapper {
title: string;
status: number;
errors: Record<string, string[]>;
} useCreateSaveWithServerValidation.ts import {
RedirectionSideEffect,
TransformData,
useCreate,
useMutationMiddlewares,
useNotify,
useRedirect,
useResourceContext,
useResourceDefinition,
} from 'react-admin';
import { useCallback } from 'react';
import axios, { AxiosError } from 'axios';
import { IExceptionWrapper } from '../../common/interfaces/IExceptionWrapper';
import { serverSideErrTransform } from '../errorUtils';
/*
* see
* useCreateController.ts (react-admin source code)
* https://marmelab.com/react-admin/Validation.html#server-side-validation
* https://github.com/marmelab/react-admin/issues/7581#issuecomment-1109543482
* https://github.com/marmelab/react-admin/issues/8278
* https://github.com/marmelab/react-admin/issues/7608#issuecomment-1143529740
* https://github.com/marmelab/react-admin/issues/5992#issuecomment-872981065
* */
const getDefaultRedirectRoute = (hasShow: boolean | undefined, hasEdit: boolean | undefined) => {
if (hasEdit) {
return 'edit';
}
if (hasShow) {
return 'show';
}
return 'list';
};
export interface UseCreateSaveWithServerValidationProps {
resourceName?: string;
redirectTo?: RedirectionSideEffect;
transformFunc?: TransformData;
}
export const useCreateSaveWithServerValidation = ({ resourceName, redirectTo, transformFunc }: UseCreateSaveWithServerValidationProps = {}) => {
const [create] = useCreate();
const { getMutateWithMiddlewares } = useMutationMiddlewares();
const notify = useNotify();
const redirect = useRedirect();
const resourceContext = useResourceContext();
const resource = resourceName ?? resourceContext;
const { hasEdit, hasShow } = useResourceDefinition({ resource });
const finalRedirectTo = redirectTo ?? getDefaultRedirectRoute(hasShow, hasEdit);
return useCallback(
async (formValues: object) => {
try {
const transformedData = await Promise.resolve(transformFunc ? transformFunc(formValues) : formValues);
const mutate = getMutateWithMiddlewares(create);
await mutate(
resource,
{ data: transformedData }, // custom meta omitted
{
returnPromise: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unused-vars
onSuccess: async (data: any, variables: any, context: unknown) => {
// custom onSuccess omitted
notify('ra.notification.created', {
type: 'info',
messageArgs: { smart_count: 1 },
});
redirect(finalRedirectTo, resource, data.id, data);
return undefined;
},
// custom onError omitted
onError: ((error: Error | string) => {
notify(
typeof error === 'string'
? error
: error.message
|| 'ra.notification.http_error',
{
type: 'warning',
messageArgs: {
_:
typeof error === 'string'
? error
: error && error.message
? error.message
: undefined,
},
},
);
}),
},
);
return undefined;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
const responseData = axiosError.response?.data;
if (!!responseData
&& typeof responseData === 'object'
&& Object.hasOwn(responseData, 'errors')) {
const responseDataTyped = responseData as IExceptionWrapper;
return serverSideErrTransform(responseDataTyped.errors);
} else {
throw error;
}
} else {
throw error;
}
}
},
[create, finalRedirectTo, getMutateWithMiddlewares, notify, redirect, resource, transformFunc],
);
}; useEditSaveWithServerValidation.ts import {
MutationMode,
RedirectionSideEffect,
TransformData,
useMutationMiddlewares,
useNotify,
useRedirect,
useResourceContext,
useUpdate,
} from 'react-admin';
import { useCallback } from 'react';
import axios, { AxiosError } from 'axios';
import { IExceptionWrapper } from '../../common/interfaces/IExceptionWrapper';
import { serverSideErrTransform } from '../errorUtils';
import { useParams } from 'react-router-dom';
/*
* see
* useEditController.ts (react-admin source code)
* https://marmelab.com/react-admin/Validation.html#server-side-validation
* https://github.com/marmelab/react-admin/issues/7581#issuecomment-1109543482
* https://github.com/marmelab/react-admin/issues/8278
* https://github.com/marmelab/react-admin/issues/7608#issuecomment-1143529740
* https://github.com/marmelab/react-admin/issues/5992#issuecomment-872981065
* */
export interface UseEditSaveWithServerValidationProps {
resourceName?: string;
recordId?: number;
mutationMode?: MutationMode;
redirectTo?: RedirectionSideEffect;
transformFunc?: TransformData;
}
function exists(value: number | undefined) {
return !(value === null || value === undefined);
}
const DefaultRedirect = 'list';
export const useEditSaveWithServerValidation = ({ resourceName, recordId, mutationMode, redirectTo, transformFunc }: UseEditSaveWithServerValidationProps = {}) => {
const [update] = useUpdate(); // recordCached, otherMutationOptions, mutationMode omitted
const { getMutateWithMiddlewares } = useMutationMiddlewares();
const notify = useNotify();
const redirect = useRedirect();
const { id: routeId } = useParams<'id'>();
const id = exists(recordId) ? recordId : decodeURIComponent(routeId ?? '');
const resourceContext = useResourceContext();
const resource = resourceName ?? resourceContext;
const finalRedirectTo = redirectTo ?? DefaultRedirect;
return useCallback(
async (formValues: object) => {
try {
const transformedData = await Promise.resolve(transformFunc ? transformFunc(formValues) : formValues);
const mutate = getMutateWithMiddlewares(update);
await mutate(
resource,
{ id, data: transformedData }, // meta omitted
{
returnPromise: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unused-vars
onSuccess: async (data: any, variables: any, context: unknown) => {
// custom onSuccess omitted
notify('ra.notification.updated', {
type: 'info',
messageArgs: { smart_count: 1 },
undoable: mutationMode === 'undoable',
});
redirect(finalRedirectTo, resource, data?.id, data);
return undefined;
},
// custom onError omitted
onError: ((error: Error | string) => {
notify(
typeof error === 'string'
? error
: error.message
|| 'ra.notification.http_error',
{
type: 'warning',
messageArgs: {
_:
typeof error === 'string'
? error
: error && error.message
? error.message
: undefined,
},
},
);
}),
},
);
return undefined;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
const responseData = axiosError.response?.data;
if (!!responseData
&& typeof responseData === 'object'
&& Object.hasOwn(responseData, 'errors')) {
const responseDataTyped = responseData as IExceptionWrapper;
return serverSideErrTransform(responseDataTyped.errors);
} else {
throw error;
}
} else {
throw error;
}
}
},
[finalRedirectTo, getMutateWithMiddlewares, id, mutationMode, notify, redirect, resource, transformFunc, update],
);
}; I use these hooks like this const save = useEditSaveWithServerValidation({ transformFunc: transformUserData, redirectTo: false });
// ...
<SimpleForm onSubmit={save} toolbar={<SaveDeleteToolbar />} warnWhenUnsavedChanges>
<UserEditInputs />
</SimpleForm> |
What you were expecting:
To display input errors when server sends errors
What happened instead:
No errors are displayed
Related code:
useCreateController returns function 'save' that calls 'create' from 'useCreate'. The 'create' function itself does not return anything. It has a prop to return a promise 'returnPromise', but that prop is not passed to 'create' in 'save'. Third argument in useCreateController that is passed to 'create' has only 'onSuccess' | 'onError' and so it does no return anything without 'returnPromise' prop.
useAugmentedForm expects saveContext.save to return the errors, if there are any.
saveContext.save comes from useCreateController. And saveContext.save(values) does not return anything, because the prop 'returnPromise' is not passed.
Same in useEditController
Environment
The text was updated successfully, but these errors were encountered: