mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +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'],
|
||||
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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './schemas';
|
||||
export * from './formatOpenAPIMethod';
|
||||
export * from './OpenAPIMethodBadge';
|
||||
export * from './OpenAPIOperation';
|
||||
export * from './OpenAPIWebhook';
|
||||
export * from './OpenAPIOperationContext';
|
||||
|
||||
Reference in New Issue
Block a user