fix(trash & tags): fix subtasks hierarchy restoration in trash and real-time tag counts sync
Build and Push Docker Image / build-and-push (push) Successful in 9m16s
Build and Push Docker Image / build-and-push (push) Successful in 9m16s
This commit is contained in:
@@ -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) => {
|
const handleRestoreTask = (id: string) => {
|
||||||
if (isDemo) {
|
if (isDemo) {
|
||||||
const store = getDemoStore();
|
const store = getDemoStore();
|
||||||
const newTasks = restoreTaskInTree(store.tasks, id);
|
const newTasks = restoreTaskInTree(store.tasks, id);
|
||||||
saveDemoStore(store.lists, newTasks);
|
saveDemoStore(store.lists, newTasks);
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== id));
|
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -427,7 +427,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
const store = getDemoStore();
|
const store = getDemoStore();
|
||||||
const newTasks = deleteTaskInTree(store.tasks, id);
|
const newTasks = deleteTaskInTree(store.tasks, id);
|
||||||
saveDemoStore(store.lists, newTasks);
|
saveDemoStore(store.lists, newTasks);
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== id));
|
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 } from "@/lib/mockData";
|
import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount } 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 } }
|
||||||
@@ -607,16 +607,22 @@ export function Sidebar({
|
|||||||
{tags.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<div className="sidebar-section">
|
<div className="sidebar-section">
|
||||||
<div className="sidebar-section-label">🏷️ {t("tags") || "Tags"}</div>
|
<div className="sidebar-section-label">🏷️ {t("tags") || "Tags"}</div>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => {
|
||||||
<div
|
const tagCount = isDemo && typeof window !== "undefined"
|
||||||
key={tag.id}
|
? getTagTaskCount(getDemoStore().tasks, tag.name)
|
||||||
className={`sidebar-item${selectedTag === tag.name ? " active" : ""}`}
|
: 0;
|
||||||
onClick={() => onTagSelect(selectedTag === tag.name ? null : tag.name)}
|
return (
|
||||||
>
|
<div
|
||||||
<span style={{ color: tag.color, fontSize: 13, fontWeight: 700 }}>#</span>
|
key={tag.id}
|
||||||
<span className="item-label">{tag.name}</span>
|
className={`sidebar-item${selectedTag === tag.name ? " active" : ""}`}
|
||||||
</div>
|
onClick={() => onTagSelect(selectedTag === tag.name ? null : tag.name)}
|
||||||
))}
|
>
|
||||||
|
<span style={{ color: tag.color, fontSize: 13, fontWeight: 700 }}>#</span>
|
||||||
|
<span className="item-label">{tag.name}</span>
|
||||||
|
{tagCount > 0 && <span className="item-count">{tagCount}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ export function TaskDetail({
|
|||||||
const updatedTask: Task = {
|
const updatedTask: Task = {
|
||||||
...task,
|
...task,
|
||||||
...data,
|
...data,
|
||||||
children: subtasks,
|
children: (data.children as Task[]) || subtasks,
|
||||||
tags,
|
tags: (data.tags as { tag: Tag }[]) || tags,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
} as Task;
|
} as Task;
|
||||||
if (onDemoUpdateTask) onDemoUpdateTask(updatedTask);
|
if (onDemoUpdateTask) onDemoUpdateTask(updatedTask);
|
||||||
@@ -251,7 +251,12 @@ export function TaskDetail({
|
|||||||
name: trimmed,
|
name: trimmed,
|
||||||
color: ["#4B7BF5", "#10B981", "#EF4444", "#F59E0B", "#8B5CF6"][Math.floor(Math.random() * 5)],
|
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);
|
toggleTag(newTag);
|
||||||
setNewTagName("");
|
setNewTagName("");
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-12
@@ -280,16 +280,16 @@ export function moveToTrashInTree(tree: MockTask[], id: string): MockTask[] {
|
|||||||
|
|
||||||
// Restore from Trash (recursively unmark node and its children)
|
// Restore from Trash (recursively unmark node and its children)
|
||||||
export function restoreTaskInTree(tree: MockTask[], id: string): MockTask[] {
|
export function restoreTaskInTree(tree: MockTask[], id: string): MockTask[] {
|
||||||
const unmarkDeleted = (node: MockTask): MockTask => ({
|
const markRestored = (node: MockTask): MockTask => ({
|
||||||
...node,
|
...node,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
children: node.children ? node.children.map(unmarkDeleted) : [],
|
children: node.children ? node.children.map(markRestored) : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return tree.map((node) => {
|
return tree.map((node) => {
|
||||||
if (node.id === id) {
|
if (node.id === id) {
|
||||||
return unmarkDeleted(node);
|
return markRestored(node);
|
||||||
}
|
}
|
||||||
if (node.children && node.children.length > 0) {
|
if (node.children && node.children.length > 0) {
|
||||||
return { ...node, children: restoreTaskInTree(node.children, id) };
|
return { ...node, children: restoreTaskInTree(node.children, id) };
|
||||||
@@ -348,25 +348,24 @@ export function findTaskInTree(tree: MockTask[], id: string): MockTask | null {
|
|||||||
return 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[] {
|
export function getAllTrashTasks(tree: MockTask[]): MockTask[] {
|
||||||
const map = new Map<string, MockTask>();
|
const result: MockTask[] = [];
|
||||||
|
|
||||||
function collect(nodes: MockTask[]) {
|
function collect(nodes: MockTask[]) {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.isDeleted) {
|
if (node.isDeleted) {
|
||||||
if (!map.has(node.id)) {
|
// Collect top-level deleted item with its full subtasks tree intact
|
||||||
map.set(node.id, { ...node, children: [] });
|
result.push(node);
|
||||||
}
|
} else if (node.children && node.children.length > 0) {
|
||||||
}
|
// If parent is not deleted, check if any subtask was individually deleted
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
collect(node.children);
|
collect(node.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
collect(tree);
|
collect(tree);
|
||||||
return Array.from(map.values());
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter tasks by custom tag
|
// Filter tasks by custom tag
|
||||||
@@ -381,4 +380,9 @@ export function filterTasksByTag(tree: MockTask[], tagName: string): MockTask[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user