From 8a85ab06b009123a8759efe72b58d22b6ea29f1f Mon Sep 17 00:00:00 2001 From: Tomas Maritano Date: Tue, 18 Aug 2026 22:58:48 -0300 Subject: [PATCH] feat(desktop): list descendant notes when collapsed Inkdrop shows a collapsed parent's notes plus its children. Expanded notebooks stay direct-only. --- .../components/sidebar/NotebookItem.tsx | 19 ++++++++++----- apps/desktop/src/renderer/data/nowBoard.md | 1 + .../src/renderer/hooks/useNavigation.ts | 24 +++++++++++++++++-- .../__tests__/notebookExpandStore.test.ts | 19 +++++++++++++++ .../renderer/stores/notebookExpandStore.ts | 24 +++++++++++++++++++ .../__tests__/listOptionsFromNav.test.ts | 16 +++++++++++++ .../src/renderer/utils/listOptionsFromNav.ts | 11 +++++++++ docs/NOW.md | 1 + 8 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/renderer/stores/__tests__/notebookExpandStore.test.ts create mode 100644 apps/desktop/src/renderer/stores/notebookExpandStore.ts diff --git a/apps/desktop/src/renderer/components/sidebar/NotebookItem.tsx b/apps/desktop/src/renderer/components/sidebar/NotebookItem.tsx index f359553b..de6215f0 100644 --- a/apps/desktop/src/renderer/components/sidebar/NotebookItem.tsx +++ b/apps/desktop/src/renderer/components/sidebar/NotebookItem.tsx @@ -11,6 +11,7 @@ import { GripVertical, } from 'lucide-react'; import type { NotebookTreeNode } from '../../../preload/index'; +import { useNotebookExpandStore } from '../../stores/notebookExpandStore'; import { CommitHistory } from '../git/CommitHistory'; import { sc } from './sc'; @@ -62,7 +63,9 @@ export const NotebookItem = memo(function NotebookItem({ onReorder, siblingIds, }: NotebookItemProps) { - const [isExpanded, setIsExpanded] = useState(true); + const isExpanded = useNotebookExpandStore(s => !s.collapsedIds.includes(node.notebook.id)); + const toggleExpanded = useNotebookExpandStore(s => s.toggle); + const expandNotebook = useNotebookExpandStore(s => s.expand); const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(node.notebook.name); const [isGitEnabled, setIsGitEnabled] = useState(false); @@ -103,10 +106,13 @@ export const NotebookItem = memo(function NotebookItem({ [node.notebook.id, onSelect] ); - const handleToggle = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - setIsExpanded(prev => !prev); - }, []); + const handleToggle = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + toggleExpanded(node.notebook.id); + }, + [node.notebook.id, toggleExpanded] + ); const handleDoubleClick = useCallback( (e: React.MouseEvent) => { @@ -287,7 +293,7 @@ export const NotebookItem = memo(function NotebookItem({ if (pos === 'inside') { // Move dragged notebook into this one as a child onMove?.(draggedId, node.notebook.id); - setIsExpanded(true); + expandNotebook(node.notebook.id); } else if (pos === 'above' || pos === 'below') { const fromSameParent = (draggedParentId === 'root' ? null : draggedParentId) === thisParentId; @@ -313,6 +319,7 @@ export const NotebookItem = memo(function NotebookItem({ onMove, onReorder, siblingIds, + expandNotebook, ] ); diff --git a/apps/desktop/src/renderer/data/nowBoard.md b/apps/desktop/src/renderer/data/nowBoard.md index 4e776c86..f40f2b2d 100644 --- a/apps/desktop/src/renderer/data/nowBoard.md +++ b/apps/desktop/src/renderer/data/nowBoard.md @@ -75,6 +75,7 @@ Shipped this week: - Note templates: Meeting, Decision, Daily, Weekly, Reading, Issue (notebook `templates`). - What’s New is authored (`docs/releases/`). Promotion PRs: `chore(release): promote X.Y.Z`. - Workspace view: click a notebook to focus the sidebar on that tree. Escape or the breadcrumb home returns to All Notes. +- Collapsed notebooks list notes from their descendants; expanded ones stay direct-only. CSS path (decided 2026-08-17): Keep CSS modules + tokens. Do not add Tailwind to desktop. diff --git a/apps/desktop/src/renderer/hooks/useNavigation.ts b/apps/desktop/src/renderer/hooks/useNavigation.ts index a5bce6b8..bcef59e5 100644 --- a/apps/desktop/src/renderer/hooks/useNavigation.ts +++ b/apps/desktop/src/renderer/hooks/useNavigation.ts @@ -20,7 +20,8 @@ import { type SortOrder, } from '../stores/navigationStore'; import { listOptionsFromNav } from '../utils/listOptionsFromNav'; -import { collectNotebookSubtreeIds } from '../utils/notebookTree'; +import { collectNotebookSubtreeIds, findNotebookNode } from '../utils/notebookTree'; +import { useNotebookExpandStore } from '../stores/notebookExpandStore'; import { useNotes, useNoteCounts, useScopedNoteCounts, withExcerpt } from './useNotes'; import { useNotebookTree, getNotebookPath, getAncestorIds } from './useNotebooks'; @@ -139,6 +140,15 @@ export function useFilteredNotes(): NoteWithExcerpt[] { const sortOrder = useSortOrder(); const workspaceListAll = useWorkspaceListAll(); const workspaceNotebookIds = useWorkspaceNotebookIds(); + const collapsedIds = useNotebookExpandStore(s => s.collapsedIds); + const { data: tree } = useNotebookTree(); + const descendantNotebookIds = useMemo(() => { + if (navigation.kind !== 'notebook') return undefined; + if (!collapsedIds.includes(navigation.id)) return undefined; + const node = findNotebookNode(tree ?? [], navigation.id); + if (!node || node.children.length === 0) return undefined; + return collectNotebookSubtreeIds(tree ?? [], navigation.id); + }, [navigation, collapsedIds, tree]); const options = useMemo( () => @@ -150,8 +160,18 @@ export function useFilteredNotes(): NoteWithExcerpt[] { sortOrder, workspaceNotebookIds, workspaceListAll, + descendantNotebookIds, }), - [navigation, statusFilter, tagFilter, sortBy, sortOrder, workspaceNotebookIds, workspaceListAll] + [ + navigation, + statusFilter, + tagFilter, + sortBy, + sortOrder, + workspaceNotebookIds, + workspaceListAll, + descendantNotebookIds, + ] ); const { data: notes } = useNotes(options); diff --git a/apps/desktop/src/renderer/stores/__tests__/notebookExpandStore.test.ts b/apps/desktop/src/renderer/stores/__tests__/notebookExpandStore.test.ts new file mode 100644 index 00000000..5776bd15 --- /dev/null +++ b/apps/desktop/src/renderer/stores/__tests__/notebookExpandStore.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { useNotebookExpandStore } from '../notebookExpandStore'; + +describe('notebookExpandStore', () => { + beforeEach(() => { + useNotebookExpandStore.setState({ collapsedIds: [] }); + }); + + it('treats notebooks as expanded by default', () => { + expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(true); + }); + + it('toggles collapse', () => { + useNotebookExpandStore.getState().toggle('work'); + expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(false); + useNotebookExpandStore.getState().toggle('work'); + expect(useNotebookExpandStore.getState().isExpanded('work')).toBe(true); + }); +}); diff --git a/apps/desktop/src/renderer/stores/notebookExpandStore.ts b/apps/desktop/src/renderer/stores/notebookExpandStore.ts new file mode 100644 index 00000000..59c05d25 --- /dev/null +++ b/apps/desktop/src/renderer/stores/notebookExpandStore.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand'; + +/** Notebooks default to expanded. This set is the collapsed ones. */ +interface NotebookExpandStore { + collapsedIds: string[]; + isExpanded: (id: string) => boolean; + toggle: (id: string) => void; + expand: (id: string) => void; +} + +export const useNotebookExpandStore = create((set, get) => ({ + collapsedIds: [], + isExpanded: id => !get().collapsedIds.includes(id), + toggle: id => + set(state => ({ + collapsedIds: state.collapsedIds.includes(id) + ? state.collapsedIds.filter(item => item !== id) + : [...state.collapsedIds, id], + })), + expand: id => + set(state => ({ + collapsedIds: state.collapsedIds.filter(item => item !== id), + })), +})); diff --git a/apps/desktop/src/renderer/utils/__tests__/listOptionsFromNav.test.ts b/apps/desktop/src/renderer/utils/__tests__/listOptionsFromNav.test.ts index d59eaf64..55d9791b 100644 --- a/apps/desktop/src/renderer/utils/__tests__/listOptionsFromNav.test.ts +++ b/apps/desktop/src/renderer/utils/__tests__/listOptionsFromNav.test.ts @@ -98,6 +98,22 @@ describe('listOptionsFromNav', () => { }); }); + it('includes descendant notebooks when the selected parent is collapsed', () => { + expect( + listOptionsFromNav({ + navigation: { kind: 'notebook', id: 'work' }, + statusFilter: null, + tagFilter: null, + descendantNotebookIds: ['work', 'api', 'web'], + }) + ).toEqual({ + notebookIds: ['work', 'api', 'web'], + archived: 'active', + isDeleted: false, + limit: 10000, + }); + }); + it('scopes sidebar counts to the workspace tree', () => { expect( listOptionsFromNav({ diff --git a/apps/desktop/src/renderer/utils/listOptionsFromNav.ts b/apps/desktop/src/renderer/utils/listOptionsFromNav.ts index 1a3a3f01..381fd80b 100644 --- a/apps/desktop/src/renderer/utils/listOptionsFromNav.ts +++ b/apps/desktop/src/renderer/utils/listOptionsFromNav.ts @@ -20,6 +20,8 @@ export function listOptionsFromNav(input: { workspaceListAll?: boolean; /** Sidebar counts: always the workspace tree when focused. */ scopeToWorkspaceTree?: boolean; + /** Selected notebook subtree when that notebook is collapsed. */ + descendantNotebookIds?: string[]; }): ListOptions { const options: ListOptions = { ...optionsForNavigation(input.navigation, input), @@ -51,6 +53,7 @@ function optionsForNavigation( workspaceNotebookIds?: string[]; workspaceListAll?: boolean; scopeToWorkspaceTree?: boolean; + descendantNotebookIds?: string[]; } ): ListOptions { switch (navigation.kind) { @@ -80,6 +83,14 @@ function optionsForNavigation( isDeleted: false, }; } + const descendants = input.descendantNotebookIds; + if (descendants && descendants.length > 1) { + return { + notebookIds: descendants, + archived: 'active', + isDeleted: false, + }; + } return { notebookId: navigation.id, archived: 'active', diff --git a/docs/NOW.md b/docs/NOW.md index 3a56516c..6bad7908 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -75,6 +75,7 @@ Shipped this week: - Note templates: Meeting, Decision, Daily, Weekly, Reading, Issue (notebook `templates`). - What’s New is authored (`docs/releases/`). Promotion PRs: `chore(release): promote X.Y.Z`. See `docs/WHATS_NEW.md`. - Workspace view: click a notebook to focus the sidebar on that tree. Escape or the breadcrumb home returns to All Notes. +- Collapsed notebooks list notes from their descendants; expanded ones stay direct-only. CSS path (decided 2026-08-17): Keep CSS modules + tokens. Do not add Tailwind to desktop.