From 7ced6f2bf2b99d811d55e74ebed1f7f5e5c09aad Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:05 +0000 Subject: [PATCH] feat: add undo toasts for task and note deletions - Capture deleted item before DELETE call - Show sonner toast with Undo action button - On Undo, POST to recreate the item with original data - Applied to tasks-list-view, task-detail-panel, and notes page --- apps/web/app/(dashboard)/notes/page.tsx | 26 +++++++++++++++- .../components/tasks/task-detail-panel.tsx | 30 ++++++++++++++++++- apps/web/components/tasks/tasks-list-view.tsx | 28 ++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(dashboard)/notes/page.tsx b/apps/web/app/(dashboard)/notes/page.tsx index fa07576..33f1816 100644 --- a/apps/web/app/(dashboard)/notes/page.tsx +++ b/apps/web/app/(dashboard)/notes/page.tsx @@ -226,6 +226,7 @@ export default function NotesPage() { } ++saveVersion.current; setDeleting(true); + const deletedNote = { ...noteToDelete }; try { const response = await fetch(`/api/domains/${domainId}/notes/${noteToDelete.id}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Unable to delete note.'); @@ -234,7 +235,30 @@ export default function NotesPage() { selected?.id === noteToDelete.id ? notes.find((note) => note.id !== noteToDelete.id) || null : selected ); setNoteToDelete(null); - toast.success('Note deleted'); + toast.success('Note deleted', { + action: { + label: 'Undo', + onClick: async () => { + try { + const res = await fetch(`/api/domains/${domainId}/notes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: deletedNote.title, + content: deletedNote.content, + }), + }); + if (!res.ok) throw new Error(); + const restored = await res.json(); + setNotes((current) => [restored, ...current]); + setSelectedNote(restored); + toast.success('Note restored'); + } catch { + toast.error('Unable to restore note'); + } + }, + }, + }); } catch (error) { console.error('Failed to delete note:', error); toast.error('Unable to delete note'); diff --git a/apps/web/components/tasks/task-detail-panel.tsx b/apps/web/components/tasks/task-detail-panel.tsx index d18f084..44c2082 100644 --- a/apps/web/components/tasks/task-detail-panel.tsx +++ b/apps/web/components/tasks/task-detail-panel.tsx @@ -148,6 +148,7 @@ export function TaskDetailPanel({ async function handleDelete() { if (!task || !domainId) return; setDeleting(true); + const deletedTask = { ...task }; try { const response = await fetch(`/api/domains/${domainId}/tasks/${task.id}`, { method: 'DELETE', @@ -156,7 +157,34 @@ export function TaskDetailPanel({ onUpdate(); setDeleteOpen(false); onOpenChange(false); - toast.success('Task deleted'); + toast.success('Task deleted', { + action: { + label: 'Undo', + onClick: async () => { + try { + await fetch(`/api/domains/${domainId}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: deletedTask.title, + description: deletedTask.description, + status: deletedTask.status, + priority: deletedTask.priority, + dueDate: deletedTask.dueDate, + estimatedMinutes: deletedTask.estimatedMinutes, + projectId: deletedTask.projectId, + sectionId: deletedTask.sectionId, + parentId: deletedTask.parentId, + }), + }); + onUpdate(); + toast.success('Task restored'); + } catch { + toast.error('Unable to restore task'); + } + }, + }, + }); } catch (error) { console.error('Failed to delete task:', error); toast.error('Unable to delete task'); diff --git a/apps/web/components/tasks/tasks-list-view.tsx b/apps/web/components/tasks/tasks-list-view.tsx index 2e7909a..bf9ba53 100644 --- a/apps/web/components/tasks/tasks-list-view.tsx +++ b/apps/web/components/tasks/tasks-list-view.tsx @@ -284,12 +284,38 @@ export function TasksListView({ onClick={async () => { if (!deleteId || !domainId) return; setDeleting(true); + const deletedTask = tasks.find(t => t.id === deleteId); try { const res = await fetch(`/api/domains/${domainId}/tasks/${deleteId}`, { method: 'DELETE' }); if (!res.ok) throw new Error(); - toast.success('Task deleted'); setDeleteId(null); fetchTasks(); + toast.success('Task deleted', { + action: { + label: 'Undo', + onClick: async () => { + if (!deletedTask) return; + try { + await fetch(`/api/domains/${domainId}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: deletedTask.title, + description: deletedTask.description, + status: deletedTask.status, + priority: deletedTask.priority, + dueDate: deletedTask.dueDate, + projectId: deletedTask.projectId, + }), + }); + fetchTasks(); + toast.success('Task restored'); + } catch { + toast.error('Unable to restore task'); + } + }, + }, + }); } catch { toast.error('Unable to delete task'); } finally {