From ab44cb18fcc8a0f328df0cd6b283099fe4408f58 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Tue, 22 Sep 2026 10:04:57 +0800 Subject: [PATCH] fix: persist notebook tree expansion state Refs #345 --- apps/web/src/components/NotebookPane.test.mjs | 13 ++++++ apps/web/src/components/NotebookPane.tsx | 25 ++++++++++++ apps/web/src/components/NotebookTreeItem.tsx | 30 +++++++------- apps/web/src/lib/app-helpers.test.mjs | 40 +++++++++++++++++++ apps/web/src/lib/app-helpers.ts | 23 +++++++++++ 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/NotebookPane.test.mjs b/apps/web/src/components/NotebookPane.test.mjs index c76f7852..0609e70f 100644 --- a/apps/web/src/components/NotebookPane.test.mjs +++ b/apps/web/src/components/NotebookPane.test.mjs @@ -99,6 +99,19 @@ describe("NotebookPane sidebar collapse", () => { }); }); +describe("NotebookPane tree expansion persistence", () => { + test("stores collapsed branches outside individual tree items", () => { + const treeItemSource = readFileSync(new URL("./NotebookTreeItem.tsx", import.meta.url), "utf8"); + + expect(source).toContain("readNotebookTreeCollapsedIdsPreference"); + expect(source).toContain("writeNotebookTreeCollapsedIdsPreference(collapsedNotebookIds)"); + expect(source).toContain("collapsedNotebookIds={collapsedNotebookIds}"); + expect(treeItemSource).toContain("const open = !collapsedNotebookIds.has(node.id)"); + expect(treeItemSource).not.toContain("const [open, setOpen] = useState(true)"); + expect(treeItemSource).not.toContain("if (hasSelectedDescendant) {"); + }); +}); + describe("NotebookPane client downloads", () => { test("keeps macOS, Windows, and Linux downloads visible in the desktop runtime", () => { expect(source).toContain('t("pwa.sidebarMac") || "macOS"'); diff --git a/apps/web/src/components/NotebookPane.tsx b/apps/web/src/components/NotebookPane.tsx index 14a4c92b..bd5a9843 100644 --- a/apps/web/src/components/NotebookPane.tsx +++ b/apps/web/src/components/NotebookPane.tsx @@ -55,7 +55,9 @@ import { getNotebookSortOptions, getNotebookSortComparator, hasEdgeEverDragData, + readNotebookTreeCollapsedIdsPreference, readNotebookSortPreference, + writeNotebookTreeCollapsedIdsPreference, writeNotebookSortPreference, } from "@/lib/app-helpers"; import type { EdgeEverRepository } from "@/lib/repository"; @@ -529,6 +531,7 @@ export const NotebookPane = ({ const notebookDragScrollFrameRef = useRef(null); const [expandSiblingsRequest, setExpandSiblingsRequest] = useState<{ parentId: string | null; token: number } | null>(null); const [notebookSortMode, setNotebookSortMode] = useState(readNotebookSortPreference); + const [collapsedNotebookIds, setCollapsedNotebookIds] = useState>(readNotebookTreeCollapsedIdsPreference); const handleMoveNotebook = useCallback((notebookId: string, targetNotebookId: string, position: NotebookDropPosition) => { setNotebookSortMode("custom"); @@ -605,6 +608,26 @@ export const NotebookPane = ({ writeNotebookSortPreference(notebookSortMode); }, [notebookSortMode]); + useEffect(() => { + writeNotebookTreeCollapsedIdsPreference(collapsedNotebookIds); + }, [collapsedNotebookIds]); + + const handleNotebookOpenChange = useCallback((notebookId: string, open: boolean) => { + setCollapsedNotebookIds((current) => { + if (current.has(notebookId) === !open) { + return current; + } + + const next = new Set(current); + if (open) { + next.delete(notebookId); + } else { + next.add(notebookId); + } + return next; + }); + }, []); + useEffect(() => { if (!selectedNotebookId) { return; @@ -803,6 +826,8 @@ export const NotebookPane = ({ onMoveNotebook={handleMoveNotebook} onMoveMemos={onMoveMemos} onDragScroll={handleNotebookScrollDragOver} + collapsedNotebookIds={collapsedNotebookIds} + onOpenChange={handleNotebookOpenChange} expandSiblingsRequest={expandSiblingsRequest} onExpandSiblings={handleExpandNotebookSiblings} /> diff --git a/apps/web/src/components/NotebookTreeItem.tsx b/apps/web/src/components/NotebookTreeItem.tsx index 4ee674a2..14b7b41c 100644 --- a/apps/web/src/components/NotebookTreeItem.tsx +++ b/apps/web/src/components/NotebookTreeItem.tsx @@ -37,6 +37,8 @@ export const NotebookTreeItem = ({ onMoveNotebook, onMoveMemos, onDragScroll, + collapsedNotebookIds, + onOpenChange, expandSiblingsRequest, onExpandSiblings, }: { @@ -50,11 +52,13 @@ export const NotebookTreeItem = ({ onMoveNotebook: (notebookId: string, targetNotebookId: string, position: NotebookDropPosition) => void; onMoveMemos: (memoIds: string[], targetNotebookId: string) => void; onDragScroll: (event: DragEvent) => void; + collapsedNotebookIds: ReadonlySet; + onOpenChange: (notebookId: string, open: boolean) => void; expandSiblingsRequest: { parentId: string | null; token: number } | null; onExpandSiblings: (parentId: string | null) => void; }) => { const { t } = useTranslation(); - const [open, setOpen] = useState(true); + const open = !collapsedNotebookIds.has(node.id); const hasChildren = node.children.length > 0; const selected = node.id === selectedNotebookId; const isInbox = node.slug === "inbox"; @@ -90,19 +94,13 @@ export const NotebookTreeItem = ({ return () => document.removeEventListener("pointerdown", closeOnOutsidePointer); }, [actionsOpen]); - useEffect(() => { - if (hasSelectedDescendant) { - setOpen(true); - } - }, [hasSelectedDescendant]); - useEffect(() => { if (!expandSiblingsRequest || expandSiblingsRequest.parentId !== node.parentId || !hasChildren) { return; } - setOpen(true); - }, [expandSiblingsRequest, hasChildren, node.parentId]); + onOpenChange(node.id, true); + }, [expandSiblingsRequest, hasChildren, node.id, node.parentId, onOpenChange]); const scheduleDragExpand = (position: NotebookDropPosition) => { if (!hasChildren || open || position !== "inside") { @@ -116,7 +114,7 @@ export const NotebookTreeItem = ({ expandTimerRef.current = window.setTimeout(() => { expandTimerRef.current = null; - setOpen(true); + onOpenChange(node.id, true); }, NOTEBOOK_DRAG_EXPAND_DELAY_MS); }; @@ -155,7 +153,7 @@ export const NotebookTreeItem = ({ if (memoIds.length > 0) { onMoveMemos(memoIds, node.id); - setOpen(true); + onOpenChange(node.id, true); return; } @@ -164,7 +162,7 @@ export const NotebookTreeItem = ({ } onMoveNotebook(notebookId, node.id, position); - setOpen(true); + onOpenChange(node.id, true); }; return ( @@ -203,7 +201,7 @@ export const NotebookTreeItem = ({