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

This commit is contained in:
2026-08-22 00:26:45 +09:00
parent 72fefe89b0
commit 06d5344aef
9 changed files with 269 additions and 62 deletions
+1 -3
View File
@@ -39,9 +39,7 @@
- Validated that `npm run lint` and `npm run build` execute with 0 errors and 0 warnings. - Validated that `npm run lint` and `npm run build` execute with 0 errors and 0 warnings.
- **Next Steps (Todo):** - **Next Steps (Todo):**
- Fix sidebar list item counts calculation in demo & API modes. - Continuous refinement of user experience based on feedback.
- 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.
- Enhance keyboard accessibility & global shortcuts (shortcuts for switching list/kanban view, quick task navigation, Command Palette bindings). - 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. - Add optional automatic synchronization background polling / webhook integration if requested.
- Plan next feature iterations or custom integrations as requested by user. - 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");
+4
View File
@@ -63,6 +63,9 @@ model Task {
priority Int @default(0) priority Int @default(0)
sortOrder Int @default(0) sortOrder Int @default(0)
isDeleted Boolean @default(false)
deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -75,6 +78,7 @@ model Task {
@@index([userId]) @@index([userId])
@@index([listId]) @@index([listId])
@@index([parentId]) @@index([parentId])
@@index([isDeleted])
} }
model Tag { model Tag {
+1 -1
View File
@@ -12,7 +12,7 @@ export async function GET() {
where: { userId: session.user.id }, where: { userId: session.user.id },
orderBy: { sortOrder: "asc" }, orderBy: { sortOrder: "asc" },
include: { include: {
_count: { select: { tasks: { where: { completed: false, parentId: null } } } }, _count: { select: { tasks: { where: { isDeleted: false, completed: false, parentId: null } } } },
}, },
}); });
return NextResponse.json(lists); return NextResponse.json(lists);
+70
View File
@@ -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 });
}
}
+46 -3
View File
@@ -36,7 +36,7 @@ export async function PATCH(req: NextRequest, { params }: Ctx) {
if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 }); if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
// Sanitize allowed fields // 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> = {}; const data: Record<string, unknown> = {};
for (const key of allowed) { for (const key of allowed) {
if (key in body) data[key] = body[key]; 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; 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({ const updated = await prisma.task.update({
where: { id }, where: { id },
data, data: data as any,
include: { children: { orderBy: { sortOrder: "asc" } }, tags: { include: { tag: true } } }, include: { children: { orderBy: { sortOrder: "asc" } }, tags: { include: { tag: true } } },
}); });
return NextResponse.json(updated); 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 { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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 } }); const task = await prisma.task.findFirst({ where: { id, userId: session.user.id } });
if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 }); if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
const { searchParams } = new URL(req.url);
const permanent = searchParams.get("permanent") === "true";
if (permanent) {
await prisma.task.delete({ where: { id } }); await prisma.task.delete({ where: { id } });
return NextResponse.json({ ok: true }); 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) { } catch (err) {
console.error("[tasks/id:DELETE]", err); console.error("[tasks/id:DELETE]", err);
return NextResponse.json({ error: "Failed to delete task" }, { status: 500 }); return NextResponse.json({ error: "Failed to delete task" }, { status: 500 });
+58 -2
View File
@@ -12,19 +12,52 @@ export async function GET(req: NextRequest) {
const listId = searchParams.get("listId"); const listId = searchParams.get("listId");
const showCompleted = searchParams.get("showCompleted") === "true"; const showCompleted = searchParams.get("showCompleted") === "true";
const tagName = searchParams.get("tag"); 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> = { const where: Record<string, unknown> = {
userId: session.user.id, userId: session.user.id,
isDeleted: false,
...(listId ? { listId, parentId: null } : tagName ? {} : { parentId: null }), ...(listId ? { listId, parentId: null } : tagName ? {} : { parentId: null }),
...(showCompleted ? {} : { completed: false }), ...(showCompleted ? {} : { completed: false }),
...(tagName ? { tags: { some: { tag: { name: tagName } } } } : {}), ...(tagName ? { tags: { some: { tag: { name: tagName } } } } : {}),
}; };
const tasks = await prisma.task.findMany({ const tasks = await prisma.task.findMany({
where, where: where as any,
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
include: { 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 } }, 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) { export async function POST(req: NextRequest) {
try { try {
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
+50 -23
View File
@@ -117,6 +117,8 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
return () => document.removeEventListener("checkflow:openCommandPalette", handler); return () => document.removeEventListener("checkflow:openCommandPalette", handler);
}, []); }, []);
const [apiTrashCount, setApiTrashCount] = useState(0);
// Task filter & reconstruct hierarchical structure for Demo and API modes // Task filter & reconstruct hierarchical structure for Demo and API modes
useEffect(() => { useEffect(() => {
if (isDemo) { if (isDemo) {
@@ -136,8 +138,17 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
} }
} else { } else {
// Authenticated API mode // 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) { 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) { } else if (selectedTag) {
fetch(`/api/tasks?tag=${encodeURIComponent(selectedTag)}&showCompleted=${showCompleted}`) fetch(`/api/tasks?tag=${encodeURIComponent(selectedTag)}&showCompleted=${showCompleted}`)
.then((r) => (r.ok ? r.json() : [])) .then((r) => (r.ok ? r.json() : []))
@@ -195,7 +206,10 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
const store = getDemoStore(); const store = getDemoStore();
setTasks(getAllTrashTasks(store.tasks) as Task[]); setTasks(getAllTrashTasks(store.tasks) as Task[]);
} else { } 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]); }, [isDemo]);
@@ -357,7 +371,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
}, 6000); }, 6000);
}, [isDemo, selectedTask]); }, [isDemo, selectedTask]);
// Undo delete // Undo delete (Restores exact soft-deleted task hierarchy)
const handleUndoDelete = useCallback(async () => { const handleUndoDelete = useCallback(async () => {
if (!undoToast) return; if (!undoToast) return;
if (undoTimerRef.current) clearTimeout(undoTimerRef.current); if (undoTimerRef.current) clearTimeout(undoTimerRef.current);
@@ -377,28 +391,16 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
refresh(); refresh();
} else { } else {
try { try {
const res = await fetch("/api/tasks", { const res = await fetch(`/api/tasks/${task.id}`, {
method: "POST", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({ isDeleted: false, deletedAt: null }),
title: task.title,
listId: task.listId,
parentId: task.parentId,
note: task.note,
completed: task.completed,
dueDate: task.dueDate,
priority: task.priority,
}),
}); });
if (res.ok) { if (res.ok) {
const created = await res.json();
if (created.listId === selectedListId) {
setTasks((prev) => [...prev, created]);
}
refresh(); refresh();
} }
} catch (err) { } catch (err) {
console.error("Failed to restore task", err); console.error("Failed to restore task via Undo", err);
} }
} }
}, [undoToast, isDemo, selectedListId, refresh]); }, [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) // Restore from Trash (Restores task and all its nested subtasks)
const handleRestoreTask = (id: string) => { const handleRestoreTask = async (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(getAllTrashTasks(newTasks) as Task[]); setTasks(getAllTrashTasks(newTasks) as Task[]);
refresh(); 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 // Permanent Delete
const handlePermanentDeleteTask = (id: string) => { const handlePermanentDeleteTask = async (id: string) => {
if (isDemo) { if (isDemo) {
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(getAllTrashTasks(newTasks) as Task[]); setTasks(getAllTrashTasks(newTasks) as Task[]);
refresh(); 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 // Empty Trash
const handleEmptyTrash = () => { const handleEmptyTrash = async () => {
if (!confirm("Permanently empty all items in trash?")) return; if (!confirm("Permanently empty all items in trash?")) return;
if (isDemo) { if (isDemo) {
const store = getDemoStore(); const store = getDemoStore();
@@ -448,6 +468,13 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
saveDemoStore(store.lists, newTasks); saveDemoStore(store.lists, newTasks);
setTasks([]); setTasks([]);
refresh(); 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} onTagSelect={handleTagSelect}
isTrashActive={isTrashActive} isTrashActive={isTrashActive}
onTrashSelect={handleTrashSelect} onTrashSelect={handleTrashSelect}
trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : undefined} trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : apiTrashCount}
mobileOpen={sidebarOpen} mobileOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)} onClose={() => setSidebarOpen(false)}
isDemo={isDemo} isDemo={isDemo}
+28 -25
View File
@@ -11,6 +11,7 @@ import { getCustomTags, MockTag, getAllTrashTasks, getDemoStore, getTagTaskCount
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 } }
interface APITag { id: string; name: string; color: string; _count?: { tasks: number } }
const LIST_COLORS = ["#5B8DEF", "#E05252", "#3DAD84", "#E8931A", "#8B6CF7", "#E4609B", "#0ABAD1", "#F17A3B", "#6875F5", "#14B8A6"]; 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 [exportListId, setExportListId] = useState<string>("all");
const [exportIncludeCompleted, setExportIncludeCompleted] = useState<boolean>(true); const [exportIncludeCompleted, setExportIncludeCompleted] = useState<boolean>(true);
const [exporting, setExporting] = useState(false); const [exporting, setExporting] = useState(false);
const [tags, setTags] = useState<MockTag[]>(() => (typeof window !== "undefined" ? getCustomTags() : [])); const [tags, setTags] = useState<Array<MockTag | APITag>>(() => (typeof window !== "undefined" ? getCustomTags() : []));
const [internalTrashCount, setInternalTrashCount] = useState(() => { const [internalTrashCount, setInternalTrashCount] = useState(0);
if (typeof window !== "undefined") {
const store = getDemoStore();
return getAllTrashTasks(store.tasks).length;
}
return 0;
});
const trashCount = externalTrashCount !== undefined ? externalTrashCount : internalTrashCount; const trashCount = externalTrashCount !== undefined ? externalTrashCount : internalTrashCount;
@@ -98,17 +93,16 @@ export function Sidebar({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
const initialListSelectedRef = useRef(false);
useEffect(() => { useEffect(() => {
if (isDemo) {
setTags(getCustomTags()); setTags(getCustomTags());
const store = getDemoStore(); const store = getDemoStore();
const trashed = getAllTrashTasks(store.tasks); const trashed = getAllTrashTasks(store.tasks);
setInternalTrashCount(trashed.length); setInternalTrashCount(trashed.length);
}, [lists]); } else {
// Load lists
const initialListSelectedRef = useRef(false);
useEffect(() => {
if (!isDemo) {
fetch("/api/lists") fetch("/api/lists")
.then((r) => (r.ok ? r.json() : [])) .then((r) => (r.ok ? r.json() : []))
.then((data) => { .then((data) => {
@@ -121,8 +115,22 @@ export function Sidebar({
} }
}) })
.catch((err) => console.error("Failed to load lists", err)); .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(() => { useEffect(() => {
if (showNewList) setTimeout(() => inputRef.current?.focus(), 50); 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); const active = filterActiveTree(store.tasks as MockTask[], false).filter((t) => t.listId === list.id);
return active.length > 0 ? active.length : ""; return active.length > 0 ? active.length : "";
})() })()
: list._count?.tasks || ""} : (list._count?.tasks ? list._count.tasks : "")}
</span> </span>
<button <button
className="icon-btn" className="icon-btn"
@@ -610,7 +618,7 @@ export function Sidebar({
{tags.map((tag) => { {tags.map((tag) => {
const tagCount = isDemo && typeof window !== "undefined" const tagCount = isDemo && typeof window !== "undefined"
? getTagTaskCount(getDemoStore().tasks, tag.name) ? getTagTaskCount(getDemoStore().tasks, tag.name)
: 0; : ((tag as APITag)._count?.tasks ?? 0);
return ( return (
<div <div
key={tag.id} 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" /> <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> </svg>
<span className="item-label">{t("trash") || "Trash"}</span> <span className="item-label">{t("trash") || "Trash"}</span>
{(() => { {trashCount > 0 ? (
const count = isDemo && typeof window !== "undefined" <span className="item-count" style={{ color: "var(--danger)", fontWeight: 700 }}>{trashCount}</span>
? getAllTrashTasks(getDemoStore().tasks).length ) : null}
: (externalTrashCount !== undefined ? externalTrashCount : internalTrashCount);
return count > 0 ? (
<span className="item-count" style={{ color: "var(--danger)", fontWeight: 700 }}>{count}</span>
) : null;
})()}
</div> </div>
</div> </div>