Skip to content

Commit 8a85ab0

Browse files
committed
feat(desktop): list descendant notes when collapsed
Inkdrop shows a collapsed parent's notes plus its children. Expanded notebooks stay direct-only.
1 parent c369bc7 commit 8a85ab0

8 files changed

Lines changed: 107 additions & 8 deletions

File tree

apps/desktop/src/renderer/components/sidebar/NotebookItem.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
GripVertical,
1212
} from 'lucide-react';
1313
import type { NotebookTreeNode } from '../../../preload/index';
14+
import { useNotebookExpandStore } from '../../stores/notebookExpandStore';
1415
import { CommitHistory } from '../git/CommitHistory';
1516
import { sc } from './sc';
1617

@@ -62,7 +63,9 @@ export const NotebookItem = memo(function NotebookItem({
6263
onReorder,
6364
siblingIds,
6465
}: NotebookItemProps) {
65-
const [isExpanded, setIsExpanded] = useState(true);
66+
const isExpanded = useNotebookExpandStore(s => !s.collapsedIds.includes(node.notebook.id));
67+
const toggleExpanded = useNotebookExpandStore(s => s.toggle);
68+
const expandNotebook = useNotebookExpandStore(s => s.expand);
6669
const [isEditing, setIsEditing] = useState(false);
6770
const [editName, setEditName] = useState(node.notebook.name);
6871
const [isGitEnabled, setIsGitEnabled] = useState(false);
@@ -103,10 +106,13 @@ export const NotebookItem = memo(function NotebookItem({
103106
[node.notebook.id, onSelect]
104107
);
105108

106-
const handleToggle = useCallback((e: React.MouseEvent) => {
107-
e.stopPropagation();
108-
setIsExpanded(prev => !prev);
109-
}, []);
109+
const handleToggle = useCallback(
110+
(e: React.MouseEvent) => {
111+
e.stopPropagation();
112+
toggleExpanded(node.notebook.id);
113+
},
114+
[node.notebook.id, toggleExpanded]
115+
);
110116

111117
const handleDoubleClick = useCallback(
112118
(e: React.MouseEvent) => {
@@ -287,7 +293,7 @@ export const NotebookItem = memo(function NotebookItem({
287293
if (pos === 'inside') {
288294
// Move dragged notebook into this one as a child
289295
onMove?.(draggedId, node.notebook.id);
290-
setIsExpanded(true);
296+
expandNotebook(node.notebook.id);
291297
} else if (pos === 'above' || pos === 'below') {
292298
const fromSameParent =
293299
(draggedParentId === 'root' ? null : draggedParentId) === thisParentId;
@@ -313,6 +319,7 @@ export const NotebookItem = memo(function NotebookItem({
313319
onMove,
314320
onReorder,
315321
siblingIds,
322+
expandNotebook,
316323
]
317324
);
318325

apps/desktop/src/renderer/data/nowBoard.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Shipped this week:
7575
- Note templates: Meeting, Decision, Daily, Weekly, Reading, Issue (notebook `templates`).
7676
- What’s New is authored (`docs/releases/`). Promotion PRs: `chore(release): promote X.Y.Z`.
7777
- Workspace view: click a notebook to focus the sidebar on that tree. Escape or the breadcrumb home returns to All Notes.
78+
- Collapsed notebooks list notes from their descendants; expanded ones stay direct-only.
7879

7980
CSS path (decided 2026-08-17):
8081
Keep CSS modules + tokens. Do not add Tailwind to desktop.

apps/desktop/src/renderer/hooks/useNavigation.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
type SortOrder,
2121
} from '../stores/navigationStore';
2222
import { listOptionsFromNav } from '../utils/listOptionsFromNav';
23-
import { collectNotebookSubtreeIds } from '../utils/notebookTree';
23+
import { collectNotebookSubtreeIds, findNotebookNode } from '../utils/notebookTree';
24+
import { useNotebookExpandStore } from '../stores/notebookExpandStore';
2425
import { useNotes, useNoteCounts, useScopedNoteCounts, withExcerpt } from './useNotes';
2526
import { useNotebookTree, getNotebookPath, getAncestorIds } from './useNotebooks';
2627

@@ -139,6 +140,15 @@ export function useFilteredNotes(): NoteWithExcerpt[] {
139140
const sortOrder = useSortOrder();
140141
const workspaceListAll = useWorkspaceListAll();
141142
const workspaceNotebookIds = useWorkspaceNotebookIds();
143+
const collapsedIds = useNotebookExpandStore(s => s.collapsedIds);
144+
const { data: tree } = useNotebookTree();
145+
const descendantNotebookIds = useMemo(() => {
146+
if (navigation.kind !== 'notebook') return undefined;
147+
if (!collapsedIds.includes(navigation.id)) return undefined;
148+
const node = findNotebookNode(tree ?? [], navigation.id);
149+
if (!node || node.children.length === 0) return undefined;
150+
return collectNotebookSubtreeIds(tree ?? [], navigation.id);
151+
}, [navigation, collapsedIds, tree]);
142152

143153
const options = useMemo(
144154
() =>
@@ -150,8 +160,18 @@ export function useFilteredNotes(): NoteWithExcerpt[] {
150160
sortOrder,
151161
workspaceNotebookIds,
152162
workspaceListAll,
163+
descendantNotebookIds,
153164
}),
154-
[navigation, statusFilter, tagFilter, sortBy, sortOrder, workspaceNotebookIds, workspaceListAll]
165+
[
166+
navigation,
167+
statusFilter,
168+
tagFilter,
169+
sortBy,
170+
sortOrder,
171+
workspaceNotebookIds,
172+
workspaceListAll,
173+
descendantNotebookIds,
174+
]
155175
);
156176
const { data: notes } = useNotes(options);
157177

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it, beforeEach } from 'vitest';
2+
import { useNotebookExpandStore } from '../notebookExpandStore';
3+
4+
describe('notebookExpandStore', () => {
5+
beforeEach(() => {
6+
useNotebookExpandStore.setState({ collapsedIds: [] });
7+
});
8+
9+
it('treats notebooks as expanded by default', () => {
10+
expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(true);
11+
});
12+
13+
it('toggles collapse', () => {
14+
useNotebookExpandStore.getState().toggle('work');
15+
expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(false);
16+
useNotebookExpandStore.getState().toggle('work');
17+
expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(true);
18+
});
19+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { create } from 'zustand';
2+
3+
/** Notebooks default to expanded. This set is the collapsed ones. */
4+
interface NotebookExpandStore {
5+
collapsedIds: string[];
6+
isExpanded: (id: string) => boolean;
7+
toggle: (id: string) => void;
8+
expand: (id: string) => void;
9+
}
10+
11+
export const useNotebookExpandStore = create<NotebookExpandStore>((set, get) => ({
12+
collapsedIds: [],
13+
isExpanded: id => !get().collapsedIds.includes(id),
14+
toggle: id =>
15+
set(state => ({
16+
collapsedIds: state.collapsedIds.includes(id)
17+
? state.collapsedIds.filter(item => item !== id)
18+
: [...state.collapsedIds, id],
19+
})),
20+
expand: id =>
21+
set(state => ({
22+
collapsedIds: state.collapsedIds.filter(item => item !== id),
23+
})),
24+
}));

apps/desktop/src/renderer/utils/__tests__/listOptionsFromNav.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,22 @@ describe('listOptionsFromNav', () => {
9898
});
9999
});
100100

101+
it('includes descendant notebooks when the selected parent is collapsed', () => {
102+
expect(
103+
listOptionsFromNav({
104+
navigation: { kind: 'notebook', id: 'work' },
105+
statusFilter: null,
106+
tagFilter: null,
107+
descendantNotebookIds: ['work', 'api', 'web'],
108+
})
109+
).toEqual({
110+
notebookIds: ['work', 'api', 'web'],
111+
archived: 'active',
112+
isDeleted: false,
113+
limit: 10000,
114+
});
115+
});
116+
101117
it('scopes sidebar counts to the workspace tree', () => {
102118
expect(
103119
listOptionsFromNav({

apps/desktop/src/renderer/utils/listOptionsFromNav.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export function listOptionsFromNav(input: {
2020
workspaceListAll?: boolean;
2121
/** Sidebar counts: always the workspace tree when focused. */
2222
scopeToWorkspaceTree?: boolean;
23+
/** Selected notebook subtree when that notebook is collapsed. */
24+
descendantNotebookIds?: string[];
2325
}): ListOptions {
2426
const options: ListOptions = {
2527
...optionsForNavigation(input.navigation, input),
@@ -51,6 +53,7 @@ function optionsForNavigation(
5153
workspaceNotebookIds?: string[];
5254
workspaceListAll?: boolean;
5355
scopeToWorkspaceTree?: boolean;
56+
descendantNotebookIds?: string[];
5457
}
5558
): ListOptions {
5659
switch (navigation.kind) {
@@ -80,6 +83,14 @@ function optionsForNavigation(
8083
isDeleted: false,
8184
};
8285
}
86+
const descendants = input.descendantNotebookIds;
87+
if (descendants && descendants.length > 1) {
88+
return {
89+
notebookIds: descendants,
90+
archived: 'active',
91+
isDeleted: false,
92+
};
93+
}
8394
return {
8495
notebookId: navigation.id,
8596
archived: 'active',

docs/NOW.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Shipped this week:
7575
- Note templates: Meeting, Decision, Daily, Weekly, Reading, Issue (notebook `templates`).
7676
- What’s New is authored (`docs/releases/`). Promotion PRs: `chore(release): promote X.Y.Z`. See `docs/WHATS_NEW.md`.
7777
- Workspace view: click a notebook to focus the sidebar on that tree. Escape or the breadcrumb home returns to All Notes.
78+
- Collapsed notebooks list notes from their descendants; expanded ones stay direct-only.
7879

7980
CSS path (decided 2026-08-17):
8081
Keep CSS modules + tokens. Do not add Tailwind to desktop.

0 commit comments

Comments
 (0)