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.
|
- 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.
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ export async function GET(req: NextRequest) {
|
|||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
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 where: Record<string, unknown> = {
|
const where: Record<string, unknown> = {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
parentId: null,
|
...(listId ? { listId, parentId: null } : tagName ? {} : { parentId: null }),
|
||||||
...(listId ? { listId } : {}),
|
|
||||||
...(showCompleted ? {} : { completed: false }),
|
...(showCompleted ? {} : { completed: false }),
|
||||||
|
...(tagName ? { tags: { some: { tag: { name: tagName } } } } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tasks = await prisma.task.findMany({
|
const tasks = await prisma.task.findMany({
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
return () => document.removeEventListener("checkflow:openCommandPalette", handler);
|
return () => document.removeEventListener("checkflow:openCommandPalette", handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Demo tasks filter & reconstruct hierarchical structure
|
// Task filter & reconstruct hierarchical structure for Demo and API modes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDemo) {
|
if (isDemo) {
|
||||||
const store = getDemoStore();
|
const store = getDemoStore();
|
||||||
@@ -130,6 +130,23 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
(t) => !t.isDeleted && t.listId === selectedListId && (showCompleted ? true : !t.completed)
|
(t) => !t.isDeleted && t.listId === selectedListId && (showCompleted ? true : !t.completed)
|
||||||
);
|
);
|
||||||
setTasks(listTasks);
|
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]);
|
}, [isDemo, selectedListId, isTrashActive, selectedTag, showCompleted, refreshKey]);
|
||||||
@@ -161,18 +178,25 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
|||||||
|
|
||||||
const handleTagSelect = useCallback((tagName: string | null) => {
|
const handleTagSelect = useCallback((tagName: string | null) => {
|
||||||
setIsTrashActive(false);
|
setIsTrashActive(false);
|
||||||
|
setSelectedListId(null);
|
||||||
setSelectedTag(tagName);
|
setSelectedTag(tagName);
|
||||||
setSelectedTask(null);
|
setSelectedTask(null);
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTrashSelect = useCallback(() => {
|
const handleTrashSelect = useCallback(() => {
|
||||||
setIsTrashActive(true);
|
|
||||||
setSelectedTag(null);
|
|
||||||
setSelectedListId(null);
|
setSelectedListId(null);
|
||||||
|
setSelectedTag(null);
|
||||||
|
setIsTrashActive(true);
|
||||||
setSelectedTask(null);
|
setSelectedTask(null);
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
}, []);
|
if (isDemo) {
|
||||||
|
const store = getDemoStore();
|
||||||
|
setTasks(getAllTrashTasks(store.tasks) as Task[]);
|
||||||
|
} else {
|
||||||
|
setTasks([]);
|
||||||
|
}
|
||||||
|
}, [isDemo]);
|
||||||
|
|
||||||
// Update List Name
|
// Update List Name
|
||||||
const handleUpdateListName = async (id: string, newName: string) => {
|
const handleUpdateListName = async (id: string, newName: string) => {
|
||||||
|
|||||||
@@ -573,7 +573,16 @@ export function Sidebar({
|
|||||||
<span className="item-label">{list.name}</span>
|
<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
|
<button
|
||||||
className="icon-btn"
|
className="icon-btn"
|
||||||
style={{ width: 20, height: 20, opacity: 0, transition: "opacity var(--dur-fast)", flexShrink: 0 }}
|
style={{ width: 20, height: 20, opacity: 0, transition: "opacity var(--dur-fast)", flexShrink: 0 }}
|
||||||
@@ -616,14 +625,24 @@ export function Sidebar({
|
|||||||
<div
|
<div
|
||||||
className={`sidebar-item${isTrashActive ? " active" : ""}`}
|
className={`sidebar-item${isTrashActive ? " active" : ""}`}
|
||||||
id="trash-menu-btn"
|
id="trash-menu-btn"
|
||||||
onClick={onTrashSelect}
|
onClick={() => {
|
||||||
|
onTagSelect(null);
|
||||||
|
onTrashSelect();
|
||||||
|
}}
|
||||||
style={{ color: isTrashActive ? "var(--danger)" : "var(--text-secondary)" }}
|
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 }}>
|
<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" />
|
<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 && <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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1026,7 +1026,7 @@ export function TaskList({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tasks Tree */}
|
{/* Tasks Tree */}
|
||||||
{incompleteTasks.map((task) => (
|
{(isTrashActive ? tasks : incompleteTasks).map((task) => (
|
||||||
<RecursiveTaskItem
|
<RecursiveTaskItem
|
||||||
key={task.id}
|
key={task.id}
|
||||||
task={task}
|
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[] {
|
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) => {
|
return tree.map((node) => {
|
||||||
if (node.id === id) {
|
if (node.id === id) {
|
||||||
return { ...node, isDeleted: true, deletedAt: new Date().toISOString() };
|
return markDeleted(node);
|
||||||
}
|
}
|
||||||
if (node.children && node.children.length > 0) {
|
if (node.children && node.children.length > 0) {
|
||||||
return { ...node, children: moveToTrashInTree(node.children, id) };
|
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[] {
|
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) => {
|
return tree.map((node) => {
|
||||||
if (node.id === id) {
|
if (node.id === id) {
|
||||||
return { ...node, isDeleted: false, deletedAt: null };
|
return unmarkDeleted(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) };
|
||||||
|
|||||||
Reference in New Issue
Block a user