Skip to content

Commit e6c3c76

Browse files
conico974Nicolas DorseuilSamyPesse
authored
refactor toc components to client (#3394)
Co-authored-by: Nicolas Dorseuil <[email protected]> Co-authored-by: Samy Pessé <[email protected]>
1 parent 5134d9e commit e6c3c76

File tree

9 files changed

+189
-89
lines changed

9 files changed

+189
-89
lines changed

packages/gitbook/src/components/PageIcon/PageIcon.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { Icon, type IconName } from '@gitbook/icons';
44
import { Emoji } from '@/components/primitives';
55
import { type ClassValue, tcls } from '@/lib/tailwind';
66

7-
export function PageIcon(props: { page: RevisionPage; style?: ClassValue }) {
7+
export function PageIcon(props: {
8+
page: Pick<RevisionPage, 'emoji' | 'icon'>;
9+
style?: ClassValue;
10+
}) {
811
const { page, style } = props;
912

1013
if (page.emoji) {

packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,39 @@
1-
import type { GitBookSiteContext } from '@/lib/context';
2-
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
1+
'use client';
2+
33
import { tcls } from '@/lib/tailwind';
4-
import {
5-
type RevisionPage,
6-
type RevisionPageDocument,
7-
SiteInsightsLinkPosition,
8-
} from '@gitbook/api';
4+
import type { ClientTOCPageDocument } from './encodeClientTableOfContents';
95

6+
import { SiteInsightsLinkPosition } from '@gitbook/api';
107
import { PagesList } from './PagesList';
118
import { TOCPageIcon } from './TOCPageIcon';
129
import { ToggleableLinkItem } from './ToggleableLinkItem';
1310

14-
export async function PageDocumentItem(props: {
15-
rootPages: RevisionPage[];
16-
page: RevisionPageDocument;
17-
context: GitBookSiteContext;
18-
}) {
19-
const { rootPages, page, context } = props;
20-
let href = context.linker.toPathForPage({ pages: rootPages, page });
21-
// toPathForPage can returns an empty path, this will cause all links to point to the current page.
22-
if (href === '') {
23-
href = '/';
24-
}
11+
export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
12+
const { page } = props;
2513

2614
return (
2715
<li className="flex flex-col">
2816
<ToggleableLinkItem
29-
href={href}
30-
pathnames={getPagePaths(rootPages, page)}
17+
href={page.href ?? '#'}
18+
pathnames={page.pathnames}
3119
insights={{
3220
type: 'link_click',
3321
link: {
34-
target: {
35-
kind: 'page',
36-
page: page.id,
37-
},
22+
target: { kind: 'page', page: page.id },
3823
position: SiteInsightsLinkPosition.Sidebar,
3924
},
4025
}}
4126
descendants={
42-
hasPageVisibleDescendant(page) ? (
27+
page.descendants && page.descendants.length > 0 ? (
4328
<PagesList
44-
rootPages={rootPages}
45-
pages={page.pages}
29+
pages={page.descendants}
4630
style={tcls(
4731
'ml-5',
4832
'my-2',
4933
'border-tint-subtle',
5034
'sidebar-list-default:border-l',
5135
'sidebar-list-line:border-l'
5236
)}
53-
context={context}
5437
/>
5538
) : null
5639
}

packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
import type { GitBookSiteContext } from '@/lib/context';
2-
import type { RevisionPage, RevisionPageGroup } from '@gitbook/api';
1+
'use client';
2+
3+
import type { ClientTOCPageGroup } from './encodeClientTableOfContents';
34

4-
import { hasPageVisibleDescendant } from '@/lib/pages';
55
import { tcls } from '@/lib/tailwind';
66

77
import { PagesList } from './PagesList';
88
import { TOCPageIcon } from './TOCPageIcon';
99

10-
export function PageGroupItem(props: {
11-
rootPages: RevisionPage[];
12-
page: RevisionPageGroup;
13-
context: GitBookSiteContext;
14-
}) {
15-
const { rootPages, page, context } = props;
10+
export function PageGroupItem(props: { page: ClientTOCPageGroup }) {
11+
const { page } = props;
1612

1713
return (
1814
<li className="group/page-group-item flex flex-col">
@@ -36,8 +32,8 @@ export function PageGroupItem(props: {
3632
<TOCPageIcon page={page} />
3733
{page.title}
3834
</div>
39-
{hasPageVisibleDescendant(page) ? (
40-
<PagesList rootPages={rootPages} pages={page.pages} context={context} />
35+
{page.descendants && page.descendants.length > 0 ? (
36+
<PagesList pages={page.descendants} />
4137
) : null}
4238
</li>
4339
);

packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1-
import type { GitBookSiteContext } from '@/lib/context';
2-
import { type RevisionPageLink, SiteInsightsLinkPosition } from '@gitbook/api';
1+
'use client';
2+
33
import { Icon } from '@gitbook/icons';
4+
import type { ClientTOCPageLink } from './encodeClientTableOfContents';
45

56
import { Link } from '@/components/primitives';
6-
import { resolveContentRef } from '@/lib/references';
77
import { tcls } from '@/lib/tailwind';
88

9+
import { SiteInsightsLinkPosition } from '@gitbook/api';
910
import { TOCPageIcon } from './TOCPageIcon';
1011

11-
export async function PageLinkItem(props: { page: RevisionPageLink; context: GitBookSiteContext }) {
12-
const { page, context } = props;
13-
14-
const resolved = await resolveContentRef(page.target, context);
12+
export function PageLinkItem(props: { page: ClientTOCPageLink }) {
13+
const { page } = props;
1514

1615
return (
1716
<li className={tcls('flex', 'flex-col')}>
1817
<Link
19-
href={resolved?.href ?? '#'}
18+
href={page.href ?? '#'}
2019
classNames={['PageLinkItemStyles']}
2120
insights={{
2221
type: 'link_click',

packages/gitbook/src/components/TableOfContents/PagesList.tsx

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { GitBookSiteContext } from '@/lib/context';
2-
import type { RevisionPage } from '@gitbook/api';
1+
'use client';
2+
3+
import type { ClientTOCPage } from './encodeClientTableOfContents';
34

45
import { type ClassValue, tcls } from '@/lib/tailwind';
56

@@ -8,50 +9,21 @@ import { PageDocumentItem } from './PageDocumentItem';
89
import { PageGroupItem } from './PageGroupItem';
910
import { PageLinkItem } from './PageLinkItem';
1011

11-
export function PagesList(props: {
12-
context: GitBookSiteContext;
13-
rootPages: RevisionPage[];
14-
pages: RevisionPage[];
15-
style?: ClassValue;
16-
}) {
17-
const { rootPages, pages, context, style } = props;
12+
export function PagesList(props: { pages: ClientTOCPage[]; style?: ClassValue }) {
13+
const { pages, style } = props;
1814

1915
return (
2016
<ul className={tcls('flex flex-col gap-y-0.5', style)}>
2117
{pages.map((page) => {
22-
if (page.type === 'computed') {
23-
throw new Error(
24-
'Unexpected computed page, it should have been computed in the API'
25-
);
26-
}
27-
28-
if (page.hidden) {
29-
return null;
30-
}
31-
3218
switch (page.type) {
3319
case 'document':
34-
return (
35-
<PageDocumentItem
36-
key={page.id}
37-
rootPages={rootPages}
38-
page={page}
39-
context={context}
40-
/>
41-
);
20+
return <PageDocumentItem key={page.id} page={page} />;
4221

4322
case 'link':
44-
return <PageLinkItem key={page.id} page={page} context={context} />;
23+
return <PageLinkItem key={page.id} page={page} />;
4524

4625
case 'group':
47-
return (
48-
<PageGroupItem
49-
key={page.id}
50-
rootPages={rootPages}
51-
page={page}
52-
context={context}
53-
/>
54-
);
26+
return <PageGroupItem key={page.id} page={page} />;
5527

5628
default:
5729
assertNever(page);

packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PageIcon } from '../PageIcon';
77
/**
88
* Styled page icon for the table of contents.
99
*/
10-
export function TOCPageIcon({ page }: { page: RevisionPage }) {
10+
export function TOCPageIcon({ page }: { page: Pick<RevisionPage, 'emoji' | 'icon'> }) {
1111
return (
1212
<PageIcon
1313
page={page}

packages/gitbook/src/components/TableOfContents/TableOfContents.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import { PagesList } from './PagesList';
88
import { TOCScrollContainer } from './TOCScroller';
99
import { TableOfContentsScript } from './TableOfContentsScript';
1010
import { Trademark } from './Trademark';
11+
import { encodeClientTableOfContents } from './encodeClientTableOfContents';
1112

12-
export function TableOfContents(props: {
13+
export async function TableOfContents(props: {
1314
context: GitBookSiteContext;
1415
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
1516
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
1617
}) {
1718
const { innerHeader, context, header } = props;
1819
const { space, customization, revision } = context;
1920

21+
const pages = await encodeClientTableOfContents(context, revision.pages, revision.pages);
22+
2023
return (
2124
<>
2225
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
@@ -106,9 +109,7 @@ export function TableOfContents(props: {
106109
)}
107110
>
108111
<PagesList
109-
rootPages={revision.pages}
110-
pages={revision.pages}
111-
context={context}
112+
pages={pages}
112113
style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l"
113114
/>
114115
{customization.trademark.enabled ? (
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { GitBookSiteContext } from '@/lib/context';
2+
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
3+
import { resolveContentRef } from '@/lib/references';
4+
import { removeUndefined } from '@/lib/typescript';
5+
import type { ContentRef, RevisionPage } from '@gitbook/api';
6+
import assertNever from 'assert-never';
7+
8+
export type ClientTOCPageLink = {
9+
type: 'link';
10+
id: string;
11+
title: string;
12+
href: string;
13+
emoji?: string;
14+
icon?: string;
15+
target: ContentRef;
16+
};
17+
18+
export type ClientTOCPageDocument = {
19+
type: 'document';
20+
id: string;
21+
title: string;
22+
href: string;
23+
emoji?: string;
24+
icon?: string;
25+
pathnames: string[];
26+
descendants?: ClientTOCPage[];
27+
};
28+
29+
export type ClientTOCPageGroup = {
30+
type: 'group';
31+
id: string;
32+
title: string;
33+
emoji?: string;
34+
icon?: string;
35+
descendants?: ClientTOCPage[];
36+
};
37+
38+
export type ClientTOCPage = ClientTOCPageLink | ClientTOCPageDocument | ClientTOCPageGroup;
39+
40+
/**
41+
*
42+
* Encodes a table of contents for client components.
43+
* We do this to reduce the amount of data sent as RSC, we only send the encoded ClientTableOfContents once to a single client component.
44+
*/
45+
export async function encodeClientTableOfContents(
46+
context: GitBookSiteContext,
47+
rootPages: RevisionPage[],
48+
pages: RevisionPage[]
49+
): Promise<ClientTOCPage[]> {
50+
const result: ClientTOCPage[] = [];
51+
52+
for (const page of pages) {
53+
if (page.type === 'computed') {
54+
throw new Error('Unexpected computed page, it should have been computed in the API');
55+
}
56+
57+
if (page.hidden) {
58+
continue;
59+
}
60+
61+
switch (page.type) {
62+
case 'document': {
63+
let href = context.linker.toPathForPage({ pages: rootPages, page });
64+
if (href === '') {
65+
href = '/';
66+
}
67+
68+
const descendants = hasPageVisibleDescendant(page)
69+
? await encodeClientTableOfContents(context, rootPages, page.pages)
70+
: undefined;
71+
72+
result.push(
73+
removeUndefined({
74+
id: page.id,
75+
title: page.title,
76+
href,
77+
emoji: page.emoji,
78+
icon: page.icon,
79+
pathnames: getPagePaths(rootPages, page),
80+
descendants,
81+
type: 'document',
82+
})
83+
);
84+
break;
85+
}
86+
case 'link': {
87+
const resolved = await resolveContentRef(page.target, context);
88+
result.push(
89+
removeUndefined({
90+
id: page.id,
91+
title: page.title,
92+
href: resolved?.href ?? '#',
93+
emoji: page.emoji,
94+
icon: page.icon,
95+
target: page.target,
96+
type: 'link',
97+
})
98+
);
99+
break;
100+
}
101+
case 'group': {
102+
const descendants = hasPageVisibleDescendant(page)
103+
? await encodeClientTableOfContents(context, rootPages, page.pages)
104+
: undefined;
105+
106+
result.push(
107+
removeUndefined({
108+
id: page.id,
109+
title: page.title,
110+
emoji: page.emoji,
111+
icon: page.icon,
112+
descendants,
113+
type: 'group',
114+
})
115+
);
116+
break;
117+
}
118+
default:
119+
assertNever(page);
120+
}
121+
}
122+
123+
return result;
124+
}

0 commit comments

Comments
 (0)