mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-08 14:20:55 +00:00
Allow configuring the tab and page the Docs Embed opens on (RND-12456) (#4669)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/embed": minor
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add `defaultTab` and `defaultPage` options to the Docs Embed to choose where it opens. `defaultTab` opens the embed on the assistant, search or docs tab. `defaultPage` sets the page the docs tab opens on in place of the site's home page, and the embed opens on that page unless `defaultTab` says otherwise. Both work with the standalone script's `configure`, `frame.configure` and `<GitBookFrame>` props.
|
||||
@@ -54,6 +54,7 @@ GitBook('configure', {
|
||||
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||
},
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -107,6 +108,7 @@ frame.clearChat();
|
||||
// Configure the embed (see Configuration section for all options)
|
||||
frame.configure({
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -142,6 +144,7 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
}}
|
||||
tabs={['assistant', 'search', 'docs']}
|
||||
defaultPage="/getting-started"
|
||||
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||
assistantName="Support Assistant"
|
||||
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||
@@ -249,6 +252,33 @@ Override which tabs are displayed. Defaults to your site's configuration.
|
||||
tabs: ['assistant', 'search', 'docs']
|
||||
```
|
||||
|
||||
### `defaultTab`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The tab the embed opens on. Without it, the embed opens on `defaultPage` if set, otherwise on the assistant (or the docs when the assistant isn't available). The tab must be one of the enabled `tabs`.
|
||||
|
||||
- **Type**: `'assistant' | 'search' | 'docs'`
|
||||
|
||||
```javascript
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultTab: 'search'
|
||||
```
|
||||
|
||||
`defaultTab` and `defaultPage` apply when the embed first loads: the standalone widget keeps its place when it is closed and reopened. To move it later, use `navigateToPage` or `navigateToAssistant`.
|
||||
|
||||
### `defaultPage`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
The page the docs tab opens on, in place of your site's home page. Accepts the same references as `navigateToPage`: the page's path within the site, an absolute path, or its full published URL. Unless `defaultTab` says otherwise, the embed opens on this page.
|
||||
|
||||
- **Type**: `string`
|
||||
|
||||
```javascript
|
||||
defaultPage: '/getting-started/quickstart'
|
||||
```
|
||||
|
||||
### `closeButton`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
@@ -62,6 +62,12 @@ export type GitBookEmbeddableConfiguration = {
|
||||
/** Tabs to display in the embed (if enabled on the site). */
|
||||
tabs: ('assistant' | 'docs' | 'search')[];
|
||||
|
||||
/** Tab to open the embed on. */
|
||||
defaultTab?: 'assistant' | 'docs' | 'search';
|
||||
|
||||
/** Page to open the docs tab on, instead of the site's home page. */
|
||||
defaultPage?: string;
|
||||
|
||||
/** Additional buttons to be displayed in the header of the GitBook embed. */
|
||||
actions: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'search', 'docs'],
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
trademark = true,
|
||||
closeButton = false,
|
||||
assistantName,
|
||||
@@ -50,6 +52,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
@@ -65,6 +69,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions,
|
||||
tools,
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
import { Button, LinkContext, type LinkContextType } from '../primitives';
|
||||
import { getChannel } from './channel';
|
||||
import { DEFAULT_EMBEDDABLE_TABS, getEmbeddableLandingTab } from './getEmbeddableLandingTab';
|
||||
import { resolveEmbedPageLink } from './server-actions';
|
||||
import { useAI, useAIChatController } from '@/components/AI';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
@@ -23,6 +24,17 @@ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() =
|
||||
trademark: true,
|
||||
}));
|
||||
|
||||
const docsHome = createStore<{ reference?: string; href?: string }>(() => ({}));
|
||||
|
||||
// Module-level so it survives the layout remounting on a cross-space navigation.
|
||||
let landed = false;
|
||||
|
||||
function resolvePageHref(pagePath: string, baseURL: string): Promise<string> {
|
||||
return resolveEmbedPageLink(pagePath)
|
||||
.then((resolved) => ('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`))
|
||||
.catch(() => `${baseURL}/page/${pagePath}`);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
function log(...data: any[]) {
|
||||
// oxlint-disable-next-line no-console
|
||||
@@ -66,6 +78,15 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
const message = payload as ParentToFrameMessage;
|
||||
|
||||
const navigate = (href: Promise<string>) => {
|
||||
const token = ++navToken.current;
|
||||
href.then((href) => {
|
||||
if (navToken.current === token) {
|
||||
refs.current.router.push(href);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
log('[gitbook] received message', message);
|
||||
|
||||
switch (message.type) {
|
||||
@@ -83,29 +104,43 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
break;
|
||||
}
|
||||
case 'configure': {
|
||||
embeddableConfiguration.setState(message.settings);
|
||||
const { settings } = message;
|
||||
embeddableConfiguration.setState(settings);
|
||||
integrationsAssistantTools.setState({
|
||||
tools: message.settings.tools,
|
||||
tools: settings.tools,
|
||||
});
|
||||
|
||||
const defaultPage =
|
||||
typeof settings.defaultPage === 'string' && settings.defaultPage
|
||||
? settings.defaultPage
|
||||
: undefined;
|
||||
let docsHomeHref: Promise<string> | undefined;
|
||||
if (defaultPage !== docsHome.getState().reference) {
|
||||
docsHome.setState({ reference: defaultPage, href: undefined });
|
||||
if (defaultPage) {
|
||||
docsHomeHref = resolvePageHref(defaultPage, baseURL);
|
||||
docsHomeHref.then((href) => {
|
||||
if (docsHome.getState().reference === defaultPage) {
|
||||
docsHome.setState({ href });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!landed) {
|
||||
landed = true;
|
||||
const tab = getEmbeddableLandingTab(settings);
|
||||
if (tab === 'docs') {
|
||||
navigate(docsHomeHref ?? Promise.resolve(`${baseURL}/page/`));
|
||||
} else if (tab) {
|
||||
navToken.current++;
|
||||
router.push(`${baseURL}/${tab}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'navigateToPage': {
|
||||
// Resolve the target server-side: on a multi-space site the page may
|
||||
// live in another space/section, whose base must go before
|
||||
// `~gitbook/embed/page`. Fall back to a same-space push on failure.
|
||||
const { pagePath } = message;
|
||||
const token = ++navToken.current;
|
||||
// Ignore the result if a later navigation has since superseded this one.
|
||||
const push = (href: string) => {
|
||||
if (navToken.current === token) {
|
||||
router.push(href);
|
||||
}
|
||||
};
|
||||
resolveEmbedPageLink(pagePath)
|
||||
.then((resolved) =>
|
||||
push('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`)
|
||||
)
|
||||
.catch(() => push(`${baseURL}/page/${pagePath}`));
|
||||
navigate(resolvePageHref(message.pagePath, baseURL));
|
||||
break;
|
||||
}
|
||||
case 'navigateToAssistant': {
|
||||
@@ -132,7 +167,7 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
||||
|
||||
export function useEmbeddableTabs() {
|
||||
const configuredTabs = useEmbeddableConfiguration((state) => state.tabs);
|
||||
return configuredTabs.length > 0 ? configuredTabs : ['assistant', 'search', 'docs'];
|
||||
return configuredTabs.length > 0 ? configuredTabs : DEFAULT_EMBEDDABLE_TABS;
|
||||
}
|
||||
|
||||
export function useEmbeddableLinkContext() {
|
||||
@@ -200,6 +235,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props;
|
||||
const actions = useEmbeddableConfiguration((state) => state.actions);
|
||||
const tabs = useEmbeddableTabs();
|
||||
const docsHomeHref = useStore(docsHome, (state) => state.href);
|
||||
|
||||
const { assistants, config } = useAI();
|
||||
const language = useLanguage();
|
||||
@@ -228,7 +264,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
key: 'docs',
|
||||
label: siteTitle,
|
||||
icon: 'book-open',
|
||||
href: `${baseURL}/page/`,
|
||||
href: docsHomeHref ?? `${baseURL}/page/`,
|
||||
}
|
||||
: null,
|
||||
].filter((tab) => tab !== null);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { getEmbeddableLandingTab } from './getEmbeddableLandingTab';
|
||||
|
||||
describe('getEmbeddableLandingTab', () => {
|
||||
it('keeps the embed default when nothing is configured', () => {
|
||||
expect(getEmbeddableLandingTab({})).toBeNull();
|
||||
expect(getEmbeddableLandingTab({ tabs: ['docs', 'search'] })).toBeNull();
|
||||
});
|
||||
|
||||
it('opens on the configured tab', () => {
|
||||
expect(getEmbeddableLandingTab({ defaultTab: 'search' })).toBe('search');
|
||||
expect(
|
||||
getEmbeddableLandingTab({ tabs: ['assistant', 'search', 'docs'], defaultTab: 'search' })
|
||||
).toBe('search');
|
||||
});
|
||||
|
||||
it('opens on the docs when only a default page is set', () => {
|
||||
expect(getEmbeddableLandingTab({ defaultPage: 'getting-started' })).toBe('docs');
|
||||
});
|
||||
|
||||
it('prefers the configured tab over the default page', () => {
|
||||
expect(
|
||||
getEmbeddableLandingTab({ defaultTab: 'assistant', defaultPage: 'getting-started' })
|
||||
).toBe('assistant');
|
||||
});
|
||||
|
||||
it('ignores a tab that is not enabled', () => {
|
||||
expect(getEmbeddableLandingTab({ tabs: ['assistant'], defaultTab: 'search' })).toBeNull();
|
||||
expect(
|
||||
getEmbeddableLandingTab({ tabs: ['assistant', 'search'], defaultPage: 'quickstart' })
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores an unknown tab', () => {
|
||||
// @ts-expect-error - the parent window is plain JS and can send anything.
|
||||
expect(getEmbeddableLandingTab({ defaultTab: 'settings' })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { GitBookEmbeddableConfiguration } from '@gitbook/embed';
|
||||
|
||||
export type EmbeddableTab = GitBookEmbeddableConfiguration['tabs'][number];
|
||||
|
||||
export const DEFAULT_EMBEDDABLE_TABS: EmbeddableTab[] = ['assistant', 'search', 'docs'];
|
||||
|
||||
export function getEmbeddableLandingTab(
|
||||
settings: Partial<Pick<GitBookEmbeddableConfiguration, 'tabs' | 'defaultTab' | 'defaultPage'>>
|
||||
): EmbeddableTab | null {
|
||||
const tab = settings.defaultTab ?? (settings.defaultPage ? 'docs' : undefined);
|
||||
if (!tab || !DEFAULT_EMBEDDABLE_TABS.includes(tab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = settings.tabs?.length ? settings.tabs : DEFAULT_EMBEDDABLE_TABS;
|
||||
return tabs.includes(tab) ? tab : null;
|
||||
}
|
||||
Reference in New Issue
Block a user