diff --git a/src/App.test.tsx b/src/App.test.tsx index d689121..e623f96 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -169,6 +169,7 @@ describe('OneNoteApplication', () => { sourceSize: 1, entries: [], sections: [], + tree: { interpreted: false, nodes: [] }, diagnostics: [], }; render( @@ -237,11 +238,36 @@ describe('OneNoteApplication', () => { ], }, ], + tree: { + interpreted: true, + tocPath: 'Open Notebook.onetoc2', + nodes: [ + { + kind: 'section-group', + id: 'Notes', + displayName: 'Notes', + children: [ + { + kind: 'section', + id: 'readable', + sectionId: 'readable', + displayName: 'Readable', + }, + { + kind: 'section', + id: 'broken', + sectionId: 'broken', + displayName: 'Broken', + }, + ], + }, + ], + }, diagnostics: [ { severity: 'warning', - code: 'TOC_NOT_INTERPRETED', - message: 'Package path order is temporary.', + code: 'TEST_PACKAGE_WARNING', + message: 'Example recoverable package warning.', recoverable: true, }, ], @@ -263,10 +289,12 @@ describe('OneNoteApplication', () => { expect( await screen.findByRole('heading', { name: 'Package summary' }) ).toBeInTheDocument(); + expect(screen.getByText('Notes')).toBeVisible(); + expect(screen.getByText('OneNote notebook order')).toBeVisible(); expect( screen.getByRole('button', { name: /Broken, failed/i }) ).toBeVisible(); - expect(screen.getByText('TOC_NOT_INTERPRETED')).toBeInTheDocument(); + expect(screen.getByText('TEST_PACKAGE_WARNING')).toBeInTheDocument(); expect( await within( screen.getByRole('article', { name: 'First meeting' }) diff --git a/src/App.tsx b/src/App.tsx index 999b332..ed75957 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,9 @@ import { SingleSectionNavigation, } from './components/SectionNavigation.js'; import type { + OneNoteNotebookNodeDto, OneNotePackageDto, + OneNotePackagedSectionDto, OneNotePageDto, OneNoteSectionDto, } from './onenote/model/dto.js'; @@ -75,6 +77,32 @@ function uniqueDiagnostics( }); } +function sectionsInNotebookOrder( + notebook: OneNotePackageDto +): OneNotePackagedSectionDto[] { + const sectionById = new Map( + notebook.sections.map((section) => [section.id, section] as const) + ); + const ordered: OneNotePackagedSectionDto[] = []; + const seen = new Set(); + const visit = (nodes: OneNoteNotebookNodeDto[]) => { + for (const node of nodes) { + if (node.kind === 'section-group') { + visit(node.children); + continue; + } + const section = sectionById.get(node.sectionId); + if (section && !seen.has(section.id)) { + ordered.push(section); + seen.add(section.id); + } + } + }; + visit(notebook.tree.nodes); + ordered.push(...notebook.sections.filter((section) => !seen.has(section.id))); + return ordered; +} + export interface OneNoteApplicationProps { createWorkerClient?: () => OneNoteWorkerClient; } @@ -209,17 +237,18 @@ export function OneNoteApplication({ return; } + const orderedSections = sectionsInNotebookOrder(response.notebook); const preferredSection = - response.notebook.sections.find( + orderedSections.find( (section) => section.parseStatus === 'parsed' && section.section !== undefined && section.section.pages.length > 0 ) ?? - response.notebook.sections.find( + orderedSections.find( (section) => section.parseStatus === 'parsed' && section.section ) ?? - response.notebook.sections[0]; + orderedSections[0]; setSelectedSectionId(preferredSection?.id); setState({ phase: 'loaded', @@ -341,6 +370,7 @@ export function OneNoteApplication({ /> ) : ( { + it('turns authored page levels into parent/subpage relationships', () => { + expect( + buildPageTree([ + page('root', 1), + page('child', 2), + page('grandchild', 3), + page('next-root', 1), + page('skipped-level', 3), + ]) + ).toEqual([ + { + page: page('root', 1), + children: [ + { + page: page('child', 2), + children: [{ page: page('grandchild', 3), children: [] }], + }, + ], + }, + { + page: page('next-root', 1), + children: [{ page: page('skipped-level', 3), children: [] }], + }, + ]); + }); + + it('treats absent and invalid levels as top-level pages', () => { + expect( + buildPageTree([page('unset'), page('zero', 0), page('nan', Number.NaN)]) + ).toMatchObject([ + { page: { id: 'unset' }, children: [] }, + { page: { id: 'zero' }, children: [] }, + { page: { id: 'nan' }, children: [] }, + ]); + }); +}); diff --git a/src/components/SectionNavigation.tsx b/src/components/SectionNavigation.tsx index 4ae670e..ef8cc46 100644 --- a/src/components/SectionNavigation.tsx +++ b/src/components/SectionNavigation.tsx @@ -1,40 +1,69 @@ import type { + OneNoteNotebookNodeDto, + OneNoteNotebookTreeDto, OneNotePackagedSectionDto, OneNotePageSummaryDto, OneNoteSectionDto, } from '../onenote/model/dto.js'; +import { buildPageTree, type PageTreeNode } from './page-tree.js'; interface PageListProps { pages: OneNotePageSummaryDto[]; selectedPageId?: string; onSelectPage(pageId: string): void; + nested?: boolean; } -function PageList({ pages, selectedPageId, onSelectPage }: PageListProps) { +function PageTreeItems({ + nodes, + selectedPageId, + onSelectPage, +}: { + nodes: PageTreeNode[]; + selectedPageId?: string; + onSelectPage(pageId: string): void; +}) { + return nodes.map(({ page, children }) => ( +
  • + + {children.length > 0 ? ( +
      + +
    + ) : null} +
  • + )); +} + +function PageList({ + pages, + selectedPageId, + onSelectPage, + nested = false, +}: PageListProps) { if (pages.length === 0) { return

    No pages were extracted.

    ; } return ( -