diff --git a/frontend/src/components/BulkToolbar.jsx b/frontend/src/components/BulkToolbar.jsx index ef2bcfe..97fa140 100644 --- a/frontend/src/components/BulkToolbar.jsx +++ b/frontend/src/components/BulkToolbar.jsx @@ -1,30 +1,93 @@ +import { useState } from 'react'; +import { MailOpen, Mail, Star, Archive, Trash2 } from 'lucide-react'; import { BulkApi } from '../api/client.js'; +import { + Button, useToast, + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, +} from './ui'; -/// -/// Toolbar shown above an email list when one or more rows are selected. -/// `onDone` is called with the action key after a successful bulk call so the -/// caller can update local state (e.g. remove trashed/archived rows). -/// +/** + * Toolbar shown above an email list when rows are selected. + * + * Safety (Phase 4 / UX-Critical): the destructive Trash action now requires an + * explicit confirmation dialog, every action surfaces success/partial-failure via + * a toast, and rows are only removed from the list when the server confirms the + * whole batch succeeded (no more optimistic removal that hides failures). + * `onDone(action, ids)` is called only on full success so the caller can prune state. + */ export default function BulkToolbar({ selectedIds, onDone, onClear }) { + const { toast } = useToast(); + const [busy, setBusy] = useState(false); + const [confirmTrash, setConfirmTrash] = useState(false); const count = selectedIds.length; if (count === 0) return null; - const run = (fn, action) => async () => { - await fn(selectedIds); - onDone(action, selectedIds); - onClear(); + const apply = async (fn, action, label) => { + setBusy(true); + try { + const res = await fn(selectedIds); + // CleanupResultDto: { succeededCount, failedCount, errors } + const ok = res?.succeededCount ?? count; + const failed = res?.failedCount ?? 0; + if (failed > 0) { + toast({ + variant: 'warning', + title: `${label}: ${ok} done, ${failed} failed`, + description: 'Some items could not be updated — the list was left unchanged so you can retry.', + }); + } else { + toast({ variant: 'success', title: `${label} ${ok} email${ok === 1 ? '' : 's'}` }); + onDone(action, selectedIds); + onClear(); + } + } catch { + toast({ variant: 'danger', title: `Couldn't ${label.toLowerCase()} ${count} email${count === 1 ? '' : 's'}`, description: 'Please try again.' }); + } finally { + setBusy(false); + setConfirmTrash(false); + } }; return ( -
- {count} selected -
- - - - - - +
+ {count} selected +
+ + + + + + + + !busy && setConfirmTrash(o)}> + + + Move {count} email{count === 1 ? '' : 's'} to Trash? + + This moves the selected mail to your Gmail Trash, where it stays recoverable + for 30 days before Gmail permanently deletes it. + + + + + + + + + +
); }