diff --git a/.changeset/purple-forks-refuse.md b/.changeset/purple-forks-refuse.md new file mode 100644 index 000000000..66c941136 --- /dev/null +++ b/.changeset/purple-forks-refuse.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': minor +'gitbook': patch +--- + +Synchronize response and response example tabs diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 83c9f5d19..014a53193 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -56,6 +56,7 @@ async function OpenAPIBody(props: BlockProps) { CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, + blockKey: block.key, }} className="openapi-block" /> diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index 4a6390e28..075178063 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -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", diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx index f2ee1e6cc..37b19d247 100644 --- a/packages/react-openapi/src/InteractiveSection.tsx +++ b/packages/react-openapi/src/InteractiveSection.tsx @@ -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>({ + 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 (
{ setSelectedTab(event.target.value); + if (stateKey) { + setSyncedTabs((state) => ({ + ...state, + [stateKey]: event.target.value, + })); + } setOpened(true); }} > diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index ead87ffd3..365cede51 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -24,6 +24,7 @@ export function OpenAPIOperation(props: { const clientContext: OpenAPIClientContext = { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, + blockKey: context.blockKey, }; return ( diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index e88434d4f..f4758c485 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -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 ( { diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 420146649..156a28ac0 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -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; } diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 9fdd2912c..5bc3ee092 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -7,3 +7,7 @@ export function noReference(input: T | OpenAPIV3.ReferenceObject): T { return input; } + +export function createStateKey(key: string, scope?: string) { + return scope ? `${scope}_${key}` : key; +}