Display validation errors returned by the server

This commit is contained in:
Maurus Decimus
2026-04-30 18:12:30 +02:00
parent 612fd796f3
commit ba6b4472d2
5 changed files with 29 additions and 31 deletions
+1
View File
@@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. This projec
### Changed ### Changed
### Fixed ### Fixed
- Display validation errors returned by the server.
## [1.0.1] - 2026-04-25 ## [1.0.1] - 2026-04-25
+2 -14
View File
@@ -19,7 +19,7 @@ import { toast } from '@/hooks/use-toast';
import { resolveObject, resolveSchema, resolveForm, buildCreateDefaults, deepMerge } from '@/lib/schemaResolver'; import { resolveObject, resolveSchema, resolveForm, buildCreateDefaults, deepMerge } from '@/lib/schemaResolver';
import { calculateJmapPatch } from '@/lib/jmapPatch'; import { calculateJmapPatch } from '@/lib/jmapPatch';
import { jmapGet, jmapSet, getAccountId } from '@/services/jmap/client'; import { jmapGet, jmapSet, getAccountId } from '@/services/jmap/client';
import { friendlySetError } from '@/lib/jmapErrors'; import { friendlySetError, validationErrorMessage } from '@/lib/jmapErrors';
import type { Field, Fields, Form, FormField } from '@/types/schema'; import type { Field, Fields, Form, FormField } from '@/types/schema';
import type { JmapSetError, JmapSetResponse } from '@/types/jmap'; import type { JmapSetError, JmapSetResponse } from '@/types/jmap';
@@ -181,19 +181,7 @@ export function BootstrapWizard() {
for (const ve of error.validationErrors) { for (const ve of error.validationErrors) {
const top = ve.property?.split('/')[0] ?? ''; const top = ve.property?.split('/')[0] ?? '';
if (!top) continue; if (!top) continue;
const msg = record(top, validationErrorMessage(ve));
ve.type === 'Required'
? t('form.required', 'This field is required.')
: ve.type === 'MaxLength'
? t('form.maxLengthIs', 'Maximum length is {{max}}.', { max: ve.required })
: ve.type === 'MinLength'
? t('form.minLengthIs', 'Minimum length is {{min}}.', { min: ve.required })
: ve.type === 'MaxValue'
? t('form.maxValueIs', 'Maximum value is {{max}}.', { max: ve.required })
: ve.type === 'MinValue'
? t('form.minValueIs', 'Minimum value is {{min}}.', { min: ve.required })
: t('form.invalidValue', 'Invalid value.');
record(top, msg);
} }
} }
+6 -15
View File
@@ -46,7 +46,7 @@ import {
} from '@/lib/schemaResolver'; } from '@/lib/schemaResolver';
import { jmapGet, jmapSet, jmapRequest, getAccountId } from '@/services/jmap/client'; import { jmapGet, jmapSet, jmapRequest, getAccountId } from '@/services/jmap/client';
import { calculateJmapPatch } from '@/lib/jmapPatch'; import { calculateJmapPatch } from '@/lib/jmapPatch';
import { friendlySetError } from '@/lib/jmapErrors'; import { friendlySetError, validationErrorMessage } from '@/lib/jmapErrors';
import { coerceLabel } from '@/lib/objectOptions'; import { coerceLabel } from '@/lib/objectOptions';
import { SECRET_MASK } from '@/lib/jmapUtils'; import { SECRET_MASK } from '@/lib/jmapUtils';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
@@ -347,21 +347,12 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
if (error.validationErrors && error.validationErrors.length > 0) { if (error.validationErrors && error.validationErrors.length > 0) {
for (const ve of error.validationErrors) { for (const ve of error.validationErrors) {
if (ve.property && currentFields?.properties[ve.property]) { if (ve.property && currentFields?.properties[ve.property]) {
const msg = newFieldErrors[ve.property] = validationErrorMessage(ve);
ve.type === 'Required'
? t('form.required', 'This field is required.')
: ve.type === 'MaxLength'
? t('form.maxLengthIs', 'Maximum length is {{max}}.', { max: ve.required })
: ve.type === 'MinLength'
? t('form.minLengthIs', 'Minimum length is {{min}}.', { min: ve.required })
: ve.type === 'MaxValue'
? t('form.maxValueIs', 'Maximum value is {{max}}.', { max: ve.required })
: ve.type === 'MinValue'
? t('form.minValueIs', 'Minimum value is {{min}}.', { min: ve.required })
: t('form.invalidValue', 'Invalid value.');
newFieldErrors[ve.property] = msg;
} else if (ve.property) { } else if (ve.property) {
setGeneralError((prev) => (prev ? `${prev}\n${ve.property}: ${ve.type}` : `${ve.property}: ${ve.type}`)); const detail = ve.value && ve.value.length > 0 ? ve.value : ve.type;
setGeneralError((prev) =>
prev ? `${prev}\n${ve.property}: ${detail}` : `${ve.property}: ${detail}`,
);
} }
} }
} }
+19 -1
View File
@@ -4,9 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
import type { JmapSetError } from '@/types/jmap'; import type { JmapSetError, ValidationError } from '@/types/jmap';
import i18n from '@/i18n'; import i18n from '@/i18n';
export function validationErrorMessage(ve: ValidationError): string {
switch (ve.type) {
case 'Required':
return i18n.t('form.required', 'This field is required.');
case 'MaxLength':
return i18n.t('form.maxLengthIs', 'Maximum length is {{max}}.', { max: ve.required });
case 'MinLength':
return i18n.t('form.minLengthIs', 'Minimum length is {{min}}.', { min: ve.required });
case 'MaxValue':
return i18n.t('form.maxValueIs', 'Maximum value is {{max}}.', { max: ve.required });
case 'MinValue':
return i18n.t('form.minValueIs', 'Minimum value is {{min}}.', { min: ve.required });
default:
if (ve.value && ve.value.length > 0) return ve.value;
return i18n.t('form.invalidValue', 'Invalid value.');
}
}
export function friendlySetError(err: JmapSetError): string { export function friendlySetError(err: JmapSetError): string {
if (err.description) return err.description; if (err.description) return err.description;
switch (err.type) { switch (err.type) {
+1 -1
View File
@@ -60,6 +60,6 @@ export interface JmapSetError {
export interface ValidationError { export interface ValidationError {
type: 'Invalid' | 'Required' | 'MaxLength' | 'MinLength' | 'MaxValue' | 'MinValue'; type: 'Invalid' | 'Required' | 'MaxLength' | 'MinLength' | 'MaxValue' | 'MinValue';
property: string; property: string;
value?: unknown; value?: string;
required?: number; required?: number;
} }