Fix: Real-time 2-level subtask sync between panels and inline title editing for list and tasks
This commit is contained in:
@@ -30,7 +30,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
// Demo tasks filter
|
||||
// Demo tasks filter & reconstruct hierarchical structure
|
||||
useEffect(() => {
|
||||
if (isDemo && selectedListId) {
|
||||
const store = getDemoStore();
|
||||
@@ -47,9 +47,28 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
setSelectedTask(task);
|
||||
}, []);
|
||||
|
||||
// Update a task and sync with main list and selected panel
|
||||
const handleTaskUpdate = useCallback((updated: Task) => {
|
||||
setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
|
||||
setSelectedTask(updated);
|
||||
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;
|
||||
})
|
||||
);
|
||||
|
||||
setSelectedTask((prev) => {
|
||||
if (prev && prev.id === updated.id) {
|
||||
return { ...updated, children: updated.children || prev.children || [] };
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleListSelect = useCallback((id: string) => {
|
||||
@@ -58,6 +77,34 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
setSidebarOpen(false);
|
||||
}, []);
|
||||
|
||||
// Update List Name (Inline edit on header or sidebar)
|
||||
const handleUpdateListName = async (id: string, newName: string) => {
|
||||
const trimmed = newName.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
if (isDemo) {
|
||||
const store = getDemoStore();
|
||||
const newLists = store.lists.map((l) => (l.id === id ? { ...l, name: trimmed } : l));
|
||||
saveDemoStore(newLists, store.tasks);
|
||||
setLists(newLists);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/lists/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: trimmed }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
setLists((prev) => prev.map((l) => (l.id === id ? { ...l, name: updated.name } : l)));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to update list name", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Demo Handlers
|
||||
const handleDemoCreateList = (name: string, color: string) => {
|
||||
const newList: MockList = {
|
||||
@@ -85,11 +132,11 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleDemoAddTask = (title: string, listId: string) => {
|
||||
const handleDemoAddTask = (title: string, listId: string, parentId: string | null = null) => {
|
||||
const newTask: Task = {
|
||||
id: "demo-task-" + Date.now(),
|
||||
listId,
|
||||
parentId: null,
|
||||
parentId: parentId || null,
|
||||
title,
|
||||
note: null,
|
||||
completed: false,
|
||||
@@ -103,9 +150,29 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
tags: [],
|
||||
};
|
||||
const store = getDemoStore();
|
||||
const newTasks = [...store.tasks, newTask as MockTask];
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => [...prev, newTask]);
|
||||
|
||||
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))
|
||||
);
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -115,12 +182,49 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
if (t.id === id) {
|
||||
return { ...t, completed, completedAt: completed ? new Date().toISOString() : null };
|
||||
}
|
||||
return {
|
||||
...t,
|
||||
children: t.children?.map((c) => (c.id === id ? { ...c, completed } : c)) || [],
|
||||
};
|
||||
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);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
};
|
||||
|
||||
@@ -128,19 +232,79 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.map((t) => (t.id === updated.id ? (updated as MockTask) : t));
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
|
||||
setSelectedTask(updated);
|
||||
handleTaskUpdate(updated);
|
||||
};
|
||||
|
||||
const handleDemoDeleteTask = (id: string) => {
|
||||
const store = getDemoStore();
|
||||
const newTasks = store.tasks.filter((t) => t.id !== id);
|
||||
// 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;
|
||||
});
|
||||
saveDemoStore(store.lists, newTasks);
|
||||
setTasks((prev) => prev.filter((t) => t.id !== id));
|
||||
setSelectedTask(null);
|
||||
setTasks((prev) =>
|
||||
prev
|
||||
.filter((t) => t.id !== id)
|
||||
.map((t) => (t.children ? { ...t, children: t.children.filter((c) => c.id !== id) } : t))
|
||||
);
|
||||
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();
|
||||
};
|
||||
|
||||
// Inline update task title from main list
|
||||
const handleUpdateTaskTitle = async (taskId: string, newTitle: string) => {
|
||||
const trimmed = newTitle.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
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));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: trimmed }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
handleTaskUpdate(updated);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to update task title", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
{/* Mobile overlay */}
|
||||
@@ -182,6 +346,8 @@ export function AppShell({ user, isDemo = false }: AppShellProps) {
|
||||
isDemo={isDemo}
|
||||
onDemoAddTask={handleDemoAddTask}
|
||||
onDemoToggleTask={handleDemoToggleTask}
|
||||
onUpdateTaskTitle={handleUpdateTaskTitle}
|
||||
onUpdateListName={handleUpdateListName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user