1 parent
86f6c93183
commit
ab44cb18fc
5 files changed
+116
-15
No files matched your search
@@ -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"');
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [expandSiblingsRequest, setExpandSiblingsRequest] = useState<{ parentId: string | null; token: number } | null>(null);
|
||||
const [notebookSortMode, setNotebookSortMode] = useState<NotebookSortMode>(readNotebookSortPreference);
|
||||
const [collapsedNotebookIds, setCollapsedNotebookIds] = useState<Set<string>>(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}
|
||||
/>
|
||||
|
||||
@@ -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<HTMLDivElement>) => void;
|
||||
collapsedNotebookIds: ReadonlySet<string>;
|
||||
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 = ({
|
||||
<button
|
||||
className="flex h-6 w-5 items-center justify-center rounded hover:bg-slate-100/50 transition-colors"
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
onClick={() => onOpenChange(node.id, !open)}
|
||||
title={t("notebookTree.expandCollapse")}
|
||||
aria-label={open ? t("notebookTree.collapse", { name: node.name }) : t("notebookTree.expand", { name: node.name })}
|
||||
aria-expanded={open}
|
||||
@@ -250,14 +248,14 @@ export const NotebookTreeItem = ({
|
||||
if (event.key === "ArrowRight" && hasChildren && !open) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpen(true);
|
||||
onOpenChange(node.id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft" && hasChildren && open) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpen(false);
|
||||
onOpenChange(node.id, false);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
@@ -397,6 +395,8 @@ export const NotebookTreeItem = ({
|
||||
onMoveNotebook={onMoveNotebook}
|
||||
onMoveMemos={onMoveMemos}
|
||||
onDragScroll={onDragScroll}
|
||||
collapsedNotebookIds={collapsedNotebookIds}
|
||||
onOpenChange={onOpenChange}
|
||||
expandSiblingsRequest={expandSiblingsRequest}
|
||||
onExpandSiblings={onExpandSiblings}
|
||||
/>
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
EDITOR_PHONE_PREVIEW_STORAGE_KEY,
|
||||
EDITOR_PHONE_PREVIEW_FOLLOW_STORAGE_KEY,
|
||||
NOTEBOOK_SORT_STORAGE_KEY,
|
||||
NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY,
|
||||
SHORTCUT_SETTINGS_STORAGE_KEY,
|
||||
getSearchShortcutScope,
|
||||
getShortcutActionForEvent,
|
||||
getNotebookSortComparator,
|
||||
readEditorContentAlignmentPreference,
|
||||
readNotebookSortPreference,
|
||||
readNotebookTreeCollapsedIdsPreference,
|
||||
readDesktopFocusModePreference,
|
||||
readDesktopReadingProtectionPreference,
|
||||
readNotebookSidebarCollapsedPreference,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
readShortcutSettingsPreference,
|
||||
writeEditorContentAlignmentPreference,
|
||||
writeNotebookSortPreference,
|
||||
writeNotebookTreeCollapsedIdsPreference,
|
||||
writeDesktopFocusModePreference,
|
||||
writeDesktopReadingProtectionPreference,
|
||||
writeNotebookSidebarCollapsedPreference,
|
||||
@@ -355,6 +358,43 @@ describe("custom notebook sorting", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("notebook tree collapsed preference", () => {
|
||||
test("round-trips the user's collapsed notebook branches", () => {
|
||||
const values = installLocalStorage();
|
||||
|
||||
writeNotebookTreeCollapsedIdsPreference(["notebook-2", "notebook-1", "notebook-2"]);
|
||||
|
||||
expect(values.get(NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY)).toBe('["notebook-2","notebook-1"]');
|
||||
expect(readNotebookTreeCollapsedIdsPreference()).toEqual(new Set(["notebook-2", "notebook-1"]));
|
||||
});
|
||||
|
||||
test("ignores malformed and invalid stored values", () => {
|
||||
const values = installLocalStorage();
|
||||
|
||||
values.set(NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY, "{");
|
||||
expect(readNotebookTreeCollapsedIdsPreference()).toEqual(new Set());
|
||||
|
||||
values.set(NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY, JSON.stringify(["notebook-1", "", null, 2]));
|
||||
expect(readNotebookTreeCollapsedIdsPreference()).toEqual(new Set(["notebook-1"]));
|
||||
});
|
||||
|
||||
test("falls back to expanded when local storage is unavailable", () => {
|
||||
globalThis.window = {
|
||||
localStorage: {
|
||||
getItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
setItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(readNotebookTreeCollapsedIdsPreference()).toEqual(new Set());
|
||||
expect(() => writeNotebookTreeCollapsedIdsPreference(["notebook-1"])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace shortcut preferences", () => {
|
||||
test("provides navigation, AI, save, reading protection, and editor mode defaults", () => {
|
||||
expect(DEFAULT_SHORTCUT_SETTINGS.focusGlobalSearch).toEqual({
|
||||
|
||||
@@ -140,6 +140,7 @@ export const EDITOR_PHONE_PREVIEW_FOLLOW_STORAGE_KEY = "edgeever.editor.phonePre
|
||||
export const MEMO_LIST_DENSITY_STORAGE_KEY = "edgeever.memoListDensity";
|
||||
export const MEMO_LIST_WIDTH_STORAGE_KEY = "edgeever.memoListWidth";
|
||||
export const NOTEBOOK_SORT_STORAGE_KEY = "edgeever.notebookSort";
|
||||
export const NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY = "edgeever.notebookTreeCollapsedIds:v1";
|
||||
export const SHORTCUT_SETTINGS_STORAGE_KEY = "edgeever.shortcutSettings";
|
||||
export const DEFAULT_MEMO_LIST_WIDTH_PX = 360;
|
||||
export const MIN_MEMO_LIST_WIDTH_PX = 300;
|
||||
@@ -516,6 +517,28 @@ export const writeNotebookSortPreference = (sortMode: NotebookSortMode) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const readNotebookTreeCollapsedIdsPreference = () => {
|
||||
try {
|
||||
const value: unknown = JSON.parse(window.localStorage.getItem(NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY) ?? "[]");
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return new Set(value.filter((notebookId): notebookId is string => typeof notebookId === "string" && notebookId.length > 0));
|
||||
} catch {
|
||||
return new Set<string>();
|
||||
}
|
||||
};
|
||||
|
||||
export const writeNotebookTreeCollapsedIdsPreference = (notebookIds: Iterable<string>) => {
|
||||
try {
|
||||
window.localStorage.setItem(NOTEBOOK_TREE_COLLAPSED_IDS_STORAGE_KEY, JSON.stringify(Array.from(new Set(notebookIds))));
|
||||
} catch {
|
||||
// Local storage can be unavailable in private or restricted browser contexts.
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeShortcutKey = (key: string) => {
|
||||
if (key === " ") {
|
||||
return "space";
|
||||
|
||||
Reference in new issue
Block a user