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

This commit is contained in:
2026-08-22 00:26:45 +09:00
parent 72fefe89b0
commit 06d5344aef
9 changed files with 269 additions and 62 deletions
+50 -23
View File
@@ -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}