From fc70b1e8cbaa5367ec441eb22947239432cc009c Mon Sep 17 00:00:00 2001 From: Neru_Han Date: Fri, 21 Aug 2026 23:00:50 +0900 Subject: [PATCH] fix(trash & tags): fix subtasks hierarchy restoration in trash and real-time tag counts sync --- src/components/layout/AppShell.tsx | 6 +++--- src/components/layout/Sidebar.tsx | 28 +++++++++++++++++----------- src/components/tasks/TaskDetail.tsx | 11 ++++++++--- src/lib/mockData.ts | 28 ++++++++++++++++------------ 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index fe2d730..76cff55 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -410,13 +410,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) { } }; - // Restore from Trash + // Restore from Trash (Restores task and all its nested subtasks) const handleRestoreTask = (id: string) => { if (isDemo) { const store = getDemoStore(); const newTasks = restoreTaskInTree(store.tasks, id); saveDemoStore(store.lists, newTasks); - setTasks((prev) => prev.filter((t) => t.id !== id)); + setTasks(getAllTrashTasks(newTasks) as Task[]); refresh(); } }; @@ -427,7 +427,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) { const store = getDemoStore(); const newTasks = deleteTaskInTree(store.tasks, id); saveDemoStore(store.lists, newTasks); - setTasks((prev) => prev.filter((t) => t.id !== id)); + setTasks(getAllTrashTasks(newTasks) as Task[]); refresh(); } }; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 89646ac..9a43d2a 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 } from "@/lib/mockData"; +import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount } 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 } } @@ -607,16 +607,22 @@ export function Sidebar({ {tags.length > 0 && (
🏷️ {t("tags") || "Tags"}
- {tags.map((tag) => ( -
onTagSelect(selectedTag === tag.name ? null : tag.name)} - > - # - {tag.name} -
- ))} + {tags.map((tag) => { + const tagCount = isDemo && typeof window !== "undefined" + ? getTagTaskCount(getDemoStore().tasks, tag.name) + : 0; + return ( +
onTagSelect(selectedTag === tag.name ? null : tag.name)} + > + # + {tag.name} + {tagCount > 0 && {tagCount}} +
+ ); + })}
)} diff --git a/src/components/tasks/TaskDetail.tsx b/src/components/tasks/TaskDetail.tsx index 7035dc7..b7b325e 100644 --- a/src/components/tasks/TaskDetail.tsx +++ b/src/components/tasks/TaskDetail.tsx @@ -160,8 +160,8 @@ export function TaskDetail({ const updatedTask: Task = { ...task, ...data, - children: subtasks, - tags, + children: (data.children as Task[]) || subtasks, + tags: (data.tags as { tag: Tag }[]) || tags, updatedAt: new Date().toISOString(), } as Task; if (onDemoUpdateTask) onDemoUpdateTask(updatedTask); @@ -251,7 +251,12 @@ export function TaskDetail({ name: trimmed, color: ["#4B7BF5", "#10B981", "#EF4444", "#F59E0B", "#8B5CF6"][Math.floor(Math.random() * 5)], }; - setAllAvailableTags((p) => [...p, newTag]); + const updatedAvailable = [...allAvailableTags, newTag]; + setAllAvailableTags(updatedAvailable); + if (typeof window !== "undefined") { + const { saveCustomTags } = require("@/lib/mockData"); + saveCustomTags(updatedAvailable); + } toggleTag(newTag); setNewTagName(""); }; diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts index b6683c1..9c5e8fc 100644 --- a/src/lib/mockData.ts +++ b/src/lib/mockData.ts @@ -280,16 +280,16 @@ 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 unmarkDeleted = (node: MockTask): MockTask => ({ + const markRestored = (node: MockTask): MockTask => ({ ...node, isDeleted: false, deletedAt: null, - children: node.children ? node.children.map(unmarkDeleted) : [], + children: node.children ? node.children.map(markRestored) : [], }); return tree.map((node) => { if (node.id === id) { - return unmarkDeleted(node); + return markRestored(node); } if (node.children && node.children.length > 0) { return { ...node, children: restoreTaskInTree(node.children, id) }; @@ -348,25 +348,24 @@ export function findTaskInTree(tree: MockTask[], id: string): MockTask | null { return null; } -// Get all active tasks or all trash tasks flattened (strictly deduplicated) +// Get all top-level trash tasks preserving intact nested sub-tasks hierarchy export function getAllTrashTasks(tree: MockTask[]): MockTask[] { - const map = new Map(); + const result: MockTask[] = []; function collect(nodes: MockTask[]) { for (const node of nodes) { if (node.isDeleted) { - if (!map.has(node.id)) { - map.set(node.id, { ...node, children: [] }); - } - } - if (node.children && node.children.length > 0) { + // Collect top-level deleted item with its full subtasks tree intact + result.push(node); + } else if (node.children && node.children.length > 0) { + // If parent is not deleted, check if any subtask was individually deleted collect(node.children); } } } collect(tree); - return Array.from(map.values()); + return result; } // Filter tasks by custom tag @@ -381,4 +380,9 @@ export function filterTasksByTag(tree: MockTask[], tagName: string): MockTask[] } } return matched; -} \ No newline at end of file +} + +// Get count of active tasks for a tag +export function getTagTaskCount(tree: MockTask[], tagName: string): number { + return filterTasksByTag(tree, tagName).filter((t) => !t.completed).length; +}