[bug] API form fixes (#13196)

* Improve API error feedback

* Add 'message' field opt

* Update error handling logic
This commit is contained in:
Oliver authored and GitHub committed 2026-11-05 18:41:11 +11:00
1 parent 5fe105b948
commit c5c878d6af
1 file changed
+110 -23
+110 -23
View File
@@ -40,6 +40,7 @@ import type {
ApiFormFieldType,
ApiFormProps
} from '@lib/types/Forms';
import { IconExclamationCircle } from '@tabler/icons-react';
import { useApi } from '../../contexts/ApiContext';
import { isEquivalent } from '../../functions/comparison';
import { constructField, extractAvailableFields } from '../../functions/forms';
@@ -569,12 +570,35 @@ export function ApiForm({
case 400:
// Data validation errors
const _nonFieldErrors: string[] = [];
let _fieldErrorCount = 0;
const setFieldError = (path: string, message: string) => {
_fieldErrorCount += 1;
form.setError(path, { message: message });
};
// Response keys which carry a general (non-field) error message
const NON_FIELD_ERROR_KEYS: string[] = [
'non_field_errors',
'__all__',
'detail',
'error',
'message',
'reason'
];
const processErrors = (errors: any, _path?: string) => {
// Ignore empty values
if (errors === null || errors === undefined) {
return;
}
// Handle an array of errors
if (Array.isArray(errors)) {
errors.forEach((error: any) => {
_nonFieldErrors.push(error.toString());
if (typeof error === 'string') {
_nonFieldErrors.push(error);
}
});
return;
}
@@ -585,42 +609,100 @@ export function ApiForm({
return;
}
// Ignore any other non-object values (numbers, booleans)
if (typeof errors !== 'object') {
return;
}
for (const [k, v] of Object.entries(errors)) {
const path = _path ? `${_path}.${k}` : k;
// Determine if field "k" is valid (exists and is visible)
const field = fields[k];
const valid = field && !field.hidden;
if (!valid || k == 'non_field_errors' || k == '__all__') {
// Known error keys are displayed as non-field errors
if (NON_FIELD_ERROR_KEYS.includes(k)) {
processErrors(v);
continue;
}
if (typeof v === 'object' && Array.isArray(v)) {
if (field?.field_type == 'table') {
// Special handling for "table" fields - they have nested errors
v.forEach((item: any, idx: number) => {
for (const [key, value] of Object.entries(item)) {
const path: string = `${k}.${idx}.${key}`;
if (Array.isArray(value)) {
form.setError(path, { message: value.join(', ') });
}
// Nested errors (below the top level) cannot be mapped to a form field,
// so they are displayed as non-field errors rather than being dropped
if (_path) {
processErrors(v);
continue;
}
const field = fields[k];
// Ignore top-level keys which do not correspond to a form field
// (e.g. response metadata such as "transition" or "state")
if (!field) {
continue;
}
// Errors against hidden fields are displayed as non-field errors
if (field.hidden) {
processErrors(v);
continue;
}
if (
field.field_type == 'table' &&
typeof v === 'object' &&
v !== null
) {
// Special handling for "table" fields - they have nested (per-row) errors.
// DRF may return these as an array (one entry per row),
// or as an object keyed by row index (only rows with errors)
const rows: [string, any][] = Array.isArray(v)
? v.map((item: any, idx: number) => [`${idx}`, item])
: Object.entries(v);
rows.forEach(([idx, item]) => {
// Row-level error (not associated with a particular column)
if (
typeof item !== 'object' ||
item === null ||
Array.isArray(item)
) {
processErrors(item);
return;
}
for (const [key, value] of Object.entries(item)) {
const rowPath: string = `${k}.${idx}.${key}`;
if (Array.isArray(value)) {
setFieldError(rowPath, value.join(', '));
} else if (typeof value === 'string') {
setFieldError(rowPath, value);
} else {
processErrors(value);
}
});
} else {
// Standard error handling for other fields
form.setError(path, { message: v.join(', ') });
}
}
});
} else if (Array.isArray(v)) {
// Standard error handling for other fields
setFieldError(path, v.join(', '));
} else if (typeof v === 'string') {
form.setError(path, { message: v });
setFieldError(path, v);
} else {
processErrors(v, path);
}
}
};
processErrors(error.response.data);
try {
processErrors(error.response.data);
} catch (err) {
// Malformed error response - fall through to the generic message below
console.error('Failed to process form errors:', err);
}
// No errors could be extracted from the response
if (_nonFieldErrors.length == 0 && _fieldErrorCount == 0) {
_nonFieldErrors.push(
t`An unknown error occurred. Refer to the browser console for details.`
);
}
setNonFieldErrors(_nonFieldErrors);
props.onFormError?.(error, form);
@@ -710,7 +792,12 @@ export function ApiForm({
{/* Form Fields */}
<Stack gap='sm'>
{(!isValid || nonFieldErrors.length > 0) && (
<Alert radius='sm' color='red' title={t`Form Error`}>
<Alert
icon={<IconExclamationCircle />}
radius='sm'
color='red'
title={t`Form Error`}
>
{nonFieldErrors.length > 0 ? (
<Stack gap='xs'>
{nonFieldErrors