mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 23:55:20 +00:00
Support OpenAPI one-operation-per-page mode (#4314)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/react-openapi": patch
|
||||||
|
"gitbook": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support OpenAPI one-operation-per-page mode
|
||||||
@@ -54,6 +54,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
|||||||
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
||||||
expandAllModelSections:
|
expandAllModelSections:
|
||||||
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
||||||
|
headless: block.type === 'openapi-operation' ? block.data.headless : undefined,
|
||||||
})}
|
})}
|
||||||
className="openapi-block"
|
className="openapi-block"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
|
|||||||
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
|
||||||
expandAllModelSections:
|
expandAllModelSections:
|
||||||
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
|
||||||
|
headless: block.data.headless,
|
||||||
})}
|
})}
|
||||||
className="openapi-block"
|
className="openapi-block"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ export function getOpenAPIContext(args: {
|
|||||||
context: GitBookAnyContext | undefined;
|
context: GitBookAnyContext | undefined;
|
||||||
expandAllResponses?: boolean;
|
expandAllResponses?: boolean;
|
||||||
expandAllModelSections?: boolean;
|
expandAllModelSections?: boolean;
|
||||||
|
headless?: boolean;
|
||||||
}): OpenAPIContextInput {
|
}): OpenAPIContextInput {
|
||||||
const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args;
|
const { props, specUrl, context, expandAllResponses, expandAllModelSections, headless } = args;
|
||||||
const { block } = props;
|
const { block } = props;
|
||||||
|
|
||||||
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
|
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
|
||||||
@@ -96,6 +97,7 @@ export function getOpenAPIContext(args: {
|
|||||||
),
|
),
|
||||||
expandAllResponses: expandAllResponses || props.context.mode === 'print',
|
expandAllResponses: expandAllResponses || props.context.mode === 'print',
|
||||||
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
|
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
|
||||||
|
headless,
|
||||||
id: block.meta?.id,
|
id: block.meta?.id,
|
||||||
blockKey: block.key,
|
blockKey: block.key,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -101,7 +101,17 @@
|
|||||||
/* Method Tags */
|
/* Method Tags */
|
||||||
.openapi-method,
|
.openapi-method,
|
||||||
.openapi-statuscode {
|
.openapi-statuscode {
|
||||||
@apply rounded-md straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold text-[0.813rem] px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
|
@apply m-0 h-5 min-w-9 justify-center rounded-md text-xs straight-corners:rounded-none circular-corners:rounded-lg uppercase font-mono items-center shrink-0 font-semibold px-1.5 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-method-small {}
|
||||||
|
|
||||||
|
.openapi-method-medium {
|
||||||
|
@apply m-0 px-2.5 py-1 h-6 text-[0.813rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.toclink .openapi-method {
|
||||||
|
@apply text-[0.625rem] flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-method-get,
|
.openapi-method-get,
|
||||||
@@ -130,7 +140,8 @@
|
|||||||
|
|
||||||
.openapi-method-head,
|
.openapi-method-head,
|
||||||
.openapi-method-options,
|
.openapi-method-options,
|
||||||
.openapi-method-trace {
|
.openapi-method-trace,
|
||||||
|
.openapi-method-hook {
|
||||||
@apply bg-tint;
|
@apply bg-tint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,16 +461,12 @@
|
|||||||
@apply max-w-full overflow-hidden shrink pl-0.5 py-0.5;
|
@apply max-w-full overflow-hidden shrink pl-0.5 py-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-response-examples-header .openapi-select > button .openapi-statuscode {
|
|
||||||
@apply h-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-codesample-header-content {
|
.openapi-codesample-header-content {
|
||||||
@apply flex flex-row items-center justify-between h-fit p-2.5;
|
@apply flex flex-row items-center justify-between h-fit p-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-codesample-header-content .openapi-path {
|
.openapi-codesample-header-content .openapi-path {
|
||||||
@apply flex items-center font-mono text-[0.813rem] gap-1 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
|
@apply flex items-center font-mono text-[0.813rem] gap-2 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-codesample-header-content .openapi-path-title {
|
.openapi-codesample-header-content .openapi-path-title {
|
||||||
@@ -493,10 +500,6 @@
|
|||||||
@apply text-tint inline;
|
@apply text-tint inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-summary .openapi-path .openapi-method {
|
|
||||||
@apply m-0 items-center flex px-2 py-1 h-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-path-title {
|
.openapi-path-title {
|
||||||
@apply flex-1 relative font-normal items-center gap-y-1 flex flex-wrap text-left overflow-x-auto font-mono text-tint-strong/10;
|
@apply flex-1 relative font-normal items-center gap-y-1 flex flex-wrap text-left overflow-x-auto font-mono text-tint-strong/10;
|
||||||
@apply whitespace-nowrap md:whitespace-normal;
|
@apply whitespace-nowrap md:whitespace-normal;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { DocumentSection } from '@/lib/document-sections';
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { useBodyLoaded } from '@/components/primitives';
|
import { useBodyLoaded } from '@/components/primitives';
|
||||||
|
import { OpenAPIMethodBadge } from '@gitbook/react-openapi';
|
||||||
import { HEADER_HEIGHT_DESKTOP } from '../layout';
|
import { HEADER_HEIGHT_DESKTOP } from '../layout';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,11 +142,11 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{section.tag ? (
|
{section.tag ? (
|
||||||
<span
|
<OpenAPIMethodBadge
|
||||||
className={`-mt-0.5 openapi-method text-xs! openapi-method-${section.tag.toLowerCase()}`}
|
method={section.tag}
|
||||||
>
|
size="small"
|
||||||
{section.tag}
|
className="-mt-0.5"
|
||||||
</span>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
import { SiteInsightsLinkPosition } from '@gitbook/api';
|
import { SiteInsightsLinkPosition } from '@gitbook/api';
|
||||||
|
import { OpenAPIMethodBadge } from '@gitbook/react-openapi';
|
||||||
import { Tag } from '../Tag';
|
import { Tag } from '../Tag';
|
||||||
import { PagesList } from './PagesList';
|
import { PagesList } from './PagesList';
|
||||||
import { TOCPageIcon } from './TOCPageIcon';
|
import { TOCPageIcon } from './TOCPageIcon';
|
||||||
@@ -41,7 +42,18 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
|
|||||||
icon={<TOCPageIcon page={page} />}
|
icon={<TOCPageIcon page={page} />}
|
||||||
tag={page.primaryTag ? <Tag tag={page.primaryTag} /> : null}
|
tag={page.primaryTag ? <Tag tag={page.primaryTag} /> : null}
|
||||||
>
|
>
|
||||||
{page.title}
|
{page.openAPIOperation ? (
|
||||||
|
<span className="flex h-[1lh] shrink-0 items-center self-center">
|
||||||
|
<OpenAPIMethodBadge
|
||||||
|
method={page.openAPIOperation.method}
|
||||||
|
short
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className={page.openAPIOperation?.deprecated ? 'line-through' : undefined}>
|
||||||
|
{page.title}
|
||||||
|
</span>
|
||||||
</ToggleableLinkItem>
|
</ToggleableLinkItem>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GitBookSiteContext } from '@/lib/context';
|
import type { GitBookSiteContext } from '@/lib/context';
|
||||||
|
import { getOpenAPIOperationPageProps } from '@/lib/openapi/computedSourceProps';
|
||||||
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
|
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
|
||||||
import { resolveContentRef } from '@/lib/references';
|
import { resolveContentRef } from '@/lib/references';
|
||||||
import { getRevisionTags, resolveTag } from '@/lib/tags';
|
import { getRevisionTags, resolveTag } from '@/lib/tags';
|
||||||
@@ -26,6 +27,7 @@ export type ClientTOCPageDocument = {
|
|||||||
pathnames: string[];
|
pathnames: string[];
|
||||||
descendants?: ClientTOCPage[];
|
descendants?: ClientTOCPage[];
|
||||||
primaryTag?: RevisionTag;
|
primaryTag?: RevisionTag;
|
||||||
|
openAPIOperation?: { method: string; deprecated: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientTOCPageGroup = {
|
export type ClientTOCPageGroup = {
|
||||||
@@ -83,6 +85,7 @@ export async function encodeClientTableOfContents(
|
|||||||
pathnames: getPagePaths(rootPages, page),
|
pathnames: getPagePaths(rootPages, page),
|
||||||
descendants,
|
descendants,
|
||||||
primaryTag,
|
primaryTag,
|
||||||
|
openAPIOperation: getOpenAPIOperationPageProps(page),
|
||||||
type: 'document',
|
type: 'document',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const ToCItemBaseStyles = [
|
|||||||
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
|
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
|
||||||
'focus-visible:-outline-offset-2',
|
'focus-visible:-outline-offset-2',
|
||||||
'before:contents[] before:-left-px before:absolute before:inset-y-0',
|
'before:contents[] before:-left-px before:absolute before:inset-y-0',
|
||||||
'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px',
|
'sidebar-list-line:rounded-l-none! sidebar-list-line:before:w-px [&+div_a]:sidebar-list-default:rounded-l-none has-[.openapi-method]:pl-3 [&+div_a]:pl-5 [&+div_a]:sidebar-list-default:before:w-px',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ToCLinkItemStyles = [
|
export const ToCLinkItemStyles = [
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ComputedContentSourceDocumentOpenAPI, RevisionPageDocument } from '@gitbook/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the method and deprecated state of a page generated by the OpenAPI
|
||||||
|
* computed source in one-operation-per-page mode.
|
||||||
|
*/
|
||||||
|
export function getOpenAPIOperationPageProps(
|
||||||
|
page: RevisionPageDocument
|
||||||
|
): { method: string; deprecated: boolean } | undefined {
|
||||||
|
if (!('computed' in page)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = page.computed;
|
||||||
|
if (source.type !== 'builtin:openapi') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { props } = source as ComputedContentSourceDocumentOpenAPI;
|
||||||
|
if (props.doc !== 'operation') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: props.method,
|
||||||
|
deprecated: props.deprecated === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -33,7 +33,13 @@ function OpenAPICodeSampleHeader(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenAPIPath context={context} canCopy={false} withServer={false} data={data} />
|
<OpenAPIPath
|
||||||
|
context={context}
|
||||||
|
canCopy={false}
|
||||||
|
withServer={false}
|
||||||
|
data={data}
|
||||||
|
methodBadgeSize="small"
|
||||||
|
/>
|
||||||
{items.length > 1 ? (
|
{items.length > 1 ? (
|
||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
icon={selectIcon}
|
icon={selectIcon}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import clsx from 'classnames';
|
||||||
|
import { formatOpenAPIMethod } from './formatOpenAPIMethod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Badge displaying the HTTP method of an OpenAPI operation.
|
||||||
|
*/
|
||||||
|
export function OpenAPIMethodBadge(props: {
|
||||||
|
method: string;
|
||||||
|
/** Abbreviate the label (DEL, OPTS) instead of the full method. */
|
||||||
|
short?: boolean;
|
||||||
|
size?: 'small' | 'medium';
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const { method, short = false, size = 'medium', className } = props;
|
||||||
|
const normalized = method.toLowerCase();
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'openapi-method',
|
||||||
|
`openapi-method-${normalized}`,
|
||||||
|
`openapi-method-${size}`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{short ? formatOpenAPIMethod(normalized) : method}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,6 +16,11 @@ export type OpenAPIPathProps = {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
|
/**
|
||||||
|
* The size of the HTTP method badge.
|
||||||
|
* @default 'medium'
|
||||||
|
*/
|
||||||
|
methodBadgeSize?: 'small' | 'medium';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||||
|
import { OpenAPIMethodBadge } from './OpenAPIMethodBadge';
|
||||||
import type { OpenAPIPathProps } from './OpenAPIPath';
|
import type { OpenAPIPathProps } from './OpenAPIPath';
|
||||||
import type { OpenAPIClientContext } from './context';
|
import type { OpenAPIClientContext } from './context';
|
||||||
|
|
||||||
@@ -10,14 +11,22 @@ export function OpenAPIPathItem(
|
|||||||
context: OpenAPIClientContext;
|
context: OpenAPIClientContext;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { value, canCopy = true, context, children, data, copyType = 'children' } = props;
|
const {
|
||||||
|
value,
|
||||||
|
canCopy = true,
|
||||||
|
context,
|
||||||
|
children,
|
||||||
|
data,
|
||||||
|
copyType = 'children',
|
||||||
|
methodBadgeSize = 'medium',
|
||||||
|
} = props;
|
||||||
const { operation, method } = data;
|
const { operation, method } = data;
|
||||||
|
|
||||||
const title = <span className="openapi-path-title">{children}</span>;
|
const title = <span className="openapi-path-title">{children}</span>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-path">
|
<div className="openapi-path">
|
||||||
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
<OpenAPIMethodBadge method={method} size={methodBadgeSize} />
|
||||||
{canCopy && value ? (
|
{canCopy && value ? (
|
||||||
copyType === 'children' ? (
|
copyType === 'children' ? (
|
||||||
<OpenAPICopyButton
|
<OpenAPICopyButton
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export function OpenAPIColumnSpec(props: {
|
|||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<OpenAPIOperationDescription operation={operation} context={context} />
|
{!context.headless ? (
|
||||||
|
<OpenAPIOperationDescription operation={operation} context={context} />
|
||||||
|
) : null}
|
||||||
<OpenAPISpec data={data} context={clientContext} />
|
<OpenAPISpec data={data} context={clientContext} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ export function OpenAPISummary(props: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
|
<div
|
||||||
|
className="openapi-summary"
|
||||||
|
id={!context.headless && operation.summary ? undefined : context.id}
|
||||||
|
>
|
||||||
{(operation.deprecated || operation['x-stability']) && (
|
{(operation.deprecated || operation['x-stability']) && (
|
||||||
<div className="openapi-summary-tags">
|
<div className="openapi-summary-tags">
|
||||||
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
||||||
@@ -32,7 +35,7 @@ export function OpenAPISummary(props: {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{title
|
{!context.headless && title
|
||||||
? context.renderHeading({
|
? context.renderHeading({
|
||||||
deprecated: operation.deprecated ?? false,
|
deprecated: operation.deprecated ?? false,
|
||||||
stability: operation['x-stability'],
|
stability: operation['x-stability'],
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ export interface OpenAPIContext
|
|||||||
*/
|
*/
|
||||||
renderDocument: (props: { document: object }) => React.ReactNode;
|
renderDocument: (props: { document: object }) => React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `true`, the block omits its own title and description (the page renders them instead).
|
||||||
|
*/
|
||||||
|
headless?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public specification URL, used by Scalar's "Test it" modal.
|
* Public specification URL, used by Scalar's "Test it" modal.
|
||||||
* When null, the "Test it" button is hidden.
|
* When null, the "Test it" button is hidden.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Format an HTTP method for display in a method badge.
|
||||||
|
* Uppercasing is left to the `.openapi-method` CSS.
|
||||||
|
*/
|
||||||
|
export function formatOpenAPIMethod(method: string): string {
|
||||||
|
switch (method) {
|
||||||
|
case 'delete':
|
||||||
|
return 'DEL';
|
||||||
|
case 'options':
|
||||||
|
return 'OPTS';
|
||||||
|
default:
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from './schemas';
|
export * from './schemas';
|
||||||
|
export * from './formatOpenAPIMethod';
|
||||||
|
export * from './OpenAPIMethodBadge';
|
||||||
export * from './OpenAPIOperation';
|
export * from './OpenAPIOperation';
|
||||||
export * from './OpenAPIWebhook';
|
export * from './OpenAPIWebhook';
|
||||||
export * from './OpenAPIOperationContext';
|
export * from './OpenAPIOperationContext';
|
||||||
|
|||||||
Reference in New Issue
Block a user