From ba6b4472d2536e4c229d1bee6ed1e4f305f9f8fa Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:12:30 +0200 Subject: [PATCH] Display validation errors returned by the server --- CHANGELOG.md | 1 + src/components/bootstrap/BootstrapWizard.tsx | 16 ++------------- src/components/forms/DynamicForm.tsx | 21 ++++++-------------- src/lib/jmapErrors.ts | 20 ++++++++++++++++++- src/types/jmap.ts | 2 +- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbffcc..76cd41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. This projec ### Changed ### Fixed +- Display validation errors returned by the server. ## [1.0.1] - 2026-04-25 diff --git a/src/components/bootstrap/BootstrapWizard.tsx b/src/components/bootstrap/BootstrapWizard.tsx index 3251169..331df61 100644 --- a/src/components/bootstrap/BootstrapWizard.tsx +++ b/src/components/bootstrap/BootstrapWizard.tsx @@ -19,7 +19,7 @@ import { toast } from '@/hooks/use-toast'; import { resolveObject, resolveSchema, resolveForm, buildCreateDefaults, deepMerge } from '@/lib/schemaResolver'; import { calculateJmapPatch } from '@/lib/jmapPatch'; 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 { JmapSetError, JmapSetResponse } from '@/types/jmap'; @@ -181,19 +181,7 @@ export function BootstrapWizard() { for (const ve of error.validationErrors) { const top = ve.property?.split('/')[0] ?? ''; if (!top) continue; - const msg = - 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); + record(top, validationErrorMessage(ve)); } } diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index 5d1e89f..1f90d37 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -46,7 +46,7 @@ import { } from '@/lib/schemaResolver'; import { jmapGet, jmapSet, jmapRequest, getAccountId } from '@/services/jmap/client'; import { calculateJmapPatch } from '@/lib/jmapPatch'; -import { friendlySetError } from '@/lib/jmapErrors'; +import { friendlySetError, validationErrorMessage } from '@/lib/jmapErrors'; import { coerceLabel } from '@/lib/objectOptions'; import { SECRET_MASK } from '@/lib/jmapUtils'; import { toast } from '@/hooks/use-toast'; @@ -347,21 +347,12 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) { if (error.validationErrors && error.validationErrors.length > 0) { for (const ve of error.validationErrors) { if (ve.property && currentFields?.properties[ve.property]) { - const msg = - 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; + newFieldErrors[ve.property] = validationErrorMessage(ve); } 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}`, + ); } } } diff --git a/src/lib/jmapErrors.ts b/src/lib/jmapErrors.ts index d48d3f4..9b483f2 100644 --- a/src/lib/jmapErrors.ts +++ b/src/lib/jmapErrors.ts @@ -4,9 +4,27 @@ * 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'; +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 { if (err.description) return err.description; switch (err.type) { diff --git a/src/types/jmap.ts b/src/types/jmap.ts index 0864675..c16eabb 100644 --- a/src/types/jmap.ts +++ b/src/types/jmap.ts @@ -60,6 +60,6 @@ export interface JmapSetError { export interface ValidationError { type: 'Invalid' | 'Required' | 'MaxLength' | 'MinLength' | 'MaxValue' | 'MinValue'; property: string; - value?: unknown; + value?: string; required?: number; }