Confirm-and-report action
What this solves
Interaction feedback: a confirm dialog cannot stay open until a network call resolves without freezing the screen behind it, so it closes the moment the user confirms. Between that close and the request settling there is a real gap where nothing on screen says the action is running. The promise toast covers it, walking pending to success or error on its own.
Use it when
- A destructive or state-changing action on a row: remove, disable, resetwalmart-mvp Settings.tsx removeMember, speedway team.tsx and accounts.tsx removeConfirmed
- Any confirm gate reused for more than one action on the same kind of rowspeedway accounts.tsx: the same ConfirmModal<MemberRow> shape wired to reset-password and toggle-access, not just remove
Rendered
Fixture data; check both themes.- JAJamie Liujamie@versable.ai
- ALAlex Chenalex@versable.ai
patterns/confirm-and-report/example.tsxCopy this into a fresh route and it renders as above.
"use client";import { Button, Card, ConfirmModal, IdentityRow, pushAlert, useModal } from "@versable-git/ui";// Fixture members. shouldFail stands in for whatever a real endpoint would// reject on (a last-owner guard, a network error). It lets this one page// reach both toast outcomes on demand.interface Member { id: string; name: string; email: string; shouldFail?: boolean;}const MEMBERS: Member[] = [ { id: "m1", name: "Jamie Liu", email: "jamie@versable.ai" }, { id: "m2", name: "Alex Chen", email: "alex@versable.ai", shouldFail: true },];const REMOVE_MEMBER_MODAL = "confirm-and-report-remove-member";// Fixture request standing in for the real API call. Real callers await a// route action or an API client here; this page only needs something async.function removeMember(member: Member): Promise<void> { return new Promise((resolve, reject) => { setTimeout(() => { if (member.shouldFail) reject(new Error(`${member.name} is the last owner. Remove another owner first.`)); else resolve(); }, 900); });}export function ConfirmAndReport() { const { openModal } = useModal<Member>(); return ( <div className="flex flex-col gap-3"> <Card noBorder noAnimate bodyClassName="px-0"> <ul className="flex flex-col gap-1"> {MEMBERS.map((m) => ( <li key={m.id} className="flex items-center justify-between gap-3 px-1 py-1.5"> <IdentityRow avatar={m.name.slice(0, 2).toUpperCase()} title={m.name} subtitle={m.email} size="sm" /> <Button size="sm" variant="outline" color="error" Icon="Trash" content="Remove" onClick={() => openModal(REMOVE_MEMBER_MODAL, null, m)} /> </li> ))} </ul> </Card> {/* After speedway/app/routes/admin/team.tsx:507-521: the row's own identity carries into the modal's message, typed through Args. */} <ConfirmModal<Member> elementId={REMOVE_MEMBER_MODAL} title="Remove member" preset="delete" message={(m) => ( <span> Remove <b>{m?.name}</b>? They lose access to this workspace immediately. </span> )} onConfirm={(m, close) => { // After walmart-mvp/frontend/src/pages/Settings.tsx:359-360: "The // confirm modal is gone before the request settles; the promise // toast is the only affordance covering the gap." Close first, // then fire the request, never the other way round. close(); if (!m) return; void pushAlert.promise(removeMember(m), { pending: `Removing ${m.name}`, success: `${m.name} removed from the workspace.`, error: (err) => (err instanceof Error ? err.message : "Something went wrong"), }); }} /> </div> );}Where it ships
walmart-mvp/frontend/src/pages/Settings.tsx:352-368removeConfirmed: pushAlert.promise(authApi.removeMember(...), ...) with the in-code comment naming the gap the toast coversspeedway/app/routes/admin/team.tsx:507-529ConfirmModal<MemberRow> for a password reset: onConfirm submits the route action, then closesspeedway/app/routes/account/accounts.tsx:888-910the same ConfirmModal<MemberRow> shape on a second admin surface, near-identical wiring
App-specific: Real callers key the confirm on a typed row (MemberRow) and hand the request to a route action via submit(...) or an API client, with per-action toast copy (reset, remove, disable). This page swaps the request for a fixture promise so the close-then-toast order is the only thing on display.