e.stopPropagation()}
+ >
{group.tabs?.length > 1 ? (
-
+
) : group.tabs[0]?.label ? (
{group.tabs[0].label}
) : null}
diff --git a/packages/react-openapi/src/OpenAPIRequestBody.tsx b/packages/react-openapi/src/OpenAPIRequestBody.tsx
index c7c2515e4..7be68a3a5 100644
--- a/packages/react-openapi/src/OpenAPIRequestBody.tsx
+++ b/packages/react-openapi/src/OpenAPIRequestBody.tsx
@@ -1,7 +1,7 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { InteractiveSection } from './InteractiveSection';
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
-import type { OpenAPIClientContext } from './types';
+import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { checkIsReference } from './utils';
/**
@@ -10,8 +10,10 @@ import { checkIsReference } from './utils';
export function OpenAPIRequestBody(props: {
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
context: OpenAPIClientContext;
+ data: OpenAPIOperationData;
}) {
- const { requestBody, context } = props;
+ const { requestBody, context, data } = props;
+ const { method, path } = data;
if (checkIsReference(requestBody)) {
return null;
@@ -21,6 +23,8 @@ export function OpenAPIRequestBody(props: {
{
return {
diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx
index 77f44d5dd..ddec3f011 100644
--- a/packages/react-openapi/src/OpenAPIResponse.tsx
+++ b/packages/react-openapi/src/OpenAPIResponse.tsx
@@ -27,7 +27,7 @@ export function OpenAPIResponse(props: {
return (
{headers.length > 0 ? (
-
+
parameterToProperty({ name, ...header })
diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx
index 8f7ae857d..8640f568d 100644
--- a/packages/react-openapi/src/OpenAPIResponseExample.tsx
+++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx
@@ -6,10 +6,11 @@ import {
getExampleFromReference,
getExamplesFromMediaTypeObject,
} from './OpenAPIExample';
-import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
-import { StaticSection } from './StaticSection';
+import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
+import { OpenAPIResponseMediaTypeContent } from './OpenAPIResponseMediaType';
import type { OpenAPIContext, OpenAPIOperationData } from './types';
-import { checkIsReference, createStateKey, resolveDescription } from './utils';
+import { getStatusCodeDefaultLabel } from './utils';
+import { checkIsReference, resolveDescription } from './utils';
/**
* Display an example of the response content.
@@ -45,11 +46,17 @@ export function OpenAPIResponseExample(props: {
const tabs = responses.map(([key, responseObject]) => {
const description = resolveDescription(responseObject);
+ const label = description ? (
+
+ ) : (
+ getStatusCodeDefaultLabel(key)
+ );
if (checkIsReference(responseObject)) {
return {
key: key,
- label: key,
+ label,
+ statusCode: key,
body: (
),
- footer: description ? : undefined,
};
}
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
return {
key: key,
- label: key,
+ label,
+ statusCode: key,
body: ,
- footer: description ? : undefined,
};
}
return {
key: key,
- label: key,
+ label,
+ statusCode: key,
body: ,
- footer: description ? : undefined,
};
});
@@ -83,11 +89,11 @@ export function OpenAPIResponseExample(props: {
}
return (
-
- } className="openapi-panel">
-
-
-
+
);
}
@@ -106,42 +112,30 @@ function OpenAPIResponse(props: {
throw new Error('One media type is required');
}
- if (entries.length === 1) {
- const [mediaType, mediaTypeObject] = firstEntry;
- return (
-
- );
- }
-
const tabs = entries.map((entry) => {
const [mediaType, mediaTypeObject] = entry;
return {
key: mediaType,
label: mediaType,
- body: (
-
- ),
+ body: <>>,
+ examples: getExamples({
+ mediaTypeObject,
+ mediaType,
+ context,
+ }),
};
});
return (
-
- } className="openapi-response-media-types">
-
-
-
+
);
}
-function OpenAPIResponseMediaType(props: {
+function getExamples(props: {
mediaTypeObject: OpenAPIV3.MediaTypeObject;
mediaType: string;
context: OpenAPIContext;
@@ -149,23 +143,8 @@ function OpenAPIResponseMediaType(props: {
const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
const syntax = getSyntaxFromMediaType(mediaType);
- const firstExample = examples[0];
- if (!firstExample) {
- return ;
- }
-
- if (examples.length === 1) {
- return (
-
- );
- }
-
- const tabs = examples.map((example) => {
+ return examples.map((example) => {
return {
key: example.key,
label: example.example.summary || example.key,
@@ -174,17 +153,6 @@ function OpenAPIResponseMediaType(props: {
),
};
});
-
- return (
-
- }
- className="openapi-response-media-type-examples"
- >
-
-
-
- );
}
/**
diff --git a/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx
new file mode 100644
index 000000000..0b193ed3f
--- /dev/null
+++ b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx
@@ -0,0 +1,123 @@
+'use client';
+
+import clsx from 'clsx';
+import type { Key } from 'react-aria';
+import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
+import { StaticSection } from './StaticSection';
+import { createStateKey, getStatusCodeClassName } from './utils';
+
+type OpenAPIResponseExampleItem = OpenAPISelectItem & {
+ statusCode: string;
+ body: React.ReactNode;
+};
+
+/**
+ * Get the state of the response examples select.
+ */
+export function useResponseExamplesState(
+ blockKey: string | undefined,
+ initialKey: Key = 'default'
+) {
+ return useSelectState(getResponseExampleStateKey(blockKey), initialKey);
+}
+
+export function OpenAPIResponseExampleContent(props: {
+ items: OpenAPIResponseExampleItem[];
+ blockKey?: string;
+ selectIcon?: React.ReactNode;
+}) {
+ const { blockKey, items, selectIcon } = props;
+
+ return (
+
+ }
+ className="openapi-response-examples"
+ >
+
+
+ );
+}
+
+function OpenAPIResponseExampleHeader(props: {
+ items: OpenAPIResponseExampleItem[];
+ blockKey?: string;
+ selectIcon?: React.ReactNode;
+}) {
+ const { items, blockKey, selectIcon } = props;
+
+ if (items.length === 1) {
+ const item = items[0];
+
+ if (!item) {
+ return null;
+ }
+
+ return (
+
+
+ {item.statusCode}
+
+ {item.label}
+
+ );
+ }
+
+ return (
+
+ {items.map((item) => (
+
+
+ {item.statusCode}
+
+ {item.label}
+
+ ))}
+
+ );
+}
+
+function OpenAPIResponseExampleBody(props: {
+ items: OpenAPIResponseExampleItem[];
+ blockKey?: string;
+}) {
+ const { blockKey, items } = props;
+ const state = useResponseExamplesState(blockKey, items[0]?.key);
+
+ const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
+
+ if (!selectedItem) {
+ return null;
+ }
+
+ return {selectedItem.body}
;
+}
+
+/**
+ * Return the state key for the response examples.
+ */
+function getResponseExampleStateKey(blockKey: string | undefined) {
+ return createStateKey('openapi-responses', blockKey);
+}
diff --git a/packages/react-openapi/src/OpenAPIResponseMediaType.tsx b/packages/react-openapi/src/OpenAPIResponseMediaType.tsx
new file mode 100644
index 000000000..b2ca31fcd
--- /dev/null
+++ b/packages/react-openapi/src/OpenAPIResponseMediaType.tsx
@@ -0,0 +1,148 @@
+'use client';
+import type { Key } from 'react-aria';
+import { OpenAPIEmptyExample } from './OpenAPIExample';
+import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
+import { StaticSection } from './StaticSection';
+import { createStateKey } from './utils';
+
+type OpenAPIResponseMediaTypeItem = OpenAPISelectItem & {
+ body: React.ReactNode;
+ examples?: OpenAPIResponseMediaTypeItem[];
+};
+
+/**
+ * Get the state of the response examples select.
+ */
+export function useResponseMediaTypesState(
+ blockKey: string | undefined,
+ initialKey: Key = 'default'
+) {
+ return useSelectState(getResponseMediaTypeStateKey(blockKey), initialKey);
+}
+
+function useResponseMediaTypeExamplesState(
+ blockKey: string | undefined,
+ initialKey: Key = 'default'
+) {
+ return useSelectState(getResponseMediaTypeExamplesStateKey(blockKey), initialKey);
+}
+
+export function OpenAPIResponseMediaTypeContent(props: {
+ items: OpenAPIResponseMediaTypeItem[];
+ blockKey?: string;
+ selectIcon?: React.ReactNode;
+}) {
+ const { blockKey, items, selectIcon } = props;
+ const state = useResponseMediaTypesState(blockKey, items[0]?.key);
+
+ const examples = items.find((item) => item.key === state.key)?.examples ?? [];
+
+ if (!items.length && !examples.length) {
+ return null;
+ }
+
+ return (
+ 1 || examples.length > 1 ? (
+
+ ) : null
+ }
+ className="openapi-response-media-types-examples"
+ >
+
+
+ );
+}
+
+function OpenAPIResponseMediaTypeFooter(props: {
+ items: OpenAPIResponseMediaTypeItem[];
+ examples?: OpenAPIResponseMediaTypeItem[];
+ blockKey?: string;
+ selectIcon?: React.ReactNode;
+}) {
+ const { items, examples, blockKey, selectIcon } = props;
+
+ return (
+ <>
+ {items.length > 1 && (
+
+ {items.map((item) => (
+
+ {item.label}
+
+ ))}
+
+ )}
+
+ {examples && examples.length > 1 ? (
+
+ {examples.map((example) => (
+
+ {example.label}
+
+ ))}
+
+ ) : null}
+ >
+ );
+}
+
+function OpenAPIResponseMediaTypeBody(props: {
+ items: OpenAPIResponseMediaTypeItem[];
+ examples?: OpenAPIResponseMediaTypeItem[];
+ blockKey?: string;
+}) {
+ const { blockKey, items, examples } = props;
+ const state = useResponseMediaTypesState(blockKey, items[0]?.key);
+
+ const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
+
+ const exampleState = useResponseMediaTypeExamplesState(
+ blockKey,
+ selectedItem?.examples?.[0]?.key
+ );
+
+ if (!selectedItem) {
+ return null;
+ }
+
+ if (examples) {
+ const selectedExample =
+ examples.find((example) => example.key === exampleState.key) ?? examples[0];
+
+ if (!selectedExample) {
+ return ;
+ }
+
+ return selectedExample.body;
+ }
+
+ return selectedItem.body;
+}
+
+/**
+ * Return the state key for the response media types.
+ */
+function getResponseMediaTypeStateKey(blockKey: string | undefined) {
+ return createStateKey('response-media-types', blockKey);
+}
+
+function getResponseMediaTypeExamplesStateKey(blockKey: string | undefined) {
+ return createStateKey('response-media-types-examples', blockKey);
+}
diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx
index cc577e53d..303a242bf 100644
--- a/packages/react-openapi/src/OpenAPIResponses.tsx
+++ b/packages/react-openapi/src/OpenAPIResponses.tsx
@@ -1,9 +1,14 @@
+'use client';
+
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
+import clsx from 'clsx';
import { Markdown } from './Markdown';
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { OpenAPIResponse } from './OpenAPIResponse';
+import { useResponseExamplesState } from './OpenAPIResponseExampleContent';
import { StaticSection } from './StaticSection';
import type { OpenAPIClientContext } from './types';
+import { createStateKey, getStatusCodeClassName, getStatusCodeDefaultLabel } from './utils';
/**
* Display an interactive response body.
@@ -14,70 +19,85 @@ export function OpenAPIResponses(props: {
}) {
const { responses, context } = props;
+ const groups = Object.entries(responses).map(
+ ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
+ const tabs = (() => {
+ // If there is no content, but there are headers, we need to show the headers
+ if (
+ (!response.content || !Object.keys(response.content).length) &&
+ response.headers &&
+ Object.keys(response.headers).length
+ ) {
+ return [
+ {
+ key: 'default',
+ label: '',
+ body: (
+
+ ),
+ },
+ ];
+ }
+
+ return Object.entries(response.content ?? {}).map(([contentType, mediaType]) => ({
+ key: contentType,
+ label: contentType,
+ body: (
+
+ ),
+ }));
+ })();
+
+ const description = response.description;
+
+ return {
+ key: statusCode,
+ label: (
+
+
+ {statusCode}
+
+ {description ? (
+
+ ) : (
+ getStatusCodeDefaultLabel(statusCode)
+ )}
+
+ ),
+ tabs,
+ };
+ }
+ );
+
+ const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
+
return (
{
- const tabs = (() => {
- // If there is no content, but there are headers, we need to show the headers
- if (
- (!response.content || !Object.keys(response.content).length) &&
- response.headers &&
- Object.keys(response.headers).length
- ) {
- return [
- {
- id: 'default',
- body: (
-
- ),
- },
- ];
- }
-
- return Object.entries(response.content ?? {}).map(
- ([contentType, mediaType]) => ({
- id: contentType,
- label: contentType,
- body: (
-
- ),
- })
- );
- })();
-
- const description = response.description;
-
- return {
- id: statusCode,
- label: (
-
-
- {statusCode}
-
- {description ? (
-
- ) : null}
-
- ),
- tabs,
- };
- }
- )}
+ expandedKeys={state.key ? new Set([state.key]) : new Set()}
+ onExpandedChange={(keys) => {
+ const key = keys.values().next().value ?? null;
+ state.setKey(key);
+ }}
+ groups={groups}
+ selectStateKey={createStateKey('response-media-types', context.blockKey)}
/>
);
diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx
index 89fe40199..0b76efc14 100644
--- a/packages/react-openapi/src/OpenAPISchema.tsx
+++ b/packages/react-openapi/src/OpenAPISchema.tsx
@@ -54,7 +54,10 @@ function OpenAPISchemaProperty(props: {
const properties = getSchemaProperties(schema);
if (properties?.length) {
return (
-
+ getDisclosureLabel(schema, isExpanded)}
+ >
) : null}
-
+ getDisclosureLabel(schema, isExpanded)}
+ >
{properties?.length ? (
{
const description = resolveDescription(security);
diff --git a/packages/react-openapi/src/OpenAPISelect.tsx b/packages/react-openapi/src/OpenAPISelect.tsx
index 14b83b77d..e8551e9b9 100644
--- a/packages/react-openapi/src/OpenAPISelect.tsx
+++ b/packages/react-openapi/src/OpenAPISelect.tsx
@@ -1,6 +1,7 @@
'use client';
import clsx from 'clsx';
+import { useCallback } from 'react';
import {
Button,
type Key,
@@ -13,38 +14,63 @@ import {
type SelectProps,
SelectValue,
} from 'react-aria-components';
+import { useStore } from 'zustand';
+import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
export type OpenAPISelectItem = {
key: Key;
- label: string;
+ label: string | React.ReactNode;
};
interface OpenAPISelectProps extends Omit, 'children'> {
items: T[];
children: React.ReactNode | ((item: T) => React.ReactNode);
- selectedKey?: Key;
- onChange?: (key: string | number) => void;
placement?: PopoverProps['placement'];
+ stateKey?: string;
+ /**
+ * Icon to display in the select button.
+ */
+ icon?: React.ReactNode;
+}
+
+export function useSelectState(stateKey = 'select-state', initialKey?: Key) {
+ const store = useStore(getOrCreateStoreByKey(stateKey, initialKey));
+ return {
+ key: store.key,
+ setKey: useCallback((key: Key | null) => store.setKey(key), [store.setKey]),
+ };
}
export function OpenAPISelect(props: OpenAPISelectProps) {
- const { items, children, className, placement } = props;
+ const {
+ icon = '▼',
+ items,
+ children,
+ className,
+ placement,
+ stateKey,
+ selectedKey,
+ onSelectionChange,
+ } = props;
+
+ const state = useSelectState(stateKey, items[0]?.key);
+
+ const selected = items.find((item) => item.key === state.key) || items[0];
return (
-