mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-29 05:29:02 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8cee669f | |||
| 30e9be6d15 | |||
| b869c9c329 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Support buttons that open an integration's UI, so an integration can be used in places an integration block cannot go, such as a table cell.
|
||||
@@ -7,10 +7,13 @@ import { Button, type ButtonProps } from '../primitives';
|
||||
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import type { InlineProps } from './Inline';
|
||||
import { InlineActionButton } from './InlineActionButton';
|
||||
import { getIntegrationAction } from './integrationAction';
|
||||
import { IntegrationActionButton } from './IntegrationActionButton';
|
||||
import { NotFoundRefHoverCard } from './NotFoundRefHoverCard';
|
||||
import { getSelectAction } from './selectAction';
|
||||
import { SelectActionButton } from './SelectActionButton';
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import { GITBOOK_INTEGRATIONS_CONTENT_HOST, GITBOOK_INTEGRATIONS_HOST } from '@/lib/env';
|
||||
import { resolveContentRefFallback, resolveContentRefInDocument } from '@/lib/references';
|
||||
|
||||
// Editor button sizes render one step smaller here; the editor default (`large`) keeps the previous `medium`.
|
||||
@@ -41,6 +44,29 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
return <SelectActionButton value={selectAction.value} buttonProps={buttonProps} />;
|
||||
}
|
||||
|
||||
// Skip in print/PDF: the integration renders into a dialog, which a static render can't show.
|
||||
const integrationAction =
|
||||
context.mode !== 'print' ? getIntegrationAction(inline.data) : null;
|
||||
const spaceId = context.contentContext?.space?.id;
|
||||
if (integrationAction && spaceId) {
|
||||
return (
|
||||
<IntegrationActionButton
|
||||
integration={integrationAction.integration}
|
||||
block={integrationAction.block}
|
||||
spaceId={spaceId}
|
||||
security={{
|
||||
firstPartyDomains: [
|
||||
...new Set([
|
||||
GITBOOK_INTEGRATIONS_HOST,
|
||||
GITBOOK_INTEGRATIONS_CONTENT_HOST,
|
||||
]),
|
||||
],
|
||||
}}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// In print/PDF mode, skip interactive action buttons (AI/search providers are not mounted).
|
||||
if (context.mode !== 'print' && 'action' in inline.data && 'query' in inline.data.action) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { ContentKitRenderOutputElement, RequestRenderIntegrationUI } from '@gitbook/api';
|
||||
import { ContentKit, type ContentKitSecurity } from '@gitbook/react-contentkit/client';
|
||||
|
||||
import { Button, type ButtonProps } from '../primitives';
|
||||
import { renderIntegrationUi } from './Integration/server-actions';
|
||||
|
||||
/**
|
||||
* Button that hands the click to an integration: the integration renders its component in modal
|
||||
* mode and decides what the reader sees. This is how an integration reaches places an integration
|
||||
* block cannot go, such as a table cell.
|
||||
*/
|
||||
export function IntegrationActionButton(props: {
|
||||
integration: string;
|
||||
block: string;
|
||||
spaceId: string;
|
||||
security: ContentKitSecurity;
|
||||
buttonProps: ButtonProps;
|
||||
}) {
|
||||
const { integration, block, spaceId, security, buttonProps } = props;
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [modal, setModal] = React.useState<null | {
|
||||
input: RequestRenderIntegrationUI;
|
||||
output: ContentKitRenderOutputElement;
|
||||
children: React.ReactNode;
|
||||
}>(null);
|
||||
|
||||
const renderContext = React.useMemo(() => ({ integrationName: integration }), [integration]);
|
||||
|
||||
const onClick = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const input: RequestRenderIntegrationUI = {
|
||||
componentId: block,
|
||||
props: {},
|
||||
context: {
|
||||
type: 'document',
|
||||
spaceId,
|
||||
editable: false,
|
||||
theme: 'light', // Same limitation as the integration block: rendering is server-side.
|
||||
},
|
||||
};
|
||||
|
||||
const result = await renderIntegrationUi({ renderContext, request: input });
|
||||
// Anything but a modal has no place to go here: a `block` would render loose next to
|
||||
// the button, and a `complete` means the integration handled the click on its own.
|
||||
if (result.output?.type === 'element' && result.output.element.type === 'modal') {
|
||||
setModal({ input, output: result.output, children: result.children });
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button {...buttonProps} disabled={loading} onClick={onClick} />
|
||||
{modal ? (
|
||||
<ContentKit
|
||||
renderContext={renderContext}
|
||||
security={security}
|
||||
initialInput={modal.input}
|
||||
initialOutput={modal.output}
|
||||
render={renderIntegrationUi}
|
||||
onAction={(action) => {
|
||||
if (action.action === '@ui.modal.close') {
|
||||
setModal(null);
|
||||
}
|
||||
}}
|
||||
onComplete={() => setModal(null)}
|
||||
>
|
||||
{modal.children}
|
||||
</ContentKit>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type * as api from '@gitbook/api';
|
||||
|
||||
/**
|
||||
* Detect an "integration" action on a button's data, returning the integration and the component
|
||||
* it targets, or `null` when the button is not an integration action.
|
||||
*/
|
||||
export function getIntegrationAction(data: api.DocumentInlineButton['data']) {
|
||||
if (!('action' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: drop the cast once `@gitbook/api` ships the `integration` variant of `DocumentAction`.
|
||||
const action = data.action as { action: string; integration?: unknown; block?: unknown };
|
||||
if (
|
||||
action.action === 'integration' &&
|
||||
typeof action.integration === 'string' &&
|
||||
typeof action.block === 'string' &&
|
||||
action.integration &&
|
||||
action.block
|
||||
) {
|
||||
return { integration: action.integration, block: action.block };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// Client-safe entrypoint: avoid pulling server rendering exports into Client Components.
|
||||
export { ContentKit } from './ContentKit';
|
||||
export type { ContentKitClientContextData } from './context';
|
||||
export type { ContentKitClientContextData, ContentKitSecurity } from './context';
|
||||
|
||||
Reference in New Issue
Block a user