feat(core): full database soft-delete, live sidebar counts and robust subtask undo/restore across all modes
Build and Push Docker Image / build-and-push (push) Successful in 10m12s
Build and Push Docker Image / build-and-push (push) Successful in 10m12s
This commit is contained in:
@@ -117,6 +117,8 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
return () => document.removeEventListener("checkflow:openCommandPalette", handler);
|
||||
}, []);
|
||||
|
||||
const [apiTrashCount, setApiTrashCount] = useState(0);
|
||||
|
||||
// Task filter & reconstruct hierarchical structure for Demo and API modes
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
@@ -136,8 +138,17 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}
|
||||
} else {
|
||||
// Authenticated API mode
|
||||
// Also fetch trash count for sidebar
|
||||
fetch("/api/tasks?countTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : { count: 0 }))
|
||||
.then((data) => setApiTrashCount(data.count || 0))
|
||||
.catch(() => {});
|
||||
|
||||
if (isTrashActive) {
|
||||
setTasks([]);
|
||||
fetch("/api/tasks?isTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load trash tasks", err));
|
||||
} else if (selectedTag) {
|
||||
fetch(`/api/tasks?tag=${encodeURIComponent(selectedTag)}&showCompleted=${showCompleted}`)
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
@@ -195,7 +206,10 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
const store = getDemoStore();
|
||||
setTasks(getAllTrashTasks(store.tasks) as Task[]);
|
||||
} else {
|
||||
setTasks([]);
|
||||
fetch("/api/tasks?isTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load trash tasks", err));
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
@@ -357,7 +371,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}, 6000);
|
||||
}, [isDemo, selectedTask]);
|
||||
|
||||
// Undo delete
|
||||
// Undo delete (Restores exact soft-deleted task hierarchy)
|
||||
const handleUndoDelete = useCallback(async () => {
|
||||
if (!undoToast) return;
|
||||
if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
|
||||
@@ -377,28 +391,16 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
const res = await fetch(`/api/tasks/${task.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: task.title,
|
||||
listId: task.listId,
|
||||
parentId: task.parentId,
|
||||
note: task.note,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
priority: task.priority,
|
||||
}),
|
||||
body: JSON.stringify({ isDeleted: false, deletedAt: null }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const created = await res.json();
|
||||
if (created.listId === selectedListId) {
|
||||
setTasks((prev) => [...prev, created]);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to restore task", err);
|
||||
console.error("Failed to restore task via Undo", err);
|
||||
}
|
||||
}
|
||||
}, [undoToast, isDemo, selectedListId, refresh]);
|
||||
@@ -418,29 +420,47 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
};
|
||||
|
||||
// Restore from Trash (Restores task and all its nested subtasks)
|
||||
const handleRestoreTask = (id: string) => {
|
||||
const handleRestoreTask = async (id: string) => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = restoreTaskInTree(store.tasks, id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isDeleted: false, deletedAt: null }),
|
||||
});
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to restore task", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Permanent Delete
|
||||
const handlePermanentDeleteTask = (id: string) => {
|
||||
const handlePermanentDeleteTask = async (id: string) => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = deleteTaskInTree(store.tasks, id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}?permanent=true`, { method: "DELETE" });
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to permanently delete task", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Empty Trash
|
||||
const handleEmptyTrash = () => {
|
||||
const handleEmptyTrash = async () => {
|
||||
if (!confirm("Permanently empty all items in trash?")) return;
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
@@ -448,6 +468,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks([]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch("/api/tasks?trash=true", { method: "DELETE" });
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to empty trash", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -514,7 +541,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
onTagSelect={handleTagSelect}
|
||||
isTrashActive={isTrashActive}
|
||||
onTrashSelect={handleTrashSelect}
|
||||
trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : undefined}
|
||||
trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : apiTrashCount}
|
||||
mobileOpen={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
isDemo={isDemo}
|
||||
|
||||
Reference in New Issue
Block a user