mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Improve OpenAPI schemas block ungrouped style (#3092)
This commit is contained in:
@@ -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.
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -20,7 +20,15 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
|
||||
return (
|
||||
<Tag
|
||||
id={id}
|
||||
className={tcls(textStyle.textSize, 'heading', 'group', 'relative', 'grid', style)}
|
||||
className={tcls(
|
||||
textStyle.textSize,
|
||||
'heading',
|
||||
'group',
|
||||
'relative',
|
||||
'grid',
|
||||
'scroll-m-12',
|
||||
style
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi';
|
||||
|
||||
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
|
||||
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 } from '@/lib/openapi/types';
|
||||
import { getOpenAPIContext } from './context';
|
||||
|
||||
/**
|
||||
* Render an openapi block or an openapi-operation block.
|
||||
@@ -55,56 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
|
||||
return (
|
||||
<BaseOpenAPIOperation
|
||||
data={data}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||
renderDocument: (documentProps) => (
|
||||
<DocumentView
|
||||
document={documentProps.document as JSONDocument}
|
||||
context={props.context}
|
||||
style="space-y-6"
|
||||
blockStyle="max-w-full"
|
||||
/>
|
||||
),
|
||||
renderHeading: (headingProps) => (
|
||||
<Heading
|
||||
document={props.document}
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>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"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<OpenAPISchemasBlock>) {
|
||||
|
||||
return (
|
||||
<BaseOpenAPISchemas
|
||||
data={data}
|
||||
schemas={data.schemas}
|
||||
grouped={block.data.grouped}
|
||||
context={{
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
defaultInteractiveOpened: context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key,
|
||||
}}
|
||||
context={getOpenAPIContext({ props, specUrl })}
|
||||
className="openapi-block"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock>;
|
||||
specUrl: string;
|
||||
}): OpenAPIContext {
|
||||
const { props, specUrl } = args;
|
||||
const { block } = props;
|
||||
return {
|
||||
specUrl,
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
plus: <Icon icon="plus" />,
|
||||
},
|
||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||
renderDocument: (documentProps) => (
|
||||
<DocumentView
|
||||
document={documentProps.document as JSONDocument}
|
||||
context={props.context}
|
||||
style="space-y-6"
|
||||
blockStyle="max-w-full"
|
||||
/>
|
||||
),
|
||||
renderHeading: (headingProps) => (
|
||||
<Heading
|
||||
document={props.document}
|
||||
ancestorBlocks={props.ancestorBlocks}
|
||||
isEstimatedOffscreen={props.isEstimatedOffscreen}
|
||||
context={props.context}
|
||||
style={tcls([
|
||||
headingProps.deprecated ? 'line-through' : undefined,
|
||||
headingProps.deprecated || !!headingProps.stability
|
||||
? '[&>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,
|
||||
};
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<OpenAPISchemasData>;
|
||||
type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{
|
||||
schemas: OpenAPISchema[];
|
||||
}>;
|
||||
|
||||
const weakmap = new WeakMap<OpenAPISchemasBlock, Promise<ResolveOpenAPISchemasBlockResult>>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 <OpenAPIEmptyExample />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<pre className="openapi-example-empty">
|
||||
<p>No Content</p>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' }) };
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={clsx('openapi-operation', className)}>
|
||||
@@ -79,7 +76,7 @@ export function OpenAPIOperation(props: {
|
||||
|
||||
function OpenAPIOperationDescription(props: {
|
||||
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||
context: OpenAPIContextProps;
|
||||
context: OpenAPIContext;
|
||||
}) {
|
||||
const { operation } = props;
|
||||
if (operation['x-gitbook-description-document']) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: <OpenAPIEmptyResponseExample />,
|
||||
body: <OpenAPIEmptyExample />,
|
||||
footer: description ? <Markdown source={description} /> : undefined,
|
||||
};
|
||||
}
|
||||
@@ -81,7 +84,7 @@ export function OpenAPIResponseExample(props: {
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-example">
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-panel">
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
@@ -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 <OpenAPIEmptyResponseExample />;
|
||||
return <OpenAPIEmptyExample />;
|
||||
}
|
||||
|
||||
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 <OpenAPIEmptyResponseExample />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<pre className="openapi-response-example-empty">
|
||||
<p>No body</p>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an example from a reference object.
|
||||
*/
|
||||
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
|
||||
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
|
||||
}
|
||||
|
||||
@@ -138,9 +138,9 @@ export function OpenAPITabsPanels() {
|
||||
|
||||
return (
|
||||
<TabPanel id={key} className="openapi-tabs-panel">
|
||||
<div className="openapi-tabs-body">{selectedTab.body}</div>
|
||||
<div className="openapi-panel-body">{selectedTab.body}</div>
|
||||
{selectedTab.footer ? (
|
||||
<div className="openapi-tabs-footer">{selectedTab.footer}</div>
|
||||
<div className="openapi-panel-footer">{selectedTab.footer}</div>
|
||||
) : null}
|
||||
</TabPanel>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIRootSchemasSchema grouped={grouped} schemas={schemas} context={clientContext} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Section>
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<div className="openapi-summary" id={context.id}>
|
||||
{context.renderHeading({
|
||||
title,
|
||||
})}
|
||||
</div>
|
||||
<div className="openapi-columns">
|
||||
<div className="openapi-column-spec">
|
||||
<StaticSection className="openapi-parameters" header="Attributes">
|
||||
<OpenAPIRootSchema
|
||||
schema={firstSchema.schema}
|
||||
context={clientContext}
|
||||
/>
|
||||
</StaticSection>
|
||||
</div>
|
||||
<div className="openapi-column-preview">
|
||||
<div className="openapi-column-preview-body">
|
||||
<div className="openapi-panel">
|
||||
<h4 className="openapi-panel-heading">{title}</h4>
|
||||
<div className="openapi-panel-body">
|
||||
<OpenAPIExample
|
||||
example={getExampleFromSchema({
|
||||
schema: firstSchema.schema,
|
||||
})}
|
||||
context={context}
|
||||
syntax="json"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If there are multiple schemas, we use a disclosure group to show them all.
|
||||
return (
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={schemas.map(({ name, schema }) => ({
|
||||
id: name,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
||||
<span className="openapi-response-statuscode">{name}</span>
|
||||
</div>
|
||||
),
|
||||
tabs: [
|
||||
{
|
||||
id: 'model',
|
||||
body: (
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={context} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={schemas.map(({ name, schema }) => ({
|
||||
id: name,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
||||
<span className="openapi-response-statuscode">{name}</span>
|
||||
</div>
|
||||
),
|
||||
tabs: [
|
||||
{
|
||||
id: 'model',
|
||||
body: (
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema
|
||||
schema={schema}
|
||||
context={clientContext}
|
||||
/>
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<OpenAPISchemasData | null> {
|
||||
): Promise<{
|
||||
schemas: OpenAPISchema[];
|
||||
} | null> {
|
||||
const { schemas: selectedSchemas } = options;
|
||||
|
||||
const schema = await dereferenceFilesystem(filesystem);
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
* Render the document of the operation.
|
||||
*/
|
||||
renderDocument: (props: { document: object }) => React.ReactNode;
|
||||
|
||||
/** Spec url for the Scalar Api Client */
|
||||
specUrl: string;
|
||||
}
|
||||
|
||||
export interface OpenAPIClientContext {
|
||||
/**
|
||||
* Icons used in the block.
|
||||
*/
|
||||
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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user