fix(forms): show the form's own sentence when a save is refused

The custom metric, allergy, vaccination, booster, visit, address-book,
workout and quick-add dose forms printed the server's error string as
it came: English in every locale, and for a network failure the
browser's own TypeError text. They now resolve the error code through
localizedApiError like the other entry forms, so a known refusal reads
in the person's language and anything else falls back to the form's
"could not save" sentence. A custom metric whose name is taken keeps
its specific message, now translated.

The workout form's browser test expected the server prose and now
expects the form's sentence.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 19:57:06 +01:00
1 parent 7001308636
commit 121b23f4a2
21 files changed
+57 -52

No files matched your search

@@ -11,7 +11,8 @@ import { DateTimeField } from "@/components/ui/date-time-field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { ApiError, apiPost } from "@/lib/api/api-fetch";
import { apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -99,11 +100,7 @@ export function CustomMetricEntryForm({
});
onSuccess?.();
} catch (err) {
const message =
err instanceof ApiError
? err.message
: t("customMetrics.entry.saveError");
setError(message);
setError(localizedApiError(err, t, "customMetrics.entry.saveError"));
} finally {
setSubmitting(false);
}
@@ -11,6 +11,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ApiError, apiPatch, apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import type { CustomMetricDto } from "./types";
@@ -122,11 +123,11 @@ export function CustomMetricForm({
);
onSuccess?.(saved);
} catch (err) {
const message =
err instanceof ApiError
? err.message
: t("customMetrics.form.saveError");
setError(message);
setError(
err instanceof ApiError && err.status === 409
? t("customMetrics.form.nameTaken")
: localizedApiError(err, t, "customMetrics.form.saveError"),
);
} finally {
setSubmitting(false);
}
@@ -22,7 +22,8 @@ import {
} from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { useTableSort } from "@/hooks/use-table-sort";
import { ApiError, apiDelete, apiPatch, apiPost } from "@/lib/api/api-fetch";
import { apiDelete, apiPatch, apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import { formatDateShort } from "@/lib/format";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
@@ -140,11 +141,7 @@ export function CustomMetricHistoryList({
closeEdit();
},
onError: (err) => {
setEditError(
err instanceof ApiError
? err.message
: t("customMetrics.entry.editError"),
);
setEditError(localizedApiError(err, t, "customMetrics.entry.editError"));
},
});
@@ -27,7 +27,8 @@ import {
} from "@/lib/medications/default-medication";
import { useAuth } from "@/hooks/use-auth";
import { useActiveRecordName } from "@/hooks/use-record-capabilities";
import { ApiError, apiGet, apiPost } from "@/lib/api/api-fetch";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import {
intakeToastOptions,
runUndoIntake,
@@ -270,9 +271,11 @@ export function MedicationIntakeQuickAdd({
void invalidateMedicationReads(queryClient);
} catch (err) {
setError(
err instanceof ApiError && err.message
? err.message
: t("dashboard.medicationIntakeQuickAdd.saveError"),
localizedApiError(
err,
t,
"dashboard.medicationIntakeQuickAdd.saveError",
),
);
} finally {
setLoading(false);
@@ -49,6 +49,7 @@ import {
import { Button } from "@/components/ui/button";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import { useEncounterMutations, type Encounter } from "@/hooks/use-encounters";
import {
@@ -128,8 +129,7 @@ export function EncounterSheet({
onSaved?.(saved);
onOpenChange(false);
} catch (err) {
const message =
err instanceof Error ? err.message : t("encounters.saveFailed");
const message = localizedApiError(err, t, "encounters.saveFailed");
setError(message);
showErrorToast(message, { cause: err });
}
@@ -20,6 +20,7 @@ import { FieldGroup } from "@/components/ui/field-group";
import { Input } from "@/components/ui/input";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Textarea } from "@/components/ui/textarea";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import {
usePractitionerMutations,
@@ -104,8 +105,7 @@ export function PractitionerSheet({
onSaved?.(saved);
onOpenChange(false);
} catch (err) {
const message =
err instanceof Error ? err.message : t("practitioners.saveFailed");
const message = localizedApiError(err, t, "practitioners.saveFailed");
setError(message);
showErrorToast(message, { cause: err });
}
+3 -6
View File
@@ -17,7 +17,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ApiError, apiPost, apiPut } from "@/lib/api/api-fetch";
import { apiPost, apiPut } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import {
allergyCategoryEnum,
allergySeverityEnum,
@@ -113,11 +114,7 @@ export function AllergyForm({
);
onSuccess?.(saved);
} catch (err) {
const message =
err instanceof ApiError
? err.message
: t("records.allergies.form.saveError");
setError(message);
setError(localizedApiError(err, t, "records.allergies.form.saveError"));
} finally {
setSubmitting(false);
}
@@ -33,6 +33,7 @@ import {
import { FieldGroup } from "@/components/ui/field-group";
import { Input } from "@/components/ui/input";
import { NativeSelect } from "@/components/ui/native-select";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import {
customLookupOf,
@@ -105,10 +106,9 @@ export function BoosterMintPrompt({
});
toastWrittenOutcome("success", t("vaccinations.booster.planned"));
} catch (err) {
showErrorToast(
err instanceof Error ? err.message : t("vaccinations.booster.failed"),
{ cause: err },
);
showErrorToast(localizedApiError(err, t, "vaccinations.booster.failed"), {
cause: err,
});
} finally {
onClose();
}
@@ -36,6 +36,7 @@ import {
import { Button } from "@/components/ui/button";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import { RelatedRecordsBlock } from "@/components/links/related-records";
import type { RelatedLink } from "@/lib/links/related";
@@ -99,8 +100,7 @@ export function VaccinationSheet({
onCreated?.(created);
}
} catch (err) {
const message =
err instanceof Error ? err.message : t("vaccinations.saveFailed");
const message = localizedApiError(err, t, "vaccinations.saveFailed");
setError(message);
showErrorToast(message, { cause: err });
}
@@ -128,9 +128,10 @@ describe("<ManualWorkoutForm>", () => {
await pickSport("Running");
await screen.getByLabelText("Minutes").fill("30");
await save.click();
// The form's own sentence, never the server's English prose.
await expect
.element(screen.getByRole("alert"))
.toHaveTextContent("Service unavailable");
.toHaveTextContent("The workout could not be saved");
expect(onSuccess).not.toHaveBeenCalled();
api.on("POST", BATCH, {
@@ -21,7 +21,8 @@ import {
} from "@/components/ui/select";
import { useAuth } from "@/hooks/use-auth";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { ApiError, apiPost } from "@/lib/api/api-fetch";
import { apiPost } from "@/lib/api/api-fetch";
import { localizedApiError } from "@/lib/api/localized-error";
import { useTranslations } from "@/lib/i18n/context";
import { getQuantityTransform } from "@/lib/measurements/display-transform";
import {
@@ -232,9 +233,7 @@ export function ManualWorkoutForm({
onSuccess?.();
} catch (err) {
setSaveError(
err instanceof ApiError && err.message
? err.message
: t("insights.workouts.manual.saveError"),
localizedApiError(err, t, "insights.workouts.manual.saveError"),
);
} finally {
setSaving(false);