fix(trash): implement robust ancestor-preserving restore and filterActiveTree for trash reliability
Build and Push Docker Image / build-and-push (push) Successful in 9m29s

This commit is contained in:
2026-08-21 23:34:23 +09:00
parent fc70b1e8cb
commit 72fefe89b0
3 changed files with 59 additions and 20 deletions
+11 -4
View File
@@ -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));
+4 -4
View File
@@ -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 || ""}
</span>
@@ -646,7 +646,7 @@ export function Sidebar({
? getAllTrashTasks(getDemoStore().tasks).length
: (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount);
return count > 0 ? (
<span className="item-count" style={{ color: "var(--danger)", fontWeight: 600 }}>{count}</span>
<span className="item-count" style={{ color: "var(--danger)", fontWeight: 700 }}>{count}</span>
) : null;
})()}
</div>
+44 -12
View File
@@ -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