fix(trash): implement robust ancestor-preserving restore and filterActiveTree for trash reliability
Build and Push Docker Image / build-and-push (push) Successful in 9m29s
Build and Push Docker Image / build-and-push (push) Successful in 9m29s
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
findTaskInTree,
|
findTaskInTree,
|
||||||
getAllTrashTasks,
|
getAllTrashTasks,
|
||||||
filterTasksByTag,
|
filterTasksByTag,
|
||||||
|
filterActiveTree,
|
||||||
} from "@/lib/mockData";
|
} from "@/lib/mockData";
|
||||||
|
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
@@ -126,10 +127,10 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
} else if (selectedTag) {
|
} else if (selectedTag) {
|
||||||
setTasks(filterTasksByTag(store.tasks, selectedTag) as Task[]);
|
setTasks(filterTasksByTag(store.tasks, selectedTag) as Task[]);
|
||||||
} else if (selectedListId) {
|
} else if (selectedListId) {
|
||||||
const listTasks = (store.tasks as Task[]).filter(
|
const listTasks = filterActiveTree(store.tasks as MockTask[], showCompleted).filter(
|
||||||
(t) => !t.isDeleted && t.listId === selectedListId && (showCompleted ? true : !t.completed)
|
(t) => t.listId === selectedListId
|
||||||
);
|
);
|
||||||
setTasks(listTasks);
|
setTasks(listTasks as Task[]);
|
||||||
} else {
|
} else {
|
||||||
setTasks([]);
|
setTasks([]);
|
||||||
}
|
}
|
||||||
@@ -337,7 +338,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
const store = getDemoStore();
|
const store = getDemoStore();
|
||||||
const newTasks = moveToTrashInTree(store.tasks, task.id);
|
const newTasks = moveToTrashInTree(store.tasks, task.id);
|
||||||
saveDemoStore(store.lists, newTasks);
|
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);
|
if (selectedTask?.id === task.id) setSelectedTask(null);
|
||||||
} else {
|
} else {
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== task.id));
|
setTasks((prev) => prev.filter((t) => t.id !== task.id));
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTheme } from "@/app/providers";
|
|||||||
import { LanguageSelector } from "@/components/ui/LanguageSelector";
|
import { LanguageSelector } from "@/components/ui/LanguageSelector";
|
||||||
import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu";
|
import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu";
|
||||||
import { SettingsModal } from "@/components/settings/SettingsModal";
|
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 User { id: string; name?: string | null; email?: string | null }
|
||||||
interface List { id: string; name: string; color: string; icon: string; _count?: { tasks: number } }
|
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 "";
|
if (typeof window === "undefined") return "";
|
||||||
const store = getDemoStore();
|
const store = getDemoStore();
|
||||||
const cnt = store.tasks.filter((t) => t.listId === list.id && !t.isDeleted && !t.completed).length;
|
const active = filterActiveTree(store.tasks as MockTask[], false).filter((t) => t.listId === list.id);
|
||||||
return cnt > 0 ? cnt : "";
|
return active.length > 0 ? active.length : "";
|
||||||
})()
|
})()
|
||||||
: list._count?.tasks || ""}
|
: list._count?.tasks || ""}
|
||||||
</span>
|
</span>
|
||||||
@@ -646,7 +646,7 @@ export function Sidebar({
|
|||||||
? getAllTrashTasks(getDemoStore().tasks).length
|
? getAllTrashTasks(getDemoStore().tasks).length
|
||||||
: (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount);
|
: (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount);
|
||||||
return count > 0 ? (
|
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;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+44
-12
@@ -278,24 +278,56 @@ export function moveToTrashInTree(tree: MockTask[], id: string): MockTask[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore from Trash (recursively unmark node and its children)
|
// Restore task (and all its subtasks) from Trash, ensuring ancestor parents are also un-deleted
|
||||||
export function restoreTaskInTree(tree: MockTask[], id: string): MockTask[] {
|
export function restoreTaskInTree(tree: MockTask[], targetId: string): MockTask[] {
|
||||||
const markRestored = (node: MockTask): MockTask => ({
|
const unmarkNodeAndChildren = (node: MockTask): MockTask => ({
|
||||||
...node,
|
...node,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
children: node.children ? node.children.map(markRestored) : [],
|
children: node.children ? node.children.map(unmarkNodeAndChildren) : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return tree.map((node) => {
|
function processNodes(nodes: MockTask[]): { updated: MockTask[]; found: boolean } {
|
||||||
if (node.id === id) {
|
let foundInThisLevel = false;
|
||||||
return markRestored(node);
|
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 result;
|
||||||
}
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permanent delete
|
// Permanent delete
|
||||||
|
|||||||
Reference in New Issue
Block a user