From b3b0db8e7ff3df7dc28494d6d22262fb10f89fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Fri, 4 Apr 2025 13:25:53 +0200 Subject: [PATCH] Improve OpenAPI schemas block ungrouped style --- .changeset/strange-chicken-provide.md | 6 + .../[[...pathname]]/PageClientLayout.tsx | 2 +- .../src/components/DocumentView/Heading.tsx | 10 +- .../DocumentView/OpenAPI/OpenAPIOperation.tsx | 57 +------ .../DocumentView/OpenAPI/OpenAPISchemas.tsx | 16 +- .../DocumentView/OpenAPI/context.tsx | 73 +++++++++ .../components/DocumentView/OpenAPI/style.css | 38 +++-- .../components/SitePage/PageClientLayout.tsx | 2 +- packages/gitbook/src/lib/document-sections.ts | 21 +++ .../lib/openapi/resolveOpenAPISchemasBlock.ts | 8 +- .../react-openapi/src/OpenAPICodeSample.tsx | 10 +- packages/react-openapi/src/OpenAPIExample.tsx | 129 ++++++++++++++++ .../react-openapi/src/OpenAPIOperation.tsx | 13 +- packages/react-openapi/src/OpenAPIPath.tsx | 4 +- .../src/OpenAPIResponseExample.tsx | 131 ++-------------- packages/react-openapi/src/OpenAPITabs.tsx | 4 +- packages/react-openapi/src/context.ts | 64 ++++++++ packages/react-openapi/src/index.ts | 2 +- .../src/schemas/OpenAPISchemas.tsx | 145 +++++++++--------- .../src/schemas/resolveOpenAPISchemas.ts | 9 +- packages/react-openapi/src/types.ts | 61 ++++---- 21 files changed, 478 insertions(+), 327 deletions(-) create mode 100644 .changeset/strange-chicken-provide.md create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx create mode 100644 packages/react-openapi/src/OpenAPIExample.tsx create mode 100644 packages/react-openapi/src/context.ts diff --git a/.changeset/strange-chicken-provide.md b/.changeset/strange-chicken-provide.md new file mode 100644 index 0000000000..6ee3415251 --- /dev/null +++ b/.changeset/strange-chicken-provide.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX. diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx index b9febb51a4..27066312c6 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx @@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks'; export function PageClientLayout(props: { withSections?: boolean }) { // We use this hook in the page layout to ensure the elements for the blocks // are rendered before we scroll to a hash or to the top of the page - useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined }); + useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined }); useStripFallbackQueryParam(); return null; diff --git a/packages/gitbook/src/components/DocumentView/Heading.tsx b/packages/gitbook/src/components/DocumentView/Heading.tsx index 8016b0f1ba..cb11038321 100644 --- a/packages/gitbook/src/components/DocumentView/Heading.tsx +++ b/packages/gitbook/src/components/DocumentView/Heading.tsx @@ -20,7 +20,15 @@ export function Heading(props: BlockProps) { return (
return ( , - chevronRight: , - plus: , - }, - renderCodeBlock: (codeProps) => , - renderDocument: (documentProps) => ( - - ), - renderHeading: (headingProps) => ( - div]:mt-0' - : undefined, - ])} - block={{ - object: 'block', - key: `${block.key}-heading`, - meta: block.meta, - data: {}, - type: 'heading-2', - nodes: [ - { - key: `${block.key}-heading-text`, - object: 'text', - leaves: [ - { text: headingProps.title, object: 'leaf', marks: [] }, - ], - }, - ], - }} - /> - ), - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - blockKey: block.key, - }} + context={getOpenAPIContext({ props, specUrl })} className="openapi-block" /> ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx index b2a8415e17..cdda5541e9 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx @@ -1,6 +1,5 @@ import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock'; import { tcls } from '@/lib/tailwind'; -import { Icon } from '@gitbook/icons'; import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi'; import type { BlockProps } from '../Block'; @@ -8,6 +7,7 @@ import type { BlockProps } from '../Block'; import './scalar.css'; import './style.css'; import type { OpenAPISchemasBlock } from '@/lib/openapi/types'; +import { getOpenAPIContext } from './context'; /** * Render an openapi-schemas block. @@ -49,19 +49,9 @@ async function OpenAPISchemasBody(props: BlockProps) { return ( , - chevronRight: , - plus: , - }, - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - blockKey: block.key, - }} + context={getOpenAPIContext({ props, specUrl })} className="openapi-block" /> ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx new file mode 100644 index 0000000000..a40411fa6a --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -0,0 +1,73 @@ +import type { JSONDocument } from '@gitbook/api'; +import { Icon } from '@gitbook/icons'; +import type { OpenAPIContext } from '@gitbook/react-openapi'; + +import { tcls } from '@/lib/tailwind'; + +import type { BlockProps } from '../Block'; +import { PlainCodeBlock } from '../CodeBlock'; +import { DocumentView } from '../DocumentView'; +import { Heading } from '../Heading'; + +import './scalar.css'; +import './style.css'; +import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock } from '@/lib/openapi/types'; + +/** + * Get the OpenAPI context to render a block. + */ +export function getOpenAPIContext(args: { + props: BlockProps; + specUrl: string; +}): OpenAPIContext { + const { props, specUrl } = args; + const { block } = props; + return { + specUrl, + icons: { + chevronDown: , + chevronRight: , + plus: , + }, + renderCodeBlock: (codeProps) => , + renderDocument: (documentProps) => ( + + ), + renderHeading: (headingProps) => ( + div]:mt-0' + : undefined, + ])} + block={{ + object: 'block', + key: `${block.key}-heading`, + meta: block.meta, + data: {}, + type: 'heading-2', + nodes: [ + { + key: `${block.key}-heading-text`, + object: 'text', + leaves: [{ text: headingProps.title, object: 'leaf', marks: [] }], + }, + ], + }} + /> + ), + defaultInteractiveOpened: props.context.mode === 'print', + id: block.meta?.id, + blockKey: block.key, + }; +} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index ffec8afd45..b7b143002d 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -1,5 +1,6 @@ /* Layout Components */ -.openapi-operation { +.openapi-operation, +.openapi-schemas { @apply flex-1 flex flex-col gap-8 mb-14 min-w-0; } @@ -17,7 +18,7 @@ } .openapi-summary { - @apply flex flex-col items-start justify-start gap-3; + @apply flex flex-col items-start justify-start gap-3 scroll-m-12; } .openapi-summary-tags { @@ -483,12 +484,30 @@ @apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle; } -/* Response Example */ -.openapi-response-example { +/* Panel */ +.openapi-panel { @apply border rounded bg-tint border-tint-subtle; } -.openapi-response-example-empty { +.openapi-panel-heading { + @apply font-medium px-4 py-2 text-xs uppercase; +} + +.openapi-panel-body { + @apply theme-gradient:bg-tint-12/1 relative; + @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10; +} + +.openapi-panel-footer { + @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint; +} + +.openapi-panel-footer .openapi-markdown { + @apply text-[0.813rem] text-tint; +} + +/* Example */ +.openapi-example-empty { @apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center; } @@ -559,15 +578,6 @@ .openapi-tabs-panel { @apply flex-1 text-sm relative focus-visible:outline-none; - @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10; -} - -.openapi-tabs-footer { - @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint; -} - -.openapi-tabs-footer .openapi-markdown { - @apply text-[0.813rem] text-tint; } /* Disclosure group */ diff --git a/packages/gitbook/src/components/SitePage/PageClientLayout.tsx b/packages/gitbook/src/components/SitePage/PageClientLayout.tsx index b9febb51a4..27066312c6 100644 --- a/packages/gitbook/src/components/SitePage/PageClientLayout.tsx +++ b/packages/gitbook/src/components/SitePage/PageClientLayout.tsx @@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks'; export function PageClientLayout(props: { withSections?: boolean }) { // We use this hook in the page layout to ensure the elements for the blocks // are rendered before we scroll to a hash or to the top of the page - useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined }); + useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined }); useStripFallbackQueryParam(); return null; diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts index 3f8dbb13a5..38619a5881 100644 --- a/packages/gitbook/src/lib/document-sections.ts +++ b/packages/gitbook/src/lib/document-sections.ts @@ -3,6 +3,7 @@ import type { GitBookAnyContext } from '@v2/lib/context'; import { getNodeText } from './document'; import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock'; +import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock'; export interface DocumentSection { id: string; @@ -52,6 +53,26 @@ export async function getDocumentSections( }); } } + + if ( + block.type === 'openapi-schemas' && + !block.data.grouped && + block.meta?.id && + block.data.schemas.length === 1 + ) { + const { data } = await resolveOpenAPISchemasBlock({ + block, + context, + }); + const schema = data?.schemas[0]; + if (schema) { + sections.push({ + id: block.meta.id, + title: `The ${schema.name} object`, + depth: 1, + }); + } + } } return sections; diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts index 5c6b64b3b9..82c10d60ba 100644 --- a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts @@ -1,5 +1,5 @@ -import { OpenAPIParseError } from '@gitbook/openapi-parser'; -import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi'; +import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser'; +import { resolveOpenAPISchemas } from '@gitbook/react-openapi'; import { fetchOpenAPIFilesystem } from './fetch'; import type { OpenAPISchemasBlock, @@ -7,7 +7,9 @@ import type { ResolveOpenAPIBlockResult, } from './types'; -type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult; +type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{ + schemas: OpenAPISchema[]; +}>; const weakmap = new WeakMap>(); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 1568a07445..5e36d50bdb 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -9,7 +9,7 @@ import { StaticSection } from './StaticSection'; import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples'; import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { stringifyOpenAPI } from './stringifyOpenAPI'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; +import type { OpenAPIContext, OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; import { checkIsReference, createStateKey } from './utils'; @@ -21,7 +21,7 @@ const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-code */ export function OpenAPICodeSample(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data } = props; @@ -58,7 +58,7 @@ export function OpenAPICodeSample(props: { */ function generateCodeSamples(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context } = props; @@ -189,7 +189,7 @@ export interface MediaTypeRenderer { function OpenAPICodeSampleFooter(props: { data: OpenAPIOperationData; renderers: MediaTypeRenderer[]; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context, renderers } = props; const { method, path } = data; @@ -227,7 +227,7 @@ function OpenAPICodeSampleFooter(props: { */ function getCustomCodeSamples(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context } = props; diff --git a/packages/react-openapi/src/OpenAPIExample.tsx b/packages/react-openapi/src/OpenAPIExample.tsx new file mode 100644 index 0000000000..56b5bfe159 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIExample.tsx @@ -0,0 +1,129 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { generateSchemaExample } from './generateSchemaExample'; +import { json2xml } from './json2xml'; +import { stringifyOpenAPI } from './stringifyOpenAPI'; +import type { OpenAPIContext } from './types'; +import { checkIsReference } from './utils'; + +/** + * Display an example. + */ +export function OpenAPIExample(props: { + example: OpenAPIV3.ExampleObject; + context: OpenAPIContext; + syntax: string; +}) { + const { example, context, syntax } = props; + const code = stringifyExample({ example, xml: syntax === 'xml' }); + + if (code === null) { + return ; + } + + return context.renderCodeBlock({ code, syntax }); +} + +function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null { + const { example, xml } = args; + + if (!example.value) { + return null; + } + + if (typeof example.value === 'string') { + return example.value; + } + + if (xml) { + return json2xml(example.value); + } + + return stringifyOpenAPI(example.value, null, 2); +} + +/** + * Empty response example. + */ +export function OpenAPIEmptyExample() { + return ( +
+            

No Content

+
+ ); +} + +/** + * Generate an example from a reference object. + */ +export function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject { + return { summary: 'Unresolved reference', value: { $ref: ref.$ref } }; +} + +/** + * Get examples from a media type object. + */ +export function getExamplesFromMediaTypeObject(args: { + mediaType: string; + mediaTypeObject: OpenAPIV3.MediaTypeObject; +}): { key: string; example: OpenAPIV3.ExampleObject }[] { + const { mediaTypeObject, mediaType } = args; + if (mediaTypeObject.examples) { + return Object.entries(mediaTypeObject.examples).map(([key, example]) => { + return { + key, + example: checkIsReference(example) ? getExampleFromReference(example) : example, + }; + }); + } + + if (mediaTypeObject.example) { + return [{ key: 'default', example: { value: mediaTypeObject.example } }]; + } + + if (mediaTypeObject.schema) { + if (mediaType === 'application/xml') { + // @TODO normally we should use the name of the schema but we don't have it + // fix it when we got the reference name + const root = mediaTypeObject.schema.xml?.name ?? 'object'; + return [ + { + key: 'default', + example: { + value: { + [root]: generateSchemaExample(mediaTypeObject.schema, { + xml: mediaType === 'application/xml', + mode: 'read', + }), + }, + }, + }, + ]; + } + return [ + { + key: 'default', + example: { + value: generateSchemaExample(mediaTypeObject.schema, { + mode: 'read', + }), + }, + }, + ]; + } + return []; +} + +/** + * Get example from a schema object. + */ +export function getExampleFromSchema(args: { + schema: OpenAPIV3.SchemaObject; +}): OpenAPIV3.ExampleObject { + const { schema } = args; + + if (schema.example) { + return { value: schema.example }; + } + + return { value: generateSchemaExample(schema, { mode: 'read' }) }; +} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 0857c3185e..3d7a63eb65 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -10,7 +10,8 @@ import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIPath } from './OpenAPIPath'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; import { OpenAPISpec } from './OpenAPISpec'; -import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types'; +import { getOpenAPIClientContext } from './context'; +import type { OpenAPIContext, OpenAPIOperationData } from './types'; import { resolveDescription } from './utils'; /** @@ -19,16 +20,12 @@ import { resolveDescription } from './utils'; export function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { className, data, context } = props; const { operation } = data; - const clientContext: OpenAPIClientContext = { - defaultInteractiveOpened: context.defaultInteractiveOpened, - icons: context.icons, - blockKey: context.blockKey, - }; + const clientContext = getOpenAPIClientContext(context); return (
@@ -79,7 +76,7 @@ export function OpenAPIOperation(props: { function OpenAPIOperationDescription(props: { operation: OpenAPIV3.OperationObject; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { operation } = props; if (operation['x-gitbook-description-document']) { diff --git a/packages/react-openapi/src/OpenAPIPath.tsx b/packages/react-openapi/src/OpenAPIPath.tsx index 45be7c38dc..ddc6893fe0 100644 --- a/packages/react-openapi/src/OpenAPIPath.tsx +++ b/packages/react-openapi/src/OpenAPIPath.tsx @@ -1,5 +1,5 @@ import { OpenAPICopyButton } from './OpenAPICopyButton'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; +import type { OpenAPIContext, OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; /** @@ -7,7 +7,7 @@ import { getDefaultServerURL } from './util/server'; */ export function OpenAPIPath(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data } = props; const { method, path, operation } = data; diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index 94a7399f91..8f7ae857d0 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -1,11 +1,14 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { Markdown } from './Markdown'; +import { + OpenAPIEmptyExample, + OpenAPIExample, + getExampleFromReference, + getExamplesFromMediaTypeObject, +} from './OpenAPIExample'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { StaticSection } from './StaticSection'; -import { generateSchemaExample } from './generateSchemaExample'; -import { json2xml } from './json2xml'; -import { stringifyOpenAPI } from './stringifyOpenAPI'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; +import type { OpenAPIContext, OpenAPIOperationData } from './types'; import { checkIsReference, createStateKey, resolveDescription } from './utils'; /** @@ -13,7 +16,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils'; */ export function OpenAPIResponseExample(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context } = props; @@ -62,7 +65,7 @@ export function OpenAPIResponseExample(props: { return { key: key, label: key, - body: , + body: , footer: description ? : undefined, }; } @@ -81,7 +84,7 @@ export function OpenAPIResponseExample(props: { return ( - } className="openapi-response-example"> + } className="openapi-panel"> @@ -89,7 +92,7 @@ export function OpenAPIResponseExample(props: { } function OpenAPIResponse(props: { - context: OpenAPIContextProps; + context: OpenAPIContext; content: { [media: string]: OpenAPIV3.MediaTypeObject; }; @@ -141,7 +144,7 @@ function OpenAPIResponse(props: { function OpenAPIResponseMediaType(props: { mediaTypeObject: OpenAPIV3.MediaTypeObject; mediaType: string; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { mediaTypeObject, mediaType } = props; const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); @@ -149,7 +152,7 @@ function OpenAPIResponseMediaType(props: { const firstExample = examples[0]; if (!firstExample) { - return ; + return ; } if (examples.length === 1) { @@ -184,42 +187,6 @@ function OpenAPIResponseMediaType(props: { ); } -/** - * Display an example. - */ -function OpenAPIExample(props: { - example: OpenAPIV3.ExampleObject; - context: OpenAPIContextProps; - syntax: string; -}) { - const { example, context, syntax } = props; - const code = stringifyExample({ example, xml: syntax === 'xml' }); - - if (code === null) { - return ; - } - - return context.renderCodeBlock({ code, syntax }); -} - -function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null { - const { example, xml } = args; - - if (!example.value) { - return null; - } - - if (typeof example.value === 'string') { - return example.value; - } - - if (xml) { - return json2xml(example.value); - } - - return stringifyOpenAPI(example.value, null, 2); -} - /** * Get the syntax from a media type. */ @@ -234,75 +201,3 @@ function getSyntaxFromMediaType(mediaType: string): string { return 'text'; } - -/** - * Get examples from a media type object. - */ -function getExamplesFromMediaTypeObject(args: { - mediaType: string; - mediaTypeObject: OpenAPIV3.MediaTypeObject; -}): { key: string; example: OpenAPIV3.ExampleObject }[] { - const { mediaTypeObject, mediaType } = args; - if (mediaTypeObject.examples) { - return Object.entries(mediaTypeObject.examples).map(([key, example]) => { - return { - key, - example: checkIsReference(example) ? getExampleFromReference(example) : example, - }; - }); - } - - if (mediaTypeObject.example) { - return [{ key: 'default', example: { value: mediaTypeObject.example } }]; - } - - if (mediaTypeObject.schema) { - if (mediaType === 'application/xml') { - // @TODO normally we should use the name of the schema but we don't have it - // fix it when we got the reference name - const root = mediaTypeObject.schema.xml?.name ?? 'object'; - return [ - { - key: 'default', - example: { - value: { - [root]: generateSchemaExample(mediaTypeObject.schema, { - xml: mediaType === 'application/xml', - mode: 'read', - }), - }, - }, - }, - ]; - } - return [ - { - key: 'default', - example: { - value: generateSchemaExample(mediaTypeObject.schema, { - mode: 'read', - }), - }, - }, - ]; - } - return []; -} - -/** - * Empty response example. - */ -function OpenAPIEmptyResponseExample() { - return ( -
-            

No body

-
- ); -} - -/** - * Generate an example from a reference object. - */ -function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject { - return { summary: 'Unresolved reference', value: { $ref: ref.$ref } }; -} diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx index 50fd6f63e3..6f8eec0c34 100644 --- a/packages/react-openapi/src/OpenAPITabs.tsx +++ b/packages/react-openapi/src/OpenAPITabs.tsx @@ -138,9 +138,9 @@ export function OpenAPITabsPanels() { return ( -
{selectedTab.body}
+
{selectedTab.body}
{selectedTab.footer ? ( -
{selectedTab.footer}
+
{selectedTab.footer}
) : null}
); diff --git a/packages/react-openapi/src/context.ts b/packages/react-openapi/src/context.ts new file mode 100644 index 0000000000..704b192e80 --- /dev/null +++ b/packages/react-openapi/src/context.ts @@ -0,0 +1,64 @@ +export interface OpenAPIClientContext { + /** + * Icons used in the block. + */ + icons: { + chevronDown: React.ReactNode; + chevronRight: React.ReactNode; + plus: React.ReactNode; + }; + + /** + * Force all sections to be opened by default. + * @default false + */ + defaultInteractiveOpened?: boolean; + + /** + * The key of the block + */ + blockKey?: string; + + /** + * Optional id attached to the heading and used as an anchor. + */ + id?: string; +} + +export interface OpenAPIContext extends OpenAPIClientContext { + /** + * Render a code block. + */ + renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode; + + /** + * Render the heading of the operation. + */ + renderHeading: (props: { + deprecated: boolean; + title: string; + stability?: string; + }) => React.ReactNode; + + /** + * Render the document of the operation. + */ + renderDocument: (props: { document: object }) => React.ReactNode; + + /** + * Specification URL. + */ + specUrl: string; +} + +/** + * Get the client context from the OpenAPI context. + */ +export function getOpenAPIClientContext(context: OpenAPIContext): OpenAPIClientContext { + return { + icons: context.icons, + defaultInteractiveOpened: context.defaultInteractiveOpened, + blockKey: context.blockKey, + id: context.id, + }; +} diff --git a/packages/react-openapi/src/index.ts b/packages/react-openapi/src/index.ts index f2e6f90bd0..ac4cd00c49 100644 --- a/packages/react-openapi/src/index.ts +++ b/packages/react-openapi/src/index.ts @@ -2,4 +2,4 @@ export * from './schemas'; export * from './OpenAPIOperation'; export * from './OpenAPIOperationContext'; export * from './resolveOpenAPIOperation'; -export type { OpenAPISchemasData, OpenAPIOperationData } from './types'; +export type { OpenAPIOperationData, OpenAPIContext } from './types'; diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index 7f024151eb..4449d1d084 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -1,99 +1,104 @@ +import type { OpenAPISchema } from '@gitbook/openapi-parser'; import clsx from 'clsx'; import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup'; +import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; -import { Section, SectionBody } from '../StaticSection'; -import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types'; - -type OpenAPISchemasContextProps = Omit< - OpenAPIContextProps, - 'renderCodeBlock' | 'renderHeading' | 'renderDocument' ->; +import { Section, SectionBody, StaticSection } from '../StaticSection'; +import { getOpenAPIClientContext } from '../context'; +import type { OpenAPIContext } from '../types'; /** - * Display OpenAPI Schemas. + * OpenAPI Schemas component. */ export function OpenAPISchemas(props: { className?: string; - data: OpenAPISchemasData; - context: OpenAPISchemasContextProps; + schemas: OpenAPISchema[]; + context: OpenAPIContext; /** * Whether to show the schema directly if there is only one. */ grouped?: boolean; }) { - const { className, data, context, grouped } = props; - const { schemas } = data; + const { schemas, context, grouped, className } = props; - const clientContext: OpenAPIClientContext = { - defaultInteractiveOpened: context.defaultInteractiveOpened, - icons: context.icons, - blockKey: context.blockKey, - }; + const firstSchema = schemas[0]; - if (!schemas.length) { + if (!firstSchema) { return null; } - return ( -
- -
- ); -} - -/** - * Root schema for OpenAPI schemas. - * It displays a single model or a disclosure group for multiple schemas. - */ -function OpenAPIRootSchemasSchema(props: { - schemas: OpenAPISchemasData['schemas']; - context: OpenAPIClientContext; - grouped?: boolean; -}) { - const { schemas, context, grouped } = props; + const clientContext = getOpenAPIClientContext(context); // If there is only one model and we are not grouping, we show it directly. if (schemas.length === 1 && !grouped) { - const schema = schemas?.[0]?.schema; - - if (!schema) { - return null; - } - + const title = `The ${firstSchema.name} object`; return ( -
- - - -
+
+
+ {context.renderHeading({ + title, + })} +
+
+
+ + + +
+
+
+
+

{title}

+
+ +
+
+
+
+
+
); } // If there are multiple schemas, we use a disclosure group to show them all. return ( - ({ - id: name, - label: ( -
- {name} -
- ), - tabs: [ - { - id: 'model', - body: ( -
- - - -
- ), - }, - ], - }))} - /> +
+ ({ + id: name, + label: ( +
+ {name} +
+ ), + tabs: [ + { + id: 'model', + body: ( +
+ + + +
+ ), + }, + ], + }))} + /> +
); } diff --git a/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts b/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts index 2e03c1de54..5497468f1f 100644 --- a/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts +++ b/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts @@ -1,9 +1,6 @@ -import type { Filesystem, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; +import type { Filesystem, OpenAPISchema, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser'; import { dereferenceFilesystem } from '../dereference'; -import type { OpenAPISchemasData } from '../types'; - -//!!TODO: We should return only the schemas that are used in the block. Still a WIP awaiting future work. /** * Resolve an OpenAPI schemas from a file and compile it to a more usable format. @@ -14,7 +11,9 @@ export async function resolveOpenAPISchemas( options: { schemas: string[]; } -): Promise { +): Promise<{ + schemas: OpenAPISchema[]; +} | null> { const { schemas: selectedSchemas } = options; const schema = await dereferenceFilesystem(filesystem); diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 011b420f1d..39b9899e9b 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -1,33 +1,13 @@ import type { OpenAPICustomOperationProperties, OpenAPICustomSpecProperties, - OpenAPISchema, OpenAPIV3, } from '@gitbook/openapi-parser'; -export interface OpenAPIContextProps extends OpenAPIClientContext { - /** - * Render a code block. - */ - renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode; - /** - * Render the heading of the operation. - */ - renderHeading: (props: { - deprecated: boolean; - title: string; - stability?: string; - }) => React.ReactNode; +export interface OpenAPIClientContext { /** - * Render the document of the operation. + * Icons used in the block. */ - renderDocument: (props: { document: object }) => React.ReactNode; - - /** Spec url for the Scalar Api Client */ - specUrl: string; -} - -export interface OpenAPIClientContext { icons: { chevronDown: React.ReactNode; chevronRight: React.ReactNode; @@ -39,14 +19,44 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; + /** * The key of the block */ blockKey?: string; - /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ + + /** + * Optional id attached to the heading and used as an anchor. + */ id?: string; } +export interface OpenAPIContext extends OpenAPIClientContext { + /** + * Render a code block. + */ + renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode; + + /** + * Render the heading of the operation. + */ + renderHeading: (props: { + deprecated?: boolean; + title: string; + stability?: string; + }) => React.ReactNode; + + /** + * Render the document of the operation. + */ + renderDocument: (props: { document: object }) => React.ReactNode; + + /** + * Specification URL. + */ + specUrl: string; +} + export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { path: string; method: string; @@ -60,8 +70,3 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { /** Securities that should be used for this operation */ securities: [string, OpenAPIV3.SecuritySchemeObject][]; } - -export interface OpenAPISchemasData { - /** Components schemas to be used for schemas */ - schemas: OpenAPISchema[]; -}