fix(coach): delete-with-undo replaces the arm-forever confirm

The history rail + /coach/conversations armed a row on the first tap
and only disarmed on a successful delete — no timeout, no
outside-click reset — so a row armed minutes earlier deleted on a
later stray tap, and there was no way back once it fired. Replace it
with the same delayed-commit delete-with-undo the Documents surface
already uses: one tap hides the row and schedules the real DELETE a
few seconds out; the toast's Undo cancels it. Centralised in
useDeleteCoachConversationWithUndo so both surfaces, which carried
byte-identical arm/confirm logic, share one implementation.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 03:52:32 +02:00
1 parent 9531bbdcec
commit 8c385b9ac2
9 files changed
+136 -59

No files matched your search

+20 -24
View File
@@ -10,6 +10,7 @@ import {
Target,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -18,7 +19,7 @@ import { QueryErrorCard } from "@/components/ui/query-error-card";
import { COACH_SCROLLBAR } from "@/components/insights/coach-panel/message-thread";
import {
useCoachConversations,
useDeleteCoachConversation,
useDeleteCoachConversationWithUndo,
} from "@/components/insights/coach-panel/use-coach";
import type { CoachConversationDTO } from "@/lib/ai/coach/types";
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
@@ -102,16 +103,17 @@ function CoachConversationsBody() {
const router = useRouter();
const { conversations, isLoading, isError, refetch } =
useCoachConversations();
const deleteMutation = useDeleteCoachConversation();
const { pendingDeleteIds, requestDelete, undoDelete } =
useDeleteCoachConversationWithUndo();
const [filter, setFilter] = useState<string>("");
const [confirmId, setConfirmId] = useState<string | null>(null);
// Mirror the rail's title substring filter.
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
if (!q) return conversations;
return conversations.filter((c) => c.title.toLowerCase().includes(q));
}, [conversations, filter]);
const visible = conversations.filter((c) => !pendingDeleteIds.has(c.id));
if (!q) return visible;
return visible.filter((c) => c.title.toLowerCase().includes(q));
}, [conversations, filter, pendingDeleteIds]);
const groups = useMemo(() => groupByRecency(filtered), [filtered]);
@@ -120,13 +122,17 @@ function CoachConversationsBody() {
router.push(`/coach?c=${id}`);
}
// v1.30.1 M5 — one tap deletes (with an Undo toast); see
// `use-coach.ts`'s `useDeleteCoachConversationWithUndo` for the
// rationale — this mirrors the rail's identical fix.
function handleDeleteRequest(id: string) {
if (confirmId === id) {
deleteMutation.mutate(id);
setConfirmId(null);
} else {
setConfirmId(id);
}
requestDelete(id);
toast.success(t("insights.coach.historyDeleted"), {
action: {
label: t("common.undo"),
onClick: () => undoDelete(id),
},
});
}
return (
@@ -212,7 +218,6 @@ function CoachConversationsBody() {
</h2>
<ul className="flex flex-col gap-1">
{group.conversations.map((c) => {
const isConfirming = confirmId === c.id;
return (
<li
key={c.id}
@@ -260,18 +265,9 @@ function CoachConversationsBody() {
variant="ghost"
size="icon-lg"
onClick={() => handleDeleteRequest(c.id)}
aria-label={
isConfirming
? t("insights.coach.historyDeleteConfirm")
: t("insights.coach.historyDeleteAria")
}
aria-label={t("insights.coach.historyDeleteAria")}
data-slot="coach-conversations-delete"
data-confirming={isConfirming ? "true" : undefined}
className={cn(
"shrink-0",
isConfirming &&
"text-destructive hover:text-destructive",
)}
className="shrink-0"
>
<Trash2 className="size-4" aria-hidden="true" />
</Button>