Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 1x 4x 1x 1x 3x 4x 3x 3x 4x 3x 1x 3x 1x | import type { FieldValues, Path, UseFormSetError } from 'react-hook-form';
import { type ApiError, toUserMessage, ValidationError } from '@/lib/api/errors';
/**
* Shows an API failure on a React Hook Form form.
*
* A `ValidationError` with field messages puts each message under its input. Pass the form's
* values (`form.getValues()`) so a message for a name the form has no input for (`account`, a
* server-only rule) goes to `errors.root.server` instead of nowhere: React Hook Form keeps an error
* under any name, but no input shows it. Anything else (or a validation error without field
* details) goes to `errors.root.server`, which the form renders above the submit button.
*/
export const setFormErrors = <T extends FieldValues>(
setError: UseFormSetError<T>,
error: ApiError,
values?: T,
) => {
if (!(error instanceof ValidationError) || Object.keys(error.fields).length === 0) {
setError('root.server', { type: 'server', message: toUserMessage(error) });
return;
}
// A nested name (`address.city`) belongs to the form when its first segment does.
const hasInput = (field: string) =>
values === undefined || Object.hasOwn(values, field.split('.')[0] ?? field);
const unshown: string[] = [];
for (const [field, message] of Object.entries(error.fields)) {
if (hasInput(field)) {
// Field names come from the server; they match the form when the API mirrors its schema.
// SAFETY: `field` is one of the form's own names (hasInput checked it against the values),
// or the caller passed no values and asks for every server name to be set as is.
setError(field as Path<T>, { type: 'server', message });
} else {
unshown.push(message);
}
}
if (unshown.length > 0) {
setError('root.server', { type: 'server', message: unshown.join(' ') });
}
};
|