Support OpenAPI one-operation-per-page mode (#4314)

This commit is contained in:
Nolann B.
2026-06-19 10:35:09 +02:00
committed by GitHub
parent 91b325ca08
commit f4469b2df3
19 changed files with 156 additions and 25 deletions
+6
View File
@@ -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'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
headless: block.type === 'openapi-operation' ? block.data.headless : undefined,
})}
className="openapi-block"
/>
@@ -56,6 +56,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
headless: block.data.headless,
})}
className="openapi-block"
/>
@@ -26,8 +26,9 @@ export function getOpenAPIContext(args: {
context: GitBookAnyContext | undefined;
expandAllResponses?: boolean;
expandAllModelSections?: boolean;
headless?: boolean;
}): OpenAPIContextInput {
const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args;
const { props, specUrl, context, expandAllResponses, expandAllModelSections, headless } = args;
const { block } = props;
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
@@ -96,6 +97,7 @@ export function getOpenAPIContext(args: {
),
expandAllResponses: expandAllResponses || props.context.mode === 'print',
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
headless,
id: block.meta?.id,
blockKey: block.key,
locale,
@@ -101,7 +101,17 @@
/* Method Tags */
.openapi-method,
.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,
@@ -130,7 +140,8 @@
.openapi-method-head,
.openapi-method-options,
.openapi-method-trace {
.openapi-method-trace,
.openapi-method-hook {
@apply bg-tint;
}
@@ -450,16 +461,12 @@
@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 {
@apply flex flex-row items-center justify-between h-fit p-2.5;
}
.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 {
@@ -493,10 +500,6 @@
@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 {
@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;
@@ -7,6 +7,7 @@ import type { DocumentSection } from '@/lib/document-sections';
import { tcls } from '@/lib/tailwind';
import { useBodyLoaded } from '@/components/primitives';
import { OpenAPIMethodBadge } from '@gitbook/react-openapi';
import { HEADER_HEIGHT_DESKTOP } from '../layout';
/**
@@ -141,11 +142,11 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
)}
>
{section.tag ? (
<span
className={`-mt-0.5 openapi-method text-xs! openapi-method-${section.tag.toLowerCase()}`}
>
{section.tag}
</span>
<OpenAPIMethodBadge
method={section.tag}
size="small"
className="-mt-0.5"
/>
) : null}
<span
@@ -2,6 +2,7 @@
import { tcls } from '@/lib/tailwind';
import { SiteInsightsLinkPosition } from '@gitbook/api';
import { OpenAPIMethodBadge } from '@gitbook/react-openapi';
import { Tag } from '../Tag';
import { PagesList } from './PagesList';
import { TOCPageIcon } from './TOCPageIcon';
@@ -41,7 +42,18 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
icon={<TOCPageIcon page={page} />}
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>
</li>
);
@@ -1,4 +1,5 @@
import type { GitBookSiteContext } from '@/lib/context';
import { getOpenAPIOperationPageProps } from '@/lib/openapi/computedSourceProps';
import { getPagePaths, hasPageVisibleDescendant } from '@/lib/pages';
import { resolveContentRef } from '@/lib/references';
import { getRevisionTags, resolveTag } from '@/lib/tags';
@@ -26,6 +27,7 @@ export type ClientTOCPageDocument = {
pathnames: string[];
descendants?: ClientTOCPage[];
primaryTag?: RevisionTag;
openAPIOperation?: { method: string; deprecated: boolean };
};
export type ClientTOCPageGroup = {
@@ -83,6 +85,7 @@ export async function encodeClientTableOfContents(
pathnames: getPagePaths(rootPages, page),
descendants,
primaryTag,
openAPIOperation: getOpenAPIOperationPageProps(page),
type: 'document',
})
);
@@ -3,7 +3,7 @@ export const ToCItemBaseStyles = [
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
'focus-visible:-outline-offset-2',
'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 = [
@@ -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 (
<>
<OpenAPIPath context={context} canCopy={false} withServer={false} data={data} />
<OpenAPIPath
context={context}
canCopy={false}
withServer={false}
data={data}
methodBadgeSize="small"
/>
{items.length > 1 ? (
<OpenAPISelect
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
*/
canCopy?: boolean;
/**
* The size of the HTTP method badge.
* @default 'medium'
*/
methodBadgeSize?: 'small' | 'medium';
};
/**
+11 -2
View File
@@ -1,4 +1,5 @@
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPIMethodBadge } from './OpenAPIMethodBadge';
import type { OpenAPIPathProps } from './OpenAPIPath';
import type { OpenAPIClientContext } from './context';
@@ -10,14 +11,22 @@ export function OpenAPIPathItem(
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 title = <span className="openapi-path-title">{children}</span>;
return (
<div className="openapi-path">
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
<OpenAPIMethodBadge method={method} size={methodBadgeSize} />
{canCopy && value ? (
copyType === 'children' ? (
<OpenAPICopyButton
@@ -24,7 +24,9 @@ export function OpenAPIColumnSpec(props: {
])}
</div>
) : null}
<OpenAPIOperationDescription operation={operation} context={context} />
{!context.headless ? (
<OpenAPIOperationDescription operation={operation} context={context} />
) : null}
<OpenAPISpec data={data} context={clientContext} />
</div>
);
@@ -23,7 +23,10 @@ export function OpenAPISummary(props: {
})();
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']) && (
<div className="openapi-summary-tags">
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
@@ -32,7 +35,7 @@ export function OpenAPISummary(props: {
)}
</div>
)}
{title
{!context.headless && title
? context.renderHeading({
deprecated: operation.deprecated ?? false,
stability: operation['x-stability'],
+5
View File
@@ -72,6 +72,11 @@ export interface OpenAPIContext
*/
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.
* 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;
}
}
+2
View File
@@ -1,4 +1,6 @@
export * from './schemas';
export * from './formatOpenAPIMethod';
export * from './OpenAPIMethodBadge';
export * from './OpenAPIOperation';
export * from './OpenAPIWebhook';
export * from './OpenAPIOperationContext';