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
This commit is contained in:
2026-07-29 19:26:05 +00:00
parent fb68009c23
commit 7ced6f2bf2
3 changed files with 81 additions and 3 deletions
+25 -1
View File
@@ -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');
@@ -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');
+27 -1
View File
@@ -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 {