Fix: Full N-depth recursive subtask tree rendering in main list, universal custom context menu on all subtask levels, and bidirectional realtime sync
This commit is contained in:
@@ -3,7 +3,16 @@ import { useState, useCallback, useEffect } from "react";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { TaskList, Task, List, User } from "../tasks/TaskList";
|
||||
import { TaskDetail } from "../tasks/TaskDetail";
|
||||
import { getDemoStore, saveDemoStore, MockList, MockTask } from "@/lib/mockData";
|
||||
import {
|
||||
getDemoStore,
|
||||
saveDemoStore,
|
||||
MockList,
|
||||
MockTask,
|
||||
updateTaskInTree,
|
||||
deleteTaskInTree,
|
||||
addTaskToTree,
|
||||
findTaskInTree,
|
||||
} from "@/lib/mockData";
|
||||
|
||||
interface AppShellProps {
|
||||
user: User;
|
||||
@@ -47,22 +56,9 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
setSelectedTask(task);
|
||||
}, []);
|
||||
|
||||
// Update a task and sync with main list and selected panel
|
||||
// Update a task in full tree (supports unlimited N-depth nesting)
|
||||
const handleTaskUpdate = useCallback((updated: Task) => {
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => {
|
||||
if (t.id === updated.id) {
|
||||
return { ...updated, children: updated.children || t.children || [] };
|
||||
}
|
||||
// If a sub-task was updated directly, update parent's children
|
||||
if (t.children && t.children.some((c) => c.id === updated.id)) {
|
||||
const newChildren = t.children.map((c) => (c.id === updated.id ? updated : c));
|
||||
return { ...t, children: newChildren };
|
||||
}
|
||||
return t;
|
||||
})
|
||||
);
|
||||
|
||||
setTasks((prev) => updateTaskInTree(prev as MockTask[], updated as MockTask) as Task[]);
|
||||
setSelectedTask((prev) => {
|
||||
if (prev && prev.id === updated.id) {
|
||||
return { ...updated, children: updated.children || prev.children || [] };
|
||||
@@ -105,7 +101,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// Demo Handlers
|
||||
// Demo Handlers with Full N-Depth Recursive Tree Support
|
||||
const handleDemoCreateList = (name: string, color: string) => {
|
||||
const newList: MockList = {
|
||||
id: "demo-list-" + Date.now(),
|
||||
@@ -150,79 +146,41 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
tags: [],
|
||||
};
|
||||
const store = getDemoStore();
|
||||
const newTasks = addTaskToTree(store.tasks, parentId, newTask as MockTask);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
|
||||
if (parentId) {
|
||||
// Add as subtask to parent
|
||||
const newTasks = store.tasks.map((t) => {
|
||||
if (t.id === parentId) {
|
||||
const currentChildren = t.children || [];
|
||||
return { ...t, children: [...currentChildren, newTask as MockTask] };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => (t.id === parentId ? { ...t, children: [...(t.children || []), newTask] } : t))
|
||||
setTasks((prev) => addTaskToTree(prev as MockTask[], parentId, newTask as MockTask) as Task[]);
|
||||
|
||||
// If currently selected task is the parent, update its children in the detail panel immediately
|
||||
if (selectedTask && selectedTask.id === parentId) {
|
||||
setSelectedTask((prev) =>
|
||||
prev ? { ...prev, children: [...(prev.children || []), newTask] } : prev
|
||||
);
|
||||
if (selectedTask && selectedTask.id === parentId) {
|
||||
setSelectedTask((prev) => (prev ? { ...prev, children: [...(prev.children || []), newTask] } : prev));
|
||||
}
|
||||
} else {
|
||||
// Top-level task
|
||||
const newTasks = [...store.tasks, newTask as MockTask];
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => [...prev, newTask]);
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleDemoToggleTask = (id: string, completed: boolean) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => {
|
||||
if (t.id === id) {
|
||||
return { ...t, completed, completedAt: completed ? new Date().toISOString() : null };
|
||||
}
|
||||
if (t.children && t.children.some((c) => c.id === id)) {
|
||||
const newChildren = t.children.map((c) =>
|
||||
c.id === id ? { ...c, completed, completedAt: completed ? new Date().toISOString() : null } : c
|
||||
);
|
||||
return { ...t, children: newChildren };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
const target = findTaskInTree(store.tasks, id);
|
||||
if (target) {
|
||||
const updated = {
|
||||
...target,
|
||||
completed,
|
||||
completedAt: completed ? new Date().toISOString() : null,
|
||||
};
|
||||
const newTasks = updateTaskInTree(store.tasks, updated);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) =>
|
||||
(updateTaskInTree(prev as MockTask[], updated) as Task[]).filter((t) => showCompleted || !t.completed)
|
||||
);
|
||||
|
||||
setTasks((prev) =>
|
||||
prev
|
||||
.map((t) => {
|
||||
if (t.id === id) {
|
||||
return { ...t, completed, completedAt: completed ? new Date().toISOString() : null };
|
||||
}
|
||||
if (t.children && t.children.some((c) => c.id === id)) {
|
||||
const newChildren = t.children.map((c) =>
|
||||
c.id === id ? { ...c, completed, completedAt: completed ? new Date().toISOString() : null } : c
|
||||
);
|
||||
return { ...t, children: newChildren };
|
||||
}
|
||||
return t;
|
||||
})
|
||||
.filter((t) => showCompleted || !t.completed)
|
||||
);
|
||||
|
||||
if (selectedTask) {
|
||||
if (selectedTask.id === id) {
|
||||
setSelectedTask((prev) => (prev ? { ...prev, completed, completedAt: completed ? new Date().toISOString() : null } : prev));
|
||||
} else if (selectedTask.children && selectedTask.children.some((c) => c.id === id)) {
|
||||
setSelectedTask((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
children: prev.children.map((c) =>
|
||||
c.id === id ? { ...c, completed, completedAt: completed ? new Date().toISOString() : null } : c
|
||||
),
|
||||
}
|
||||
: prev
|
||||
);
|
||||
// Keep selectedTask 100% in sync with the tree
|
||||
if (selectedTask) {
|
||||
const latestSelected = findTaskInTree(newTasks, selectedTask.id);
|
||||
if (latestSelected) {
|
||||
setSelectedTask(latestSelected as Task);
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
@@ -230,32 +188,18 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
|
||||
const handleDemoUpdateTask = (updated: Task) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => (t.id === updated.id ? (updated as MockTask) : t));
|
||||
const newTasks = updateTaskInTree(store.tasks, updated as MockTask);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
handleTaskUpdate(updated);
|
||||
};
|
||||
|
||||
const handleDemoDeleteTask = (id: string) => {
|
||||
const store = getDemoStore();
|
||||
// Remove if main task or remove from parent children
|
||||
const newTasks = store.tasks
|
||||
.filter((t) => t.id !== id)
|
||||
.map((t) => {
|
||||
if (t.children && t.children.some((c) => c.id === id)) {
|
||||
return { ...t, children: t.children.filter((c) => c.id !== id) };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
const newTasks = deleteTaskInTree(store.tasks, id);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) =>
|
||||
prev
|
||||
.filter((t) => t.id !== id)
|
||||
.map((t) => (t.children ? { ...t, children: t.children.filter((c) => c.id !== id) } : t))
|
||||
);
|
||||
setTasks((prev) => deleteTaskInTree(prev as MockTask[], id) as Task[]);
|
||||
if (selectedTask?.id === id) {
|
||||
setSelectedTask(null);
|
||||
} else if (selectedTask?.children?.some((c) => c.id === id)) {
|
||||
setSelectedTask((prev) => (prev ? { ...prev, children: prev.children.filter((c) => c.id !== id) } : prev));
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
@@ -267,25 +211,12 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => {
|
||||
if (t.id === taskId) return { ...t, title: trimmed };
|
||||
if (t.children && t.children.some((c) => c.id === taskId)) {
|
||||
return { ...t, children: t.children.map((c) => (c.id === taskId ? { ...c, title: trimmed } : c)) };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => {
|
||||
if (t.id === taskId) return { ...t, title: trimmed };
|
||||
if (t.children && t.children.some((c) => c.id === taskId)) {
|
||||
return { ...t, children: t.children.map((c) => (c.id === taskId ? { ...c, title: trimmed } : c)) };
|
||||
}
|
||||
return t;
|
||||
})
|
||||
);
|
||||
if (selectedTask?.id === taskId) {
|
||||
setSelectedTask((prev) => (prev ? { ...prev, title: trimmed } : prev));
|
||||
const target = findTaskInTree(store.tasks, taskId);
|
||||
if (target) {
|
||||
const updated = { ...target, title: trimmed };
|
||||
const newTasks = updateTaskInTree(store.tasks, updated);
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
handleTaskUpdate(updated as Task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user