fix(trash & sidebar): isolate trash view, sync list task counts in real-time, and fix restore/delete actions
Build and Push Docker Image / build-and-push (push) Successful in 10m24s
Build and Push Docker Image / build-and-push (push) Successful in 10m24s
This commit is contained in:
@@ -39,6 +39,9 @@
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -11,12 +11,13 @@ export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const listId = searchParams.get("listId");
|
||||
const showCompleted = searchParams.get("showCompleted") === "true";
|
||||
const tagName = searchParams.get("tag");
|
||||
|
||||
const where: Record<string, unknown> = {
|
||||
userId: session.user.id,
|
||||
parentId: null,
|
||||
...(listId ? { listId } : {}),
|
||||
...(listId ? { listId, parentId: null } : tagName ? {} : { parentId: null }),
|
||||
...(showCompleted ? {} : { completed: false }),
|
||||
...(tagName ? { tags: { some: { tag: { name: tagName } } } } : {}),
|
||||
};
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
|
||||
@@ -116,7 +116,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
return () => document.removeEventListener("checkflow:openCommandPalette", handler);
|
||||
}, []);
|
||||
|
||||
// Demo tasks filter & reconstruct hierarchical structure
|
||||
// Task filter & reconstruct hierarchical structure for Demo and API modes
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
@@ -130,6 +130,23 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
(t) => !t.isDeleted && t.listId === selectedListId && (showCompleted ? true : !t.completed)
|
||||
);
|
||||
setTasks(listTasks);
|
||||
} else {
|
||||
setTasks([]);
|
||||
}
|
||||
} else {
|
||||
// Authenticated API mode
|
||||
if (isTrashActive) {
|
||||
setTasks([]);
|
||||
} else if (selectedTag) {
|
||||
fetch(`/api/tasks?tag=${encodeURIComponent(selectedTag)}&showCompleted=${showCompleted}`)
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load tag tasks", err));
|
||||
} else if (selectedListId) {
|
||||
fetch(`/api/tasks?listId=${selectedListId}&showCompleted=${showCompleted}`)
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((data) => setTasks(Array.isArray(data) ? data : []))
|
||||
.catch((err) => console.error("Failed to load list tasks", err));
|
||||
}
|
||||
}
|
||||
}, [isDemo, selectedListId, isTrashActive, selectedTag, showCompleted, refreshKey]);
|
||||
@@ -161,18 +178,25 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
|
||||
const handleTagSelect = useCallback((tagName: string | null) => {
|
||||
setIsTrashActive(false);
|
||||
setSelectedListId(null);
|
||||
setSelectedTag(tagName);
|
||||
setSelectedTask(null);
|
||||
setSidebarOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleTrashSelect = useCallback(() => {
|
||||
setIsTrashActive(true);
|
||||
setSelectedTag(null);
|
||||
setSelectedListId(null);
|
||||
setSelectedTag(null);
|
||||
setIsTrashActive(true);
|
||||
setSelectedTask(null);
|
||||
setSidebarOpen(false);
|
||||
}, []);
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
setTasks(getAllTrashTasks(store.tasks) as Task[]);
|
||||
} else {
|
||||
setTasks([]);
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
// Update List Name
|
||||
const handleUpdateListName = async (id: string, newName: string) => {
|
||||
|
||||
@@ -573,7 +573,16 @@ export function Sidebar({
|
||||
<span className="item-label">{list.name}</span>
|
||||
)}
|
||||
|
||||
<span className="item-count">{list._count?.tasks || ""}</span>
|
||||
<span className="item-count">
|
||||
{isDemo
|
||||
? (() => {
|
||||
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 : "";
|
||||
})()
|
||||
: list._count?.tasks || ""}
|
||||
</span>
|
||||
<button
|
||||
className="icon-btn"
|
||||
style={{ width: 20, height: 20, opacity: 0, transition: "opacity var(--dur-fast)", flexShrink: 0 }}
|
||||
@@ -616,14 +625,24 @@ export function Sidebar({
|
||||
<div
|
||||
className={`sidebar-item${isTrashActive ? " active" : ""}`}
|
||||
id="trash-menu-btn"
|
||||
onClick={onTrashSelect}
|
||||
onClick={() => {
|
||||
onTagSelect(null);
|
||||
onTrashSelect();
|
||||
}}
|
||||
style={{ color: isTrashActive ? "var(--danger)" : "var(--text-secondary)" }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ opacity: 0.7 }}>
|
||||
<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>
|
||||
{trashCount > 0 && <span className="item-count" style={{ color: "var(--danger)" }}>{trashCount}</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: 600 }}>{count}</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1026,7 +1026,7 @@ export function TaskList({
|
||||
)}
|
||||
|
||||
{/* Tasks Tree */}
|
||||
{incompleteTasks.map((task) => (
|
||||
{(isTrashActive ? tasks : incompleteTasks).map((task) => (
|
||||
<RecursiveTaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
|
||||
+18
-4
@@ -258,11 +258,18 @@ export function updateTaskInTree(tree: MockTask[], updated: MockTask): MockTask[
|
||||
});
|
||||
}
|
||||
|
||||
// Soft delete to Trash
|
||||
// Soft delete to Trash (recursively mark node and its children)
|
||||
export function moveToTrashInTree(tree: MockTask[], id: string): MockTask[] {
|
||||
const markDeleted = (node: MockTask): MockTask => ({
|
||||
...node,
|
||||
isDeleted: true,
|
||||
deletedAt: new Date().toISOString(),
|
||||
children: node.children ? node.children.map(markDeleted) : [],
|
||||
});
|
||||
|
||||
return tree.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, isDeleted: true, deletedAt: new Date().toISOString() };
|
||||
return markDeleted(node);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
return { ...node, children: moveToTrashInTree(node.children, id) };
|
||||
@@ -271,11 +278,18 @@ export function moveToTrashInTree(tree: MockTask[], id: string): MockTask[] {
|
||||
});
|
||||
}
|
||||
|
||||
// Restore from Trash
|
||||
// Restore from Trash (recursively unmark node and its children)
|
||||
export function restoreTaskInTree(tree: MockTask[], id: string): MockTask[] {
|
||||
const unmarkDeleted = (node: MockTask): MockTask => ({
|
||||
...node,
|
||||
isDeleted: false,
|
||||
deletedAt: null,
|
||||
children: node.children ? node.children.map(unmarkDeleted) : [],
|
||||
});
|
||||
|
||||
return tree.map((node) => {
|
||||
if (node.id === id) {
|
||||
return { ...node, isDeleted: false, deletedAt: null };
|
||||
return unmarkDeleted(node);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
return { ...node, children: restoreTaskInTree(node.children, id) };
|
||||
|
||||
Reference in New Issue
Block a user