From 72fefe89b06046baa82e282fd1f5c30ab998672b Mon Sep 17 00:00:00 2001 From: Neru_Han Date: Fri, 21 Aug 2026 23:34:23 +0900 Subject: [PATCH] fix(trash): implement robust ancestor-preserving restore and filterActiveTree for trash reliability --- src/components/layout/AppShell.tsx | 15 +++++--- src/components/layout/Sidebar.tsx | 8 ++--- src/lib/mockData.ts | 56 +++++++++++++++++++++++------- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 76cff55..1af09ef 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -20,6 +20,7 @@ import { findTaskInTree, getAllTrashTasks, filterTasksByTag, + filterActiveTree, } from "@/lib/mockData"; interface AppShellProps { @@ -126,10 +127,10 @@ export function AppShell({ user, isDemo = false }: AppShellProps) { } else if (selectedTag) { setTasks(filterTasksByTag(store.tasks, selectedTag) as Task[]); } else if (selectedListId) { - const listTasks = (store.tasks as Task[]).filter( - (t) => !t.isDeleted && t.listId === selectedListId && (showCompleted ? true : !t.completed) + const listTasks = filterActiveTree(store.tasks as MockTask[], showCompleted).filter( + (t) => t.listId === selectedListId ); - setTasks(listTasks); + setTasks(listTasks as Task[]); } else { setTasks([]); } @@ -337,7 +338,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) { const store = getDemoStore(); const newTasks = moveToTrashInTree(store.tasks, task.id); saveDemoStore(store.lists, newTasks); - setTasks((prev) => prev.filter((t) => t.id !== task.id)); + if (isTrashActive) { + setTasks(getAllTrashTasks(newTasks) as Task[]); + } else if (selectedListId) { + setTasks(filterActiveTree(newTasks, showCompleted).filter((t) => t.listId === selectedListId) as Task[]); + } else if (selectedTag) { + setTasks(filterTasksByTag(newTasks, selectedTag) as Task[]); + } if (selectedTask?.id === task.id) setSelectedTask(null); } else { setTasks((prev) => prev.filter((t) => t.id !== task.id)); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 9a43d2a..9fa7221 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -7,7 +7,7 @@ import { useTheme } from "@/app/providers"; import { LanguageSelector } from "@/components/ui/LanguageSelector"; import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu"; import { SettingsModal } from "@/components/settings/SettingsModal"; -import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount } from "@/lib/mockData"; +import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount, filterActiveTree, MockTask } from "@/lib/mockData"; interface User { id: string; name?: string | null; email?: string | null } interface List { id: string; name: string; color: string; icon: string; _count?: { tasks: number } } @@ -578,8 +578,8 @@ export function Sidebar({ ? (() => { if (typeof window === "undefined") return ""; const store = getDemoStore(); - const cnt = store.tasks.filter((t) => t.listId === list.id && !t.isDeleted && !t.completed).length; - return cnt > 0 ? cnt : ""; + const active = filterActiveTree(store.tasks as MockTask[], false).filter((t) => t.listId === list.id); + return active.length > 0 ? active.length : ""; })() : list._count?.tasks || ""} @@ -646,7 +646,7 @@ export function Sidebar({ ? getAllTrashTasks(getDemoStore().tasks).length : (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount); return count > 0 ? ( - {count} + {count} ) : null; })()} diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts index 9c5e8fc..c62bdd8 100644 --- a/src/lib/mockData.ts +++ b/src/lib/mockData.ts @@ -278,24 +278,56 @@ export function moveToTrashInTree(tree: MockTask[], id: string): MockTask[] { }); } -// Restore from Trash (recursively unmark node and its children) -export function restoreTaskInTree(tree: MockTask[], id: string): MockTask[] { - const markRestored = (node: MockTask): MockTask => ({ +// Restore task (and all its subtasks) from Trash, ensuring ancestor parents are also un-deleted +export function restoreTaskInTree(tree: MockTask[], targetId: string): MockTask[] { + const unmarkNodeAndChildren = (node: MockTask): MockTask => ({ ...node, isDeleted: false, deletedAt: null, - children: node.children ? node.children.map(markRestored) : [], + children: node.children ? node.children.map(unmarkNodeAndChildren) : [], }); - return tree.map((node) => { - if (node.id === id) { - return markRestored(node); + function processNodes(nodes: MockTask[]): { updated: MockTask[]; found: boolean } { + let foundInThisLevel = false; + const updated = nodes.map((node) => { + if (node.id === targetId) { + foundInThisLevel = true; + return unmarkNodeAndChildren(node); + } + if (node.children && node.children.length > 0) { + const res = processNodes(node.children); + if (res.found) { + foundInThisLevel = true; + // If a child was restored, ancestor parent must also be restored + return { + ...node, + isDeleted: false, + deletedAt: null, + children: res.updated, + }; + } + return { ...node, children: res.updated }; + } + return node; + }); + return { updated, found: foundInThisLevel }; + } + + return processNodes(tree).updated; +} + +// Recursively filter active tree (stripping isDeleted tasks at all depths) +export function filterActiveTree(nodes: MockTask[], showCompleted = true): MockTask[] { + const result: MockTask[] = []; + for (const node of nodes) { + if (!node.isDeleted) { + if (showCompleted || !node.completed) { + const cleanChildren = node.children ? filterActiveTree(node.children, showCompleted) : []; + result.push({ ...node, children: cleanChildren }); + } } - if (node.children && node.children.length > 0) { - return { ...node, children: restoreTaskInTree(node.children, id) }; - } - return node; - }); + } + return result; } // Permanent delete