fix(errors) : humanize backend error messages for end users

Add centralized error translation layer (humanizeError) that converts
raw Symfony/Doctrine/API Platform messages into user-friendly French.
Fix useApi to extract errors from all backend response formats
(violations, error, message, hydra:description, detail).
Add toast deduplication to prevent double display. Replace error toast
icon (X → CircleX) to distinguish from the dismiss button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-04 09:48:51 +01:00
parent 55e2a4fafe
commit c7fd8328d6
15 changed files with 227 additions and 49 deletions

View File

@@ -24,7 +24,7 @@
class="w-4 h-4"
aria-hidden="true"
/>
<IconLucideX
<IconLucideCircleX
v-else-if="toast.type === 'error'"
class="w-4 h-4"
aria-hidden="true"
@@ -64,6 +64,7 @@
import { useToast } from '~/composables/useToast'
import IconLucideCheck from '~icons/lucide/check'
import IconLucideX from '~icons/lucide/x'
import IconLucideCircleX from '~icons/lucide/circle-x'
import IconLucideAlertTriangle from '~icons/lucide/alert-triangle'
import IconLucideInfo from '~icons/lucide/info'

View File

@@ -119,6 +119,7 @@ import {
type ModelTypeListResponse,
} from "~/services/modelTypes";
import { useToast } from "~/composables/useToast";
import { humanizeError } from "~/shared/utils/errorMessages";
import { invalidateEntityTypeCache } from "~/composables/useEntityTypes";
const DEFAULT_DESCRIPTION =
@@ -183,7 +184,8 @@ useHead(() => ({
title: headingText.value,
}));
const extractErrorMessage = (error: unknown) => {
const extractErrorMessage = (error: unknown): string => {
let raw: string | null = null;
if (error && typeof error === "object") {
const maybeFetchError = error as {
data?: Record<string, unknown>;
@@ -192,21 +194,16 @@ const extractErrorMessage = (error: unknown) => {
};
if (maybeFetchError.data) {
const data = maybeFetchError.data;
if (typeof data.message === "string") {
return data.message;
}
if (Array.isArray(data.message) && data.message.length > 0) {
return data.message[0];
}
}
if (typeof maybeFetchError.statusMessage === "string") {
return maybeFetchError.statusMessage;
}
if (typeof maybeFetchError.message === "string") {
return maybeFetchError.message;
if (typeof data['hydra:description'] === "string") raw = data['hydra:description'];
else if (typeof data.detail === "string") raw = data.detail;
else if (typeof data.message === "string") raw = data.message;
else if (Array.isArray(data.message) && data.message.length > 0) raw = data.message[0];
else if (typeof data.error === "string") raw = data.error;
}
if (!raw && typeof maybeFetchError.statusMessage === "string") raw = maybeFetchError.statusMessage;
if (!raw && typeof maybeFetchError.message === "string") raw = maybeFetchError.message;
}
return "Une erreur est survenue lors de la communication avec le serveur.";
return humanizeError(raw);
};
const refresh = async ({