feat(core): full database soft-delete, live sidebar counts and robust subtask undo/restore across all modes
Build and Push Docker Image / build-and-push (push) Successful in 10m12s
Build and Push Docker Image / build-and-push (push) Successful in 10m12s
This commit is contained in:
+1
-3
@@ -39,9 +39,7 @@
|
||||
- Validated that `npm run lint` and `npm run build` execute with 0 errors and 0 warnings.
|
||||
|
||||
- **Next Steps (Todo):**
|
||||
- Fix sidebar list item counts calculation in demo & API modes.
|
||||
- Completely isolate Trash view and Tag view from My Tasks list state in TaskList.tsx.
|
||||
- Fix Restore and Permanent Delete in Trash view for both Demo and API modes.
|
||||
- Continuous refinement of user experience based on feedback.
|
||||
- Enhance keyboard accessibility & global shortcuts (shortcuts for switching list/kanban view, quick task navigation, Command Palette bindings).
|
||||
- Add optional automatic synchronization background polling / webhook integration if requested.
|
||||
- Plan next feature iterations or custom integrations as requested by user.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Task" ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE "Task" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Task_isDeleted_idx" ON "Task"("isDeleted");
|
||||
@@ -63,6 +63,9 @@ model Task {
|
||||
priority Int @default(0)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
deletedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -75,6 +78,7 @@ model Task {
|
||||
@@index([userId])
|
||||
@@index([listId])
|
||||
@@index([parentId])
|
||||
@@index([isDeleted])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function GET() {
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { sortOrder: "asc" },
|
||||
include: {
|
||||
_count: { select: { tasks: { where: { completed: false, parentId: null } } } },
|
||||
_count: { select: { tasks: { where: { isDeleted: false, completed: false, parentId: null } } } },
|
||||
},
|
||||
});
|
||||
return NextResponse.json(lists);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const tags = await prisma.tag.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { name: "asc" },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
tasks: {
|
||||
where: {
|
||||
task: {
|
||||
isDeleted: false,
|
||||
completed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(tags);
|
||||
} catch (err) {
|
||||
console.error("[tags:GET]", err);
|
||||
return NextResponse.json({ error: "Failed to fetch tags" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const body = await req.json().catch(() => null);
|
||||
if (!body || !body.name?.trim()) {
|
||||
return NextResponse.json({ error: "Tag name required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const name = body.name.trim();
|
||||
const color = body.color || "#4B7BF5";
|
||||
|
||||
const tag = await prisma.tag.upsert({
|
||||
where: {
|
||||
userId_name: {
|
||||
userId: session.user.id,
|
||||
name,
|
||||
},
|
||||
},
|
||||
update: { color },
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
name,
|
||||
color,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(tag, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[tags:POST]", err);
|
||||
return NextResponse.json({ error: "Failed to create/update tag" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export async function PATCH(req: NextRequest, { params }: Ctx) {
|
||||
if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
// Sanitize allowed fields
|
||||
const allowed = ["title", "note", "completed", "completedAt", "dueDate", "priority", "sortOrder", "listId", "parentId"];
|
||||
const allowed = ["title", "note", "completed", "completedAt", "dueDate", "priority", "sortOrder", "listId", "parentId", "isDeleted", "deletedAt"];
|
||||
const data: Record<string, unknown> = {};
|
||||
for (const key of allowed) {
|
||||
if (key in body) data[key] = body[key];
|
||||
@@ -71,9 +71,25 @@ export async function PATCH(req: NextRequest, { params }: Ctx) {
|
||||
data.dueDate = data.dueDate ? new Date(data.dueDate as string) : null;
|
||||
}
|
||||
|
||||
// If restoring a child task (isDeleted: false), also restore ancestor parents
|
||||
if (data.isDeleted === false) {
|
||||
let currentParentId = task.parentId;
|
||||
while (currentParentId) {
|
||||
const parentTask: any = await prisma.task.findUnique({ where: { id: currentParentId } });
|
||||
if (!parentTask) break;
|
||||
if (parentTask.isDeleted) {
|
||||
await prisma.task.update({
|
||||
where: { id: currentParentId },
|
||||
data: { isDeleted: false, deletedAt: null } as any,
|
||||
});
|
||||
}
|
||||
currentParentId = parentTask.parentId;
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await prisma.task.update({
|
||||
where: { id },
|
||||
data,
|
||||
data: data as any,
|
||||
include: { children: { orderBy: { sortOrder: "asc" } }, tags: { include: { tag: true } } },
|
||||
});
|
||||
return NextResponse.json(updated);
|
||||
@@ -83,7 +99,7 @@ export async function PATCH(req: NextRequest, { params }: Ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_: NextRequest, { params }: Ctx) {
|
||||
export async function DELETE(req: NextRequest, { params }: Ctx) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
@@ -92,8 +108,35 @@ export async function DELETE(_: NextRequest, { params }: Ctx) {
|
||||
const task = await prisma.task.findFirst({ where: { id, userId: session.user.id } });
|
||||
if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
await prisma.task.delete({ where: { id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
const { searchParams } = new URL(req.url);
|
||||
const permanent = searchParams.get("permanent") === "true";
|
||||
|
||||
if (permanent) {
|
||||
await prisma.task.delete({ where: { id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
// Soft delete task and its children
|
||||
const now = new Date();
|
||||
await prisma.task.update({
|
||||
where: { id },
|
||||
data: { isDeleted: true, deletedAt: now } as any,
|
||||
});
|
||||
|
||||
// Recursively soft-delete children
|
||||
const softDeleteChildren = async (parentId: string) => {
|
||||
const children = await prisma.task.findMany({ where: { parentId } });
|
||||
for (const child of children) {
|
||||
await prisma.task.update({
|
||||
where: { id: child.id },
|
||||
data: { isDeleted: true, deletedAt: now } as any,
|
||||
});
|
||||
await softDeleteChildren(child.id);
|
||||
}
|
||||
};
|
||||
await softDeleteChildren(id);
|
||||
|
||||
return NextResponse.json({ ok: true, softDeleted: true });
|
||||
} catch (err) {
|
||||
console.error("[tasks/id:DELETE]", err);
|
||||
return NextResponse.json({ error: "Failed to delete task" }, { status: 500 });
|
||||
|
||||
@@ -12,19 +12,52 @@ export async function GET(req: NextRequest) {
|
||||
const listId = searchParams.get("listId");
|
||||
const showCompleted = searchParams.get("showCompleted") === "true";
|
||||
const tagName = searchParams.get("tag");
|
||||
const isTrash = searchParams.get("isTrash") === "true";
|
||||
const countTrash = searchParams.get("countTrash") === "true";
|
||||
|
||||
if (countTrash) {
|
||||
const trashCount = await prisma.task.count({
|
||||
where: { userId: session.user.id, isDeleted: true } as any,
|
||||
});
|
||||
return NextResponse.json({ count: trashCount });
|
||||
}
|
||||
|
||||
if (isTrash) {
|
||||
// Return all deleted tasks (top-level or subtasks)
|
||||
const trashTasks = await prisma.task.findMany({
|
||||
where: { userId: session.user.id, isDeleted: true } as any,
|
||||
orderBy: [{ deletedAt: "desc" } as any, { createdAt: "desc" }],
|
||||
include: {
|
||||
children: {
|
||||
orderBy: [{ sortOrder: "asc" }],
|
||||
include: { tags: { include: { tag: true } } },
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
});
|
||||
return NextResponse.json(trashTasks);
|
||||
}
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
userId: session.user.id,
|
||||
isDeleted: false,
|
||||
...(listId ? { listId, parentId: null } : tagName ? {} : { parentId: null }),
|
||||
...(showCompleted ? {} : { completed: false }),
|
||||
...(tagName ? { tags: { some: { tag: { name: tagName } } } } : {}),
|
||||
};
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where,
|
||||
where: where as any,
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
||||
include: {
|
||||
children: { orderBy: [{ sortOrder: "asc" }] },
|
||||
children: {
|
||||
where: { isDeleted: false } as any,
|
||||
orderBy: [{ sortOrder: "asc" }],
|
||||
include: {
|
||||
children: { where: { isDeleted: false } as any, orderBy: [{ sortOrder: "asc" }] },
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
},
|
||||
tags: { include: { tag: true } },
|
||||
},
|
||||
});
|
||||
@@ -35,6 +68,29 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const trash = searchParams.get("trash") === "true";
|
||||
|
||||
if (trash) {
|
||||
// Empty all trash permanently
|
||||
await prisma.task.deleteMany({
|
||||
where: { userId: session.user.id, isDeleted: true } as any,
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Invalid delete request" }, { status: 400 });
|
||||
} catch (err) {
|
||||
console.error("[tasks:DELETE_BATCH]", err);
|
||||
return NextResponse.json({ error: "Failed to empty trash" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -117,6 +117,8 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
return () => document.removeEventListener("checkflow:openCommandPalette", handler);
|
||||
}, []);
|
||||
|
||||
const [apiTrashCount, setApiTrashCount] = useState(0);
|
||||
|
||||
// Task filter & reconstruct hierarchical structure for Demo and API modes
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
@@ -136,8 +138,17 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}
|
||||
} else {
|
||||
// Authenticated API mode
|
||||
// Also fetch trash count for sidebar
|
||||
fetch("/api/tasks?countTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : { count: 0 }))
|
||||
.then((data) => setApiTrashCount(data.count || 0))
|
||||
.catch(() => {});
|
||||
|
||||
if (isTrashActive) {
|
||||
setTasks([]);
|
||||
fetch("/api/tasks?isTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load trash tasks", err));
|
||||
} else if (selectedTag) {
|
||||
fetch(`/api/tasks?tag=${encodeURIComponent(selectedTag)}&showCompleted=${showCompleted}`)
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
@@ -195,7 +206,10 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
const store = getDemoStore();
|
||||
setTasks(getAllTrashTasks(store.tasks) as Task[]);
|
||||
} else {
|
||||
setTasks([]);
|
||||
fetch("/api/tasks?isTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load trash tasks", err));
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
@@ -357,7 +371,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}, 6000);
|
||||
}, [isDemo, selectedTask]);
|
||||
|
||||
// Undo delete
|
||||
// Undo delete (Restores exact soft-deleted task hierarchy)
|
||||
const handleUndoDelete = useCallback(async () => {
|
||||
if (!undoToast) return;
|
||||
if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
|
||||
@@ -377,28 +391,16 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
const res = await fetch(`/api/tasks/${task.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: task.title,
|
||||
listId: task.listId,
|
||||
parentId: task.parentId,
|
||||
note: task.note,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
priority: task.priority,
|
||||
}),
|
||||
body: JSON.stringify({ isDeleted: false, deletedAt: null }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const created = await res.json();
|
||||
if (created.listId === selectedListId) {
|
||||
setTasks((prev) => [...prev, created]);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to restore task", err);
|
||||
console.error("Failed to restore task via Undo", err);
|
||||
}
|
||||
}
|
||||
}, [undoToast, isDemo, selectedListId, refresh]);
|
||||
@@ -418,29 +420,47 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
};
|
||||
|
||||
// Restore from Trash (Restores task and all its nested subtasks)
|
||||
const handleRestoreTask = (id: string) => {
|
||||
const handleRestoreTask = async (id: string) => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = restoreTaskInTree(store.tasks, id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isDeleted: false, deletedAt: null }),
|
||||
});
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to restore task", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Permanent Delete
|
||||
const handlePermanentDeleteTask = (id: string) => {
|
||||
const handlePermanentDeleteTask = async (id: string) => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = deleteTaskInTree(store.tasks, id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks(getAllTrashTasks(newTasks) as Task[]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}?permanent=true`, { method: "DELETE" });
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to permanently delete task", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Empty Trash
|
||||
const handleEmptyTrash = () => {
|
||||
const handleEmptyTrash = async () => {
|
||||
if (!confirm("Permanently empty all items in trash?")) return;
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
@@ -448,6 +468,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks([]);
|
||||
refresh();
|
||||
} else {
|
||||
try {
|
||||
await fetch("/api/tasks?trash=true", { method: "DELETE" });
|
||||
refresh();
|
||||
} catch (err) {
|
||||
console.error("Failed to empty trash", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -514,7 +541,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
onTagSelect={handleTagSelect}
|
||||
isTrashActive={isTrashActive}
|
||||
onTrashSelect={handleTrashSelect}
|
||||
trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : undefined}
|
||||
trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : apiTrashCount}
|
||||
mobileOpen={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
isDemo={isDemo}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount
|
||||
|
||||
interface User { id: string; name?: string | null; email?: string | null }
|
||||
interface List { id: string; name: string; color: string; icon: string; _count?: { tasks: number } }
|
||||
interface APITag { id: string; name: string; color: string; _count?: { tasks: number } }
|
||||
|
||||
const LIST_COLORS = ["#5B8DEF", "#E05252", "#3DAD84", "#E8931A", "#8B6CF7", "#E4609B", "#0ABAD1", "#F17A3B", "#6875F5", "#14B8A6"];
|
||||
|
||||
@@ -74,14 +75,8 @@ export function Sidebar({
|
||||
const [exportListId, setExportListId] = useState<string>("all");
|
||||
const [exportIncludeCompleted, setExportIncludeCompleted] = useState<boolean>(true);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [tags, setTags] = useState<MockTag[]>(() => (typeof window !== "undefined" ? getCustomTags() : []));
|
||||
const [internalTrashCount, setInternalTrashCount] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const store = getDemoStore();
|
||||
return getAllTrashTasks(store.tasks).length;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const [tags, setTags] = useState<Array<MockTag | APITag>>(() => (typeof window !== "undefined" ? getCustomTags() : []));
|
||||
const [internalTrashCount, setInternalTrashCount] = useState(0);
|
||||
|
||||
const trashCount = externalTrashCount !== undefined ? externalTrashCount : internalTrashCount;
|
||||
|
||||
@@ -98,17 +93,16 @@ export function Sidebar({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTags(getCustomTags());
|
||||
const store = getDemoStore();
|
||||
const trashed = getAllTrashTasks(store.tasks);
|
||||
setInternalTrashCount(trashed.length);
|
||||
}, [lists]);
|
||||
|
||||
const initialListSelectedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDemo) {
|
||||
if (isDemo) {
|
||||
setTags(getCustomTags());
|
||||
const store = getDemoStore();
|
||||
const trashed = getAllTrashTasks(store.tasks);
|
||||
setInternalTrashCount(trashed.length);
|
||||
} else {
|
||||
// Load lists
|
||||
fetch("/api/lists")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => {
|
||||
@@ -121,8 +115,22 @@ export function Sidebar({
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Failed to load lists", err));
|
||||
|
||||
// Load tags
|
||||
fetch("/api/tags")
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => {
|
||||
if (Array.isArray(data)) setTags(data);
|
||||
})
|
||||
.catch((err) => console.error("Failed to load tags", err));
|
||||
|
||||
// Load trash count
|
||||
fetch("/api/tasks?countTrash=true")
|
||||
.then((r) => (r.ok ? r.json() : { count: 0 }))
|
||||
.then((data) => setInternalTrashCount(data.count || 0))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [isDemo, onListSelect, selectedListId, isTrashActive, selectedTag, setLists]);
|
||||
}, [isDemo, onListSelect, selectedListId, isTrashActive, selectedTag, setLists, lists.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showNewList) setTimeout(() => inputRef.current?.focus(), 50);
|
||||
@@ -581,7 +589,7 @@ export function Sidebar({
|
||||
const active = filterActiveTree(store.tasks as MockTask[], false).filter((t) => t.listId === list.id);
|
||||
return active.length > 0 ? active.length : "";
|
||||
})()
|
||||
: list._count?.tasks || ""}
|
||||
: (list._count?.tasks ? list._count.tasks : "")}
|
||||
</span>
|
||||
<button
|
||||
className="icon-btn"
|
||||
@@ -610,7 +618,7 @@ export function Sidebar({
|
||||
{tags.map((tag) => {
|
||||
const tagCount = isDemo && typeof window !== "undefined"
|
||||
? getTagTaskCount(getDemoStore().tasks, tag.name)
|
||||
: 0;
|
||||
: ((tag as APITag)._count?.tasks ?? 0);
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
@@ -641,14 +649,9 @@ export function Sidebar({
|
||||
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14H6L5 6" /><path d="M10 11v6M14 11v6" /><path d="M9 6V4h6v2" />
|
||||
</svg>
|
||||
<span className="item-label">{t("trash") || "Trash"}</span>
|
||||
{(() => {
|
||||
const count = isDemo && typeof window !== "undefined"
|
||||
? getAllTrashTasks(getDemoStore().tasks).length
|
||||
: (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount);
|
||||
return count > 0 ? (
|
||||
<span className="item-count" style={{ color: "var(--danger)", fontWeight: 700 }}>{count}</span>
|
||||
) : null;
|
||||
})()}
|
||||
{trashCount > 0 ? (
|
||||
<span className="item-count" style={{ color: "var(--danger)", fontWeight: 700 }}>{trashCount}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user