Support expand-all options for OpenAPI (#4116)

This commit is contained in:
Nolann B.
2026-03-25 23:53:51 +01:00
committed by GitHub
parent 1ba412ff6c
commit c56b569a05
16 changed files with 106 additions and 20 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@gitbook/openapi-parser": patch
"@gitbook/react-openapi": patch
"gitbook": patch
---
Support expand-all options for OpenAPI
@@ -50,6 +50,10 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
props, props,
specUrl: publicURL, specUrl: publicURL,
context: context.contentContext, context: context.contentContext,
expandAllResponses:
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
})} })}
className="openapi-block" className="openapi-block"
/> />
@@ -46,12 +46,13 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return ( return (
<BaseOpenAPISchemas <BaseOpenAPISchemas
schemas={data.schemas} data={data}
grouped={block.data.grouped} grouped={block.data.grouped}
context={getOpenAPIContext({ context={getOpenAPIContext({
props, props,
specUrl: publicURL, specUrl: publicURL,
context: context.contentContext, context: context.contentContext,
expandAllModelSections: data['x-expandAllModelSections'],
})} })}
className="openapi-block" className="openapi-block"
/> />
@@ -52,6 +52,10 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
props, props,
specUrl: publicURL, specUrl: publicURL,
context: context.contentContext, context: context.contentContext,
expandAllResponses:
data.operation['x-expandAllResponses'] ?? data['x-expandAllResponses'],
expandAllModelSections:
data.operation['x-expandAllModelSections'] ?? data['x-expandAllModelSections'],
})} })}
className="openapi-block" className="openapi-block"
/> />
@@ -24,8 +24,10 @@ export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>; props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string | null; specUrl: string | null;
context: GitBookAnyContext | undefined; context: GitBookAnyContext | undefined;
expandAllResponses?: boolean;
expandAllModelSections?: boolean;
}): OpenAPIContextInput { }): OpenAPIContextInput {
const { props, specUrl, context } = args; const { props, specUrl, context, expandAllResponses, expandAllModelSections } = args;
const { block } = props; const { block } = props;
const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE; const customizationLocale = context ? getSpaceLocale(context) : DEFAULT_LOCALE;
@@ -91,7 +93,8 @@ export function getOpenAPIContext(args: {
}} }}
/> />
), ),
defaultInteractiveOpened: props.context.mode === 'print', expandAllResponses: expandAllResponses || props.context.mode === 'print',
expandAllModelSections: expandAllModelSections || props.context.mode === 'print',
id: block.meta?.id, id: block.meta?.id,
blockKey: block.key, blockKey: block.key,
locale, locale,
@@ -1,5 +1,5 @@
import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser'; import { OpenAPIParseError } from '@gitbook/openapi-parser';
import { resolveOpenAPISchemas } from '@gitbook/react-openapi'; import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi';
import { fetchOpenAPIFilesystem } from './fetch'; import { fetchOpenAPIFilesystem } from './fetch';
import type { import type {
OpenAPISchemasBlock, OpenAPISchemasBlock,
@@ -7,9 +7,7 @@ import type {
ResolveOpenAPIBlockResult, ResolveOpenAPIBlockResult,
} from './types'; } from './types';
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{ type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<OpenAPISchemasData>;
schemas: OpenAPISchema[];
}>;
const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>(); const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>();
+20
View File
@@ -23,6 +23,16 @@ export interface OpenAPICustomSpecProperties {
*/ */
'x-enable-proxy'?: boolean; 'x-enable-proxy'?: boolean;
/**
* If `true`, all response sections will be expanded by default.
*/
'x-expandAllResponses'?: boolean;
/**
* If `true`, all model/schema sections will be expanded by default.
*/
'x-expandAllModelSections'?: boolean;
/** /**
* Description in HTML format. * Description in HTML format.
*/ */
@@ -75,6 +85,16 @@ export interface OpenAPICustomOperationProperties {
*/ */
'x-enable-proxy'?: boolean; 'x-enable-proxy'?: boolean;
/**
* If `true`, all response sections will be expanded by default.
*/
'x-expandAllResponses'?: boolean;
/**
* If `true`, all model/schema sections will be expanded by default.
*/
'x-expandAllModelSections'?: boolean;
/** /**
* Stability of the operation. * Stability of the operation.
* @enum 'experimental' | 'alpha' | 'beta' * @enum 'experimental' | 'alpha' | 'beta'
@@ -43,6 +43,7 @@ export function OpenAPIResponse(props: {
/> />
} }
icon={context.icons.plus} icon={context.icons.plus}
defaultExpanded={context.expandAllModelSections}
label={(isExpanded) => label={(isExpanded) =>
tString( tString(
context.translation, context.translation,
@@ -107,11 +107,19 @@ export function OpenAPIResponses(props: {
const state = useResponseExamplesState(context.blockKey, groups[0]?.key); const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
const expandAll = context.expandAllResponses;
const expandedKeys = expandAll
? new Set(groups.map((g) => g.key))
: state.key
? new Set([state.key])
: new Set<string>();
return ( return (
<StaticSection header={t(context.translation, 'responses')} className="openapi-responses"> <StaticSection header={t(context.translation, 'responses')} className="openapi-responses">
<OpenAPIDisclosureGroup <OpenAPIDisclosureGroup
icon={context.icons.chevronRight} icon={context.icons.chevronRight}
expandedKeys={state.key ? new Set([state.key]) : new Set()} allowsMultipleExpanded={expandAll}
expandedKeys={expandedKeys}
onExpandedChange={(keys) => { onExpandedChange={(keys) => {
const key = keys.values().next().value ?? null; const key = keys.values().next().value ?? null;
state.setKey(key); state.setKey(key);
@@ -130,6 +130,7 @@ function OpenAPISchemaProperty(
label={(isExpanded) => label={(isExpanded) =>
getDisclosureLabel({ schema: effectiveSchema, isExpanded, context }) getDisclosureLabel({ schema: effectiveSchema, isExpanded, context })
} }
defaultExpanded={context.expandAllModelSections}
> >
{content} {content}
</OpenAPIDisclosure> </OpenAPIDisclosure>
@@ -408,6 +409,7 @@ function OpenAPISchemaAlternative(props: {
icon={context.icons.plus} icon={context.icons.plus}
header={<OpenAPISchemaPresentation property={{ schema }} context={context} />} header={<OpenAPISchemaPresentation property={{ schema }} context={context} />}
label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
defaultExpanded={context.expandAllModelSections}
> >
<OpenAPISchemaProperties <OpenAPISchemaProperties
properties={properties} properties={properties}
+10 -3
View File
@@ -19,10 +19,16 @@ export interface OpenAPIClientContext {
}; };
/** /**
* Force all sections to be opened by default. * If `true`, all response sections will be expanded by default.
* @default false * @default false
*/ */
defaultInteractiveOpened?: boolean; expandAllResponses?: boolean;
/**
* If `true`, all model/schema sections will be expanded by default.
* @default false
*/
expandAllModelSections?: boolean;
/** /**
* The key of the block * The key of the block
@@ -107,7 +113,8 @@ export function getOpenAPIClientContext(context: OpenAPIUniversalContext): OpenA
return { return {
translation: context.translation, translation: context.translation,
icons: context.icons, icons: context.icons,
defaultInteractiveOpened: context.defaultInteractiveOpened, expandAllResponses: context.expandAllResponses,
expandAllModelSections: context.expandAllModelSections,
blockKey: context.blockKey, blockKey: context.blockKey,
id: context.id, id: context.id,
proxyUrl: '$$isClientContext$$' in context ? context.proxyUrl : undefined, proxyUrl: '$$isClientContext$$' in context ? context.proxyUrl : undefined,
@@ -83,6 +83,14 @@ export async function resolveOpenAPIOperation(
: undefined, : undefined,
'x-enable-proxy': 'x-enable-proxy':
typeof schema['x-enable-proxy'] === 'boolean' ? schema['x-enable-proxy'] : undefined, typeof schema['x-enable-proxy'] === 'boolean' ? schema['x-enable-proxy'] : undefined,
'x-expandAllResponses':
typeof schema['x-expandAllResponses'] === 'boolean'
? schema['x-expandAllResponses']
: undefined,
'x-expandAllModelSections':
typeof schema['x-expandAllModelSections'] === 'boolean'
? schema['x-expandAllModelSections']
: undefined,
}; };
} }
@@ -43,6 +43,14 @@ export async function resolveOpenAPIWebhook(
operation, operation,
method, method,
name, name,
'x-expandAllResponses':
typeof schema['x-expandAllResponses'] === 'boolean'
? schema['x-expandAllResponses']
: undefined,
'x-expandAllModelSections':
typeof schema['x-expandAllModelSections'] === 'boolean'
? schema['x-expandAllModelSections']
: undefined,
}; };
} }
@@ -23,6 +23,7 @@ export function OpenAPISchemaItem(props: {
icon={context.icons.plus} icon={context.icons.plus}
header={name} header={name}
label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })}
defaultExpanded={context.expandAllModelSections}
> >
<Section className="openapi-section-schemas"> <Section className="openapi-section-schemas">
<SectionBody> <SectionBody>
@@ -1,4 +1,3 @@
import type { OpenAPISchema } from '@gitbook/openapi-parser';
import clsx from 'classnames'; import clsx from 'classnames';
import { OpenAPIExample } from '../OpenAPIExample'; import { OpenAPIExample } from '../OpenAPIExample';
import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
@@ -11,20 +10,22 @@ import {
import { t } from '../translate'; import { t } from '../translate';
import { getExampleFromSchema } from '../util/example'; import { getExampleFromSchema } from '../util/example';
import { OpenAPISchemaItem } from './OpenAPISchemaItem'; import { OpenAPISchemaItem } from './OpenAPISchemaItem';
import type { OpenAPISchemasData } from './resolveOpenAPISchemas';
/** /**
* OpenAPI Schemas component. * OpenAPI Schemas component.
*/ */
export function OpenAPISchemas(props: { export function OpenAPISchemas(props: {
className?: string; className?: string;
schemas: OpenAPISchema[]; data: OpenAPISchemasData;
context: OpenAPIContextInput; context: OpenAPIContextInput;
/** /**
* Whether to show the schema directly if there is only one. * Whether to show the schema directly if there is only one.
*/ */
grouped?: boolean; grouped?: boolean;
}) { }) {
const { schemas, context: contextInput, grouped, className } = props; const { data, context: contextInput, grouped, className } = props;
const { schemas } = data;
const firstSchema = schemas[0]; const firstSchema = schemas[0];
@@ -1,7 +1,16 @@
import type { Filesystem, OpenAPISchema, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import type {
Filesystem,
OpenAPICustomSpecProperties,
OpenAPISchema,
OpenAPIV3xDocument,
} from '@gitbook/openapi-parser';
import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser'; import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser';
import { dereferenceFilesystem } from '../dereference'; import { dereferenceFilesystem } from '../dereference';
export type OpenAPISchemasData = Pick<OpenAPICustomSpecProperties, 'x-expandAllModelSections'> & {
schemas: OpenAPISchema[];
};
/** /**
* Resolve an OpenAPI schemas from a file and compile it to a more usable format. * Resolve an OpenAPI schemas from a file and compile it to a more usable format.
* Schemas are extracted from the OpenAPI components.schemas * Schemas are extracted from the OpenAPI components.schemas
@@ -11,9 +20,7 @@ export async function resolveOpenAPISchemas(
options: { options: {
schemas: string[]; schemas: string[];
} }
): Promise<{ ): Promise<OpenAPISchemasData | null> {
schemas: OpenAPISchema[];
} | null> {
const { schemas: selectedSchemas } = options; const { schemas: selectedSchemas } = options;
const schema = await dereferenceFilesystem(filesystem); const schema = await dereferenceFilesystem(filesystem);
@@ -24,5 +31,11 @@ export async function resolveOpenAPISchemas(
return null; return null;
} }
return { schemas }; return {
schemas,
'x-expandAllModelSections':
typeof schema['x-expandAllModelSections'] === 'boolean'
? schema['x-expandAllModelSections']
: undefined,
};
} }