From 6eb79a1d47b3c330e03fe3f14f3447bf4373cd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 2 Oct 2026 21:31:25 +0200 Subject: [PATCH] fix(workouts): report manual saves through the outcome module and register the delete A repeated submit that wrote nothing reads as a neutral notice rather than a success. The delete control is entered in the destructive-control registry, and its 409 code in the published error catalogue. Refs #1085 --- src/__tests__/destructive-control-guard.test.ts | 7 +++++++ .../workouts/delete-workout-button.tsx | 6 +++--- src/components/workouts/manual-workout-form.tsx | 17 +++++++++++------ src/lib/openapi/error-codes.ts | 1 + 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/__tests__/destructive-control-guard.test.ts b/src/__tests__/destructive-control-guard.test.ts index fcf209cfe..49b4bf75f 100644 --- a/src/__tests__/destructive-control-guard.test.ts +++ b/src/__tests__/destructive-control-guard.test.ts @@ -380,6 +380,13 @@ const REGISTRY: DestructiveEntry[] = [ recovery: "tombstoned-no-restore", confirm: ["AlertDialog"], }, + { + file: "components/workouts/delete-workout-button.tsx", + destroys: + "one workout entered by hand, with the personal records it set; synced workouts are refused by the route", + recovery: "permanent", + confirm: ["AlertDialog"], + }, // ── Visits and the address book ───────────────────────────────────────── { diff --git a/src/components/workouts/delete-workout-button.tsx b/src/components/workouts/delete-workout-button.tsx index bc6e84b19..904ea6280 100644 --- a/src/components/workouts/delete-workout-button.tsx +++ b/src/components/workouts/delete-workout-button.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { useQueryClient, type QueryClient } from "@tanstack/react-query"; import { Loader2, Trash2 } from "lucide-react"; -import { toast } from "sonner"; import { AlertDialog, @@ -17,6 +16,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; +import { toastWrittenOutcome } from "@/components/outcome/outcome-toast"; import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; import { apiDelete } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; @@ -78,11 +78,11 @@ export function DeleteWorkoutButton({ setDeleting(true); try { await deleteManualWorkout(workout.id, queryClient); - toast.success(t("insights.workouts.manual.deleted")); + toastWrittenOutcome("success", t("insights.workouts.manual.deleted")); setOpen(false); router.replace("/insights/workouts"); } catch { - toast.error(t("insights.workouts.manual.deleteError")); + toastWrittenOutcome("failed", t("insights.workouts.manual.deleteError")); } finally { setDeleting(false); } diff --git a/src/components/workouts/manual-workout-form.tsx b/src/components/workouts/manual-workout-form.tsx index d50f130f9..9902cac66 100644 --- a/src/components/workouts/manual-workout-form.tsx +++ b/src/components/workouts/manual-workout-form.tsx @@ -4,9 +4,9 @@ import { useId, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useQueryClient, type QueryClient } from "@tanstack/react-query"; import { Loader2, Plus } from "lucide-react"; -import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { toastWrittenOutcome } from "@/components/outcome/outcome-toast"; import { DateTimeField } from "@/components/ui/date-time-field"; import { FieldError } from "@/components/forms/field-error"; import { Input } from "@/components/ui/input"; @@ -166,11 +166,16 @@ export function ManualWorkoutForm({ setSaving(true); try { const outcome = await saveManualWorkout(built.entry, queryClient); - toast.success( - outcome === "duplicate" - ? t("insights.workouts.manual.alreadySaved") - : t("common.saved"), - ); + // A duplicate wrote nothing new: the same form already landed this + // session. That is reported as such, not as a fresh save. + if (outcome === "duplicate") { + toastWrittenOutcome( + "empty", + t("insights.workouts.manual.alreadySaved"), + ); + } else { + toastWrittenOutcome("success", t("common.saved")); + } onSuccess?.(); } catch (err) { setSaveError( diff --git a/src/lib/openapi/error-codes.ts b/src/lib/openapi/error-codes.ts index 39ea316f3..d54d2e68e 100644 --- a/src/lib/openapi/error-codes.ts +++ b/src/lib/openapi/error-codes.ts @@ -357,6 +357,7 @@ export const ERROR_CODE_CATALOGUE: Readonly> = "workout.batch.payload_too_large", "workout.batch.source_not_permitted", "workout.batch.too_large", + "workout.delete.synced_source", ], "(unprefixed)": [ "about_me_conflict",