mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
RND-3387: synchronize response tabs (#2457)
Co-authored-by: Samy Pessé <samypesse@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': minor
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Synchronize response and response example tabs
|
||||
@@ -56,6 +56,7 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
||||
CodeBlock: PlainCodeBlock,
|
||||
defaultInteractiveOpened: context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key,
|
||||
}}
|
||||
className="openapi-block"
|
||||
/>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"classnames": "^2.5.1",
|
||||
"flatted": "^3.2.9",
|
||||
"openapi-types": "^12.1.3",
|
||||
"yaml": "1.10.2",
|
||||
"swagger2openapi": "^7.0.8"
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"yaml": "1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/swagger2openapi": "^7.0.4",
|
||||
@@ -24,7 +24,8 @@
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
"react": "*",
|
||||
"recoil": "^0.7.7"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { atom, useRecoilState } from 'recoil';
|
||||
|
||||
interface InteractiveSectionTab {
|
||||
key: string;
|
||||
@@ -9,6 +10,11 @@ interface InteractiveSectionTab {
|
||||
body: React.ReactNode;
|
||||
}
|
||||
|
||||
const syncedTabsAtom = atom<Record<string, string>>({
|
||||
key: 'syncedTabState',
|
||||
default: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* To optimize rendering, most of the components are server-components,
|
||||
* and the interactiveness is mainly handled by a few key components like this one.
|
||||
@@ -34,6 +40,8 @@ export function InteractiveSection(props: {
|
||||
children?: React.ReactNode;
|
||||
/** Children to display within the container */
|
||||
overlay?: React.ReactNode;
|
||||
/** An optional key referencing a value in global state */
|
||||
stateKey?: string;
|
||||
}) {
|
||||
const {
|
||||
id,
|
||||
@@ -47,12 +55,18 @@ export function InteractiveSection(props: {
|
||||
overlay,
|
||||
toggleOpenIcon = '▶',
|
||||
toggleCloseIcon = '▼',
|
||||
stateKey,
|
||||
} = props;
|
||||
const [syncedTabs, setSyncedTabs] = useRecoilState(syncedTabsAtom);
|
||||
const tabFromState =
|
||||
stateKey && stateKey in syncedTabs
|
||||
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
|
||||
: undefined;
|
||||
|
||||
const [opened, setOpened] = React.useState(defaultOpened);
|
||||
const [selectedTabKey, setSelectedTab] = React.useState(defaultTab);
|
||||
const [selectedTabKey, setSelectedTab] = React.useState(tabFromState?.key ?? defaultTab);
|
||||
const selectedTab: InteractiveSectionTab | undefined =
|
||||
tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
||||
tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -99,6 +113,12 @@ export function InteractiveSection(props: {
|
||||
value={selectedTab.key}
|
||||
onChange={(event) => {
|
||||
setSelectedTab(event.target.value);
|
||||
if (stateKey) {
|
||||
setSyncedTabs((state) => ({
|
||||
...state,
|
||||
[stateKey]: event.target.value,
|
||||
}));
|
||||
}
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -24,6 +24,7 @@ export function OpenAPIOperation(props: {
|
||||
const clientContext: OpenAPIClientContext = {
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
icons: context.icons,
|
||||
blockKey: context.blockKey,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { generateSchemaExample } from './generateSchemaExample';
|
||||
import { OpenAPIContextProps } from './types';
|
||||
import { noReference } from './utils';
|
||||
import { createStateKey, noReference } from './utils';
|
||||
|
||||
/**
|
||||
* Display an example of the response content.
|
||||
@@ -78,6 +78,7 @@ export function OpenAPIResponseExample(props: {
|
||||
|
||||
return (
|
||||
<InteractiveSection
|
||||
stateKey={createStateKey('response', context.blockKey)}
|
||||
header="Response"
|
||||
className="openapi-response-example"
|
||||
tabs={examples}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { noReference } from './utils';
|
||||
import { createStateKey, noReference } from './utils';
|
||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
@@ -17,6 +17,7 @@ export function OpenAPIResponses(props: {
|
||||
|
||||
return (
|
||||
<InteractiveSection
|
||||
stateKey={createStateKey('response', context.blockKey)}
|
||||
header="Response"
|
||||
className={classNames('openapi-responses')}
|
||||
tabs={Object.entries(responses).map(([statusCode, response]) => {
|
||||
|
||||
@@ -15,7 +15,10 @@ 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 */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
@@ -7,3 +7,7 @@ export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export function createStateKey(key: string, scope?: string) {
|
||||
return scope ? `${scope}_${key}` : key;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user