mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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:
1 parent
7001308636
commit
121b23f4a2
21 files changed
+57
-52
No files matched your search
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Speichern",
|
||||
"savedToast": "Eigene Metrik gespeichert",
|
||||
"updatedToast": "Eigene Metrik aktualisiert",
|
||||
"saveError": "Eigene Metrik konnte nicht gespeichert werden"
|
||||
"saveError": "Eigene Metrik konnte nicht gespeichert werden",
|
||||
"nameTaken": "Es gibt bereits eine eigene Metrik mit diesem Namen."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Wert",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Save",
|
||||
"savedToast": "Custom metric saved",
|
||||
"updatedToast": "Custom metric updated",
|
||||
"saveError": "Could not save the custom metric"
|
||||
"saveError": "Could not save the custom metric",
|
||||
"nameTaken": "A custom metric with this name already exists."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Value",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Guardar",
|
||||
"savedToast": "Métrica personalizada guardada",
|
||||
"updatedToast": "Métrica personalizada actualizada",
|
||||
"saveError": "No se pudo guardar la métrica personalizada"
|
||||
"saveError": "No se pudo guardar la métrica personalizada",
|
||||
"nameTaken": "Ya existe una métrica personalizada con este nombre."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Valor",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Enregistrer",
|
||||
"savedToast": "Mesure personnalisée enregistrée",
|
||||
"updatedToast": "Mesure personnalisée mise à jour",
|
||||
"saveError": "Impossible d'enregistrer la mesure personnalisée"
|
||||
"saveError": "Impossible d'enregistrer la mesure personnalisée",
|
||||
"nameTaken": "Une mesure personnalisée portant ce nom existe déjà."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Valeur",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Salva",
|
||||
"savedToast": "Metrica personalizzata salvata",
|
||||
"updatedToast": "Metrica personalizzata aggiornata",
|
||||
"saveError": "Impossibile salvare la metrica personalizzata"
|
||||
"saveError": "Impossibile salvare la metrica personalizzata",
|
||||
"nameTaken": "Esiste già una metrica personalizzata con questo nome."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Valore",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "저장",
|
||||
"savedToast": "사용자 지표를 저장했어요",
|
||||
"updatedToast": "사용자 지표를 수정했어요",
|
||||
"saveError": "사용자 지표를 저장하지 못했어요"
|
||||
"saveError": "사용자 지표를 저장하지 못했어요",
|
||||
"nameTaken": "이 이름의 사용자 지표가 이미 있어요."
|
||||
},
|
||||
"entry": {
|
||||
"value": "값",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Opslaan",
|
||||
"savedToast": "Eigen meting opgeslagen",
|
||||
"updatedToast": "Eigen meting bijgewerkt",
|
||||
"saveError": "De eigen meting kon niet worden opgeslagen"
|
||||
"saveError": "De eigen meting kon niet worden opgeslagen",
|
||||
"nameTaken": "Er bestaat al een eigen meting met deze naam."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Waarde",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Zapisz",
|
||||
"savedToast": "Zapisano metrykę własną",
|
||||
"updatedToast": "Zaktualizowano metrykę własną",
|
||||
"saveError": "Nie udało się zapisać metryki własnej"
|
||||
"saveError": "Nie udało się zapisać metryki własnej",
|
||||
"nameTaken": "Metryka własna o tej nazwie już istnieje."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Wartość",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Salvar",
|
||||
"savedToast": "Métrica personalizada salva",
|
||||
"updatedToast": "Métrica personalizada atualizada",
|
||||
"saveError": "Não foi possível salvar a métrica personalizada"
|
||||
"saveError": "Não foi possível salvar a métrica personalizada",
|
||||
"nameTaken": "Já existe uma métrica personalizada com este nome."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Valor",
|
||||
|
||||
+2
-1
@@ -12536,7 +12536,8 @@
|
||||
"save": "Сохранить",
|
||||
"savedToast": "Свой показатель сохранён",
|
||||
"updatedToast": "Свой показатель обновлён",
|
||||
"saveError": "Не удалось сохранить свой показатель"
|
||||
"saveError": "Не удалось сохранить свой показатель",
|
||||
"nameTaken": "Свой показатель с таким названием уже существует."
|
||||
},
|
||||
"entry": {
|
||||
"value": "Значение",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user