Collapse completed to-dos in group session dialog
- Move completed drafts out of the active list to reduce clutter. - Add a collapsible “Completed” section with per-row expansion controls.
This commit is contained in:
parent
ddba881380
commit
661a1bfab6
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Pencil, Plus, Sparkles, Trash2, X, XIcon } from "lucide-react";
|
import { ChevronDown, ChevronRight, Pencil, Plus, Sparkles, Trash2, X, XIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -108,6 +108,8 @@ export function GroupSessionDialog({
|
||||||
const [aiOpen, setAiOpen] = useState(false);
|
const [aiOpen, setAiOpen] = useState(false);
|
||||||
const [todoAiOpen, setTodoAiOpen] = useState(false);
|
const [todoAiOpen, setTodoAiOpen] = useState(false);
|
||||||
const [savingAll, setSavingAll] = useState(false);
|
const [savingAll, setSavingAll] = useState(false);
|
||||||
|
const [completedOpen, setCompletedOpen] = useState(false);
|
||||||
|
const [expandedCompletedIds, setExpandedCompletedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const rowFlushersRef = useRef(new Map<string, () => Promise<void>>());
|
const rowFlushersRef = useRef(new Map<string, () => Promise<void>>());
|
||||||
const dirtyRowIdsRef = useRef(new Set<string>());
|
const dirtyRowIdsRef = useRef(new Set<string>());
|
||||||
|
|
@ -150,6 +152,9 @@ export function GroupSessionDialog({
|
||||||
setTodoDrafts((prev) => reconcileTodoDrafts(prev, group.todos, dirtyRowIdsRef.current));
|
setTodoDrafts((prev) => reconcileTodoDrafts(prev, group.todos, dirtyRowIdsRef.current));
|
||||||
}, [group.todos, open]);
|
}, [group.todos, open]);
|
||||||
|
|
||||||
|
const activeTodoDrafts = todoDrafts.filter((d) => !d.completed);
|
||||||
|
const completedTodoDrafts = todoDrafts.filter((d) => d.completed);
|
||||||
|
|
||||||
function updateTodoDraft(id: string, patch: Partial<TodoDraft>) {
|
function updateTodoDraft(id: string, patch: Partial<TodoDraft>) {
|
||||||
setTodoDrafts((prev) => prev.map((d) => (d.id === id ? { ...d, ...patch } : d)));
|
setTodoDrafts((prev) => prev.map((d) => (d.id === id ? { ...d, ...patch } : d)));
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +181,15 @@ export function GroupSessionDialog({
|
||||||
removeTodoDraftLocalOnly(tempIdToRemove);
|
removeTodoDraftLocalOnly(tempIdToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCompletedExpanded(id: string) {
|
||||||
|
setExpandedCompletedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addEmptyTodoDraft() {
|
function addEmptyTodoDraft() {
|
||||||
setTodoDrafts((prev) => [
|
setTodoDrafts((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -231,7 +245,7 @@ export function GroupSessionDialog({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">To-dos</Label>
|
<Label className="text-sm font-medium">To-dos</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{todoDrafts.map((draft) => (
|
{activeTodoDrafts.map((draft) => (
|
||||||
<SessionTodoRow
|
<SessionTodoRow
|
||||||
key={draft.id}
|
key={draft.id}
|
||||||
draft={draft}
|
draft={draft}
|
||||||
|
|
@ -257,6 +271,60 @@ export function GroupSessionDialog({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{completedTodoDrafts.length > 0 && (
|
||||||
|
<div className="space-y-2 rounded-md border">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCompletedOpen((v) => !v)}
|
||||||
|
className="flex w-full items-center gap-2 p-3 text-left text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{completedOpen ? (
|
||||||
|
<ChevronDown className="size-4 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="size-4 shrink-0" />
|
||||||
|
)}
|
||||||
|
Completed ({completedTodoDrafts.length})
|
||||||
|
</button>
|
||||||
|
{completedOpen && (
|
||||||
|
<div className="space-y-2 p-3 pt-0">
|
||||||
|
{completedTodoDrafts.map((draft) =>
|
||||||
|
expandedCompletedIds.has(draft.id) ? (
|
||||||
|
<SessionTodoRow
|
||||||
|
key={draft.id}
|
||||||
|
draft={draft}
|
||||||
|
groupId={group.id}
|
||||||
|
categoryId={group.categoryId}
|
||||||
|
accentColor={accentColor}
|
||||||
|
todoTextColor={todoTextColor}
|
||||||
|
isDark={isDark}
|
||||||
|
onChangeLocal={(patch) => updateTodoDraft(draft.id, patch)}
|
||||||
|
onRemoveLocalOnly={() => removeTodoDraftLocalOnly(draft.id)}
|
||||||
|
onCreated={() => handleAddTodoCreated(draft.id)}
|
||||||
|
onDirtyChange={(dirty) => setRowDirty(draft.id, dirty)}
|
||||||
|
registerFlush={registerRowFlush}
|
||||||
|
addTodo={addTodo}
|
||||||
|
editTodo={editTodo}
|
||||||
|
toggleTodoDone={toggleTodoDone}
|
||||||
|
removeTodo={removeTodo}
|
||||||
|
onCollapse={() => toggleCompletedExpanded(draft.id)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
key={draft.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleCompletedExpanded(draft.id)}
|
||||||
|
className="flex w-full items-center gap-2 rounded-md border border-dashed p-2 text-left text-sm text-muted-foreground hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-3.5 shrink-0" />
|
||||||
|
<span className="flex-1 truncate line-through">{draft.title}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -304,6 +372,7 @@ function SessionTodoRow({
|
||||||
editTodo,
|
editTodo,
|
||||||
toggleTodoDone,
|
toggleTodoDone,
|
||||||
removeTodo,
|
removeTodo,
|
||||||
|
onCollapse,
|
||||||
}: {
|
}: {
|
||||||
draft: TodoDraft;
|
draft: TodoDraft;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
|
@ -320,6 +389,7 @@ function SessionTodoRow({
|
||||||
editTodo: ReturnType<typeof useBoard>["editTodo"];
|
editTodo: ReturnType<typeof useBoard>["editTodo"];
|
||||||
toggleTodoDone: ReturnType<typeof useBoard>["toggleTodoDone"];
|
toggleTodoDone: ReturnType<typeof useBoard>["toggleTodoDone"];
|
||||||
removeTodo: ReturnType<typeof useBoard>["removeTodo"];
|
removeTodo: ReturnType<typeof useBoard>["removeTodo"];
|
||||||
|
onCollapse?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [editingDetails, setEditingDetails] = useState(false);
|
const [editingDetails, setEditingDetails] = useState(false);
|
||||||
// Guards against creating the same new to-do twice if title/details both
|
// Guards against creating the same new to-do twice if title/details both
|
||||||
|
|
@ -435,6 +505,17 @@ function SessionTodoRow({
|
||||||
style={draft.completed ? undefined : { color: todoTextColor }}
|
style={draft.completed ? undefined : { color: todoTextColor }}
|
||||||
className={cn("flex-1", draft.completed && "text-muted-foreground line-through")}
|
className={cn("flex-1", draft.completed && "text-muted-foreground line-through")}
|
||||||
/>
|
/>
|
||||||
|
{onCollapse && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="shrink-0"
|
||||||
|
onClick={onCollapse}
|
||||||
|
aria-label={`Collapse "${draft.title || "to-do"}"`}
|
||||||
|
>
|
||||||
|
<ChevronDown className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue