diff --git a/.changeset/fifty-donkeys-talk.md b/.changeset/fifty-donkeys-talk.md new file mode 100644 index 000000000..130f66f0d --- /dev/null +++ b/.changeset/fifty-donkeys-talk.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Sync tabs across all OpenAPI blocks diff --git a/.changeset/six-trainers-bathe.md b/.changeset/six-trainers-bathe.md new file mode 100644 index 000000000..8f4120f5f --- /dev/null +++ b/.changeset/six-trainers-bathe.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Support for OpenAPI references diff --git a/bun.lock b/bun.lock index d6494ab8b..952eba438 100644 --- a/bun.lock +++ b/bun.lock @@ -31,7 +31,7 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.5.0", + "version": "0.6.0", "dependencies": { "@gitbook/api": "^0.93.0", "@gitbook/cache-do": "workspace:*", @@ -118,7 +118,7 @@ }, "packages/gitbook-v2": { "name": "gitbook-v2", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "@gitbook/api": "^0.93.0", "next": "canary", @@ -151,7 +151,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@scalar/openapi-parser": "^0.10.4", "@scalar/openapi-types": "^0.1.6", @@ -175,7 +175,7 @@ }, "packages/react-contentkit": { "name": "@gitbook/react-contentkit", - "version": "0.5.1", + "version": "0.6.0", "dependencies": { "@gitbook/api": "^0.93.0", "assert-never": "^1.2.1", @@ -207,7 +207,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "0.7.1", + "version": "1.0.0", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "1.0.87", @@ -217,6 +217,7 @@ "react-aria": "^3.37.0", "react-aria-components": "^1.6.0", "usehooks-ts": "^3.1.0", + "zustand": "^5.0.3", }, "devDependencies": { "bun-types": "^1.1.20", @@ -3425,6 +3426,8 @@ "zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], diff --git a/packages/openapi-parser/src/filesystem.ts b/packages/openapi-parser/src/filesystem.ts index 6ba1e5339..c256034e9 100644 --- a/packages/openapi-parser/src/filesystem.ts +++ b/packages/openapi-parser/src/filesystem.ts @@ -20,5 +20,6 @@ export async function createFileSystem(input: { const { filesystem } = await load(input.value, { plugins: [fetchURLs({ rootURL: input.rootURL })], }); + return filesystem; } diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index 5c8fc2055..1e8abb888 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -18,7 +18,8 @@ "flatted": "^3.2.9", "react-aria-components": "^1.6.0", "react-aria": "^3.37.0", - "usehooks-ts": "^3.1.0" + "usehooks-ts": "^3.1.0", + "zustand": "^5.0.3" }, "devDependencies": { "bun-types": "^1.1.20", diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx index adcd1f8b2..26a6a8198 100644 --- a/packages/react-openapi/src/InteractiveSection.tsx +++ b/packages/react-openapi/src/InteractiveSection.tsx @@ -1,7 +1,7 @@ 'use client'; import clsx from 'clsx'; -import { useCallback, useRef, useState, useSyncExternalStore } from 'react'; +import { useRef, useState } from 'react'; import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria'; import { useDisclosureState } from 'react-stately'; @@ -11,30 +11,6 @@ interface InteractiveSectionTab { body: React.ReactNode; } -let globalState: Record = {}; -const listeners = new Set<() => void>(); - -function useSyncedTabsGlobalState() { - const subscribe = useCallback((callback: () => void) => { - listeners.add(callback); - return () => listeners.delete(callback); - }, []); - - const getSnapshot = useCallback(() => globalState, []); - - const setSyncedTabs = useCallback( - (updater: (tabs: Record) => Record) => { - globalState = updater(globalState); - listeners.forEach((listener) => listener()); - }, - [], - ); - - const tabs = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); - - return [tabs, setSyncedTabs] as const; -} - /** * To optimize rendering, most of the components are server-components, * and the interactiveness is mainly handled by a few key components like this one. @@ -59,8 +35,6 @@ 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, @@ -73,16 +47,11 @@ export function InteractiveSection(props: { children, overlay, toggleIcon = '▶', - stateKey, } = props; - const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState(); - const tabFromState = - stateKey && stateKey in syncedTabs - ? tabs.find((tab) => tab.key === syncedTabs[stateKey]) - : undefined; - const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab); + + const [selectedTabKey, setSelectedTab] = useState(defaultTab); const selectedTab: InteractiveSectionTab | undefined = - tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0]; + tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0]; const state = useDisclosureState({ defaultExpanded: defaultOpened, @@ -153,12 +122,6 @@ export function InteractiveSection(props: { value={selectedTab?.key ?? ''} onChange={(event) => { setSelectedTab(event.target.value); - if (stateKey) { - setSyncedTabs((state) => ({ - ...state, - [stateKey]: event.target.value, - })); - } state.expand(); }} > diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 5d60d4c70..69ba204fb 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -3,9 +3,10 @@ import { generateMediaTypeExample, generateSchemaExample } from './generateSchem import { InteractiveSection } from './InteractiveSection'; import { getServersURL } from './OpenAPIServerURL'; import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; -import { noReference } from './utils'; +import { createStateKey } from './utils'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; +import { checkIsReference } from './utils'; /** * Display code samples to execute the operation. @@ -20,24 +21,19 @@ export function OpenAPICodeSample(props: { const searchParams = new URLSearchParams(); const headersObject: { [k: string]: string } = {}; - data.operation.parameters?.forEach((rawParam) => { - const param = noReference(rawParam); + data.operation.parameters?.forEach((param) => { if (!param) { return; } if (param.in === 'header' && param.required) { - const example = param.schema - ? generateSchemaExample(noReference(param.schema)) - : undefined; + const example = param.schema ? generateSchemaExample(param.schema) : undefined; if (example !== undefined && param.name) { headersObject[param.name] = typeof example !== 'string' ? stringifyOpenAPI(example) : example; } } else if (param.in === 'query' && param.required) { - const example = param.schema - ? generateSchemaExample(noReference(param.schema)) - : undefined; + const example = param.schema ? generateSchemaExample(param.schema) : undefined; if (example !== undefined && param.name) { searchParams.append( param.name, @@ -47,7 +43,9 @@ export function OpenAPICodeSample(props: { } }); - const requestBody = noReference(data.operation.requestBody); + const requestBody = !checkIsReference(data.operation.requestBody) + ? data.operation.requestBody + : undefined; const requestBodyContentEntries = requestBody?.content ? Object.entries(requestBody.content) : undefined; @@ -115,7 +113,7 @@ export function OpenAPICodeSample(props: { } return ( - + } className="openapi-codesample"> diff --git a/packages/react-openapi/src/OpenAPIRequestBody.tsx b/packages/react-openapi/src/OpenAPIRequestBody.tsx index e03e36bc1..2dc76ff24 100644 --- a/packages/react-openapi/src/OpenAPIRequestBody.tsx +++ b/packages/react-openapi/src/OpenAPIRequestBody.tsx @@ -1,18 +1,22 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { OpenAPIRootSchema } from './OpenAPISchema'; -import { noReference } from './utils'; import type { OpenAPIClientContext } from './types'; import { InteractiveSection } from './InteractiveSection'; +import { checkIsReference } from './utils'; /** * Display an interactive request body. */ export function OpenAPIRequestBody(props: { - requestBody: OpenAPIV3.RequestBodyObject; + requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject; context: OpenAPIClientContext; }) { const { requestBody, context } = props; + if (checkIsReference(requestBody)) { + return null; + } + return ( ), diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx index 448abbc66..f72b9ed75 100644 --- a/packages/react-openapi/src/OpenAPIResponse.tsx +++ b/packages/react-openapi/src/OpenAPIResponse.tsx @@ -1,6 +1,6 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { OpenAPISchemaProperties } from './OpenAPISchema'; -import { checkIsReference, noReference, resolveDescription } from './utils'; +import { resolveDescription } from './utils'; import type { OpenAPIClientContext } from './types'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; @@ -14,7 +14,7 @@ export function OpenAPIResponse(props: { }) { const { response, context, mediaType } = props; const headers = Object.entries(response.headers ?? {}).map( - ([name, header]) => [name, noReference(header) ?? {}] as const, + ([name, header]) => [name, header ?? {}] as const, ); const content = Object.entries(mediaType.schema ?? {}); @@ -31,7 +31,7 @@ export function OpenAPIResponse(props: { ({ propertyName: name, - schema: noReference(header.schema) ?? {}, + schema: header.schema ?? {}, required: header.required, }))} context={context} @@ -43,7 +43,7 @@ export function OpenAPIResponse(props: { id={`response-${context.blockKey}`} properties={[ { - schema: handleUnresolvedReference(mediaType.schema) ?? {}, + schema: mediaType.schema ?? {}, }, ]} context={context} @@ -52,17 +52,3 @@ export function OpenAPIResponse(props: { ); } - -function handleUnresolvedReference( - input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined, -): OpenAPIV3.SchemaObject { - const isReference = checkIsReference(input); - - if (isReference || input === undefined) { - // If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it. - // Instead we render `any` - return {}; - } - - return input; -} diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index eab4bd0ed..2658e16ce 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -1,7 +1,7 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { generateSchemaExample } from './generateSchemaExample'; import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; -import { checkIsReference, noReference, resolveDescription } from './utils'; +import { checkIsReference, createStateKey, resolveDescription } from './utils'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; import { InteractiveSection } from './InteractiveSection'; @@ -40,7 +40,7 @@ export function OpenAPIResponseExample(props: { const examples = responses .map(([key, value]) => { - const responseObject = noReference(value); + const responseObject = value; const mediaTypeObject = (() => { if (!responseObject.content) { return null; @@ -68,7 +68,7 @@ export function OpenAPIResponseExample(props: { const key = Object.keys(examples)[0]; if (key) { // @TODO handle multiple examples - const firstExample = noReference(examples[key]); + const firstExample = examples[key]; if (firstExample) { return firstExample; } @@ -79,7 +79,7 @@ export function OpenAPIResponseExample(props: { return { value: example }; } - const schema = noReference(mediaTypeObject.schema); + const schema = mediaTypeObject.schema; if (!schema) { return null; } @@ -115,7 +115,7 @@ export function OpenAPIResponseExample(props: { } return ( - + } className="openapi-response-example"> diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx index a52a938d1..97f3978c0 100644 --- a/packages/react-openapi/src/OpenAPIResponses.tsx +++ b/packages/react-openapi/src/OpenAPIResponses.tsx @@ -1,9 +1,9 @@ import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'; -import { createStateKey, resolveDescription } from './utils'; import { OpenAPIResponse } from './OpenAPIResponse'; import { OpenAPIClientContext } from './types'; import { InteractiveSection } from './InteractiveSection'; import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup'; +import { Markdown } from './Markdown'; /** * Display an interactive response body. @@ -15,18 +15,14 @@ export function OpenAPIResponses(props: { const { responses, context } = props; return ( - + { const content = Object.entries(response.content ?? {}); - const description = resolveDescription(response); + const description = response.description; return { id: statusCode, @@ -39,9 +35,10 @@ export function OpenAPIResponses(props: { {statusCode} {description ? ( -
- {htmlToText(description)} -
+ ) : null} ), @@ -64,8 +61,3 @@ export function OpenAPIResponses(props: {
); } - -function htmlToText(html: string): string { - const doc = new DOMParser().parseFromString(html, 'text/html'); - return doc.body.textContent?.trim() || ''; -} diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index efb2e2c23..0844717f8 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -5,7 +5,7 @@ import { useId } from 'react'; import { InteractiveSection } from './InteractiveSection'; import { Markdown } from './Markdown'; import type { OpenAPIClientContext } from './types'; -import { checkIsReference, noReference, resolveDescription } from './utils'; +import { checkIsReference, resolveDescription } from './utils'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { OpenAPISchemaName } from './OpenAPISchemaName'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; @@ -70,7 +70,7 @@ export function OpenAPISchemaProperty( {alternatives[0].map((alternative, index) => ( { - const properties = getSchemaProperties(noReference(subSchema)) ?? [ + const properties = getSchemaProperties(subSchema) ?? [ { - schema: noReference(subSchema), + schema: subSchema, }, ]; return [...acc, ...properties]; @@ -289,7 +289,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche // check array AND schema.items as this is sometimes null despite what the type indicates if (schema.type === 'array' && !!schema.items) { - const items = noReference(schema.items); + const items = schema.items; const itemProperties = getSchemaProperties(items); if (itemProperties) { return itemProperties; @@ -307,12 +307,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche const result: OpenAPISchemaPropertyEntry[] = []; if (schema.properties) { - Object.entries(schema.properties).forEach(([propertyName, rawPropertySchema]) => { - const isReference = checkIsReference(rawPropertySchema); - const propertySchema: OpenAPIV3.SchemaObject = isReference - ? { propertyName } - : rawPropertySchema; - + Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => { result.push({ propertyName, required: Array.isArray(schema.required) @@ -324,7 +319,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche } if (schema.additionalProperties) { - const additionalProperties = noReference(schema.additionalProperties); + const additionalProperties = schema.additionalProperties; result.push({ propertyName: 'Other properties', @@ -348,17 +343,11 @@ export function getSchemaAlternatives( const downAncestors = new Set(ancestors).add(schema); if (schema.anyOf) { - return [ - flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors), - noReference(schema.discriminator), - ]; + return [flattenAlternatives('anyOf', schema.anyOf, downAncestors), schema.discriminator]; } if (schema.oneOf) { - return [ - flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors), - noReference(schema.discriminator), - ]; + return [flattenAlternatives('oneOf', schema.oneOf, downAncestors), schema.discriminator]; } if (schema.allOf) { @@ -396,8 +385,8 @@ export function getSchemaTitle( // Try using the discriminator if (discriminator?.propertyName && schema.properties) { - const discriminatorProperty = noReference(schema.properties[discriminator.propertyName]); - if (discriminatorProperty) { + const discriminatorProperty = schema.properties[discriminator.propertyName]; + if (discriminatorProperty && !checkIsReference(discriminatorProperty)) { if (discriminatorProperty.enum) { return discriminatorProperty.enum.map((value) => value.toString()).join(' | '); } @@ -411,7 +400,7 @@ export function getSchemaTitle( type = 'enum'; // check array AND schema.items as this is sometimes null despite what the type indicates } else if (schema.type === 'array' && !!schema.items) { - type = `${getSchemaTitle(noReference(schema.items))}[]`; + type = `${getSchemaTitle(schema.items)}[]`; } else if (Array.isArray(schema.type)) { type = schema.type.join(' | '); } else if (schema.type || schema.properties) { diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index f0bb46056..c4a2571ed 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -8,7 +8,7 @@ import { OpenAPIResponses } from './OpenAPIResponses'; import { OpenAPISchemaProperties } from './OpenAPISchema'; import { OpenAPISecurities } from './OpenAPISecurities'; import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; -import { noReference, resolveDescription } from './utils'; +import { resolveDescription } from './utils'; /** * Client component to render the spec for the request and response. @@ -49,7 +49,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP example: parameter.example, // Deprecated can be defined at the parameter level deprecated: parameter.deprecated, - ...(noReference(parameter.schema) ?? {}), + ...(parameter.schema ?? {}), }, required: parameter.required, }; @@ -61,13 +61,10 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP })} {operation.requestBody ? ( - + ) : null} {operation.responses ? ( - + ) : null} ); diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx index 7460c8eb8..e2a5b3ebb 100644 --- a/packages/react-openapi/src/OpenAPITabs.tsx +++ b/packages/react-openapi/src/OpenAPITabs.tsx @@ -1,8 +1,10 @@ 'use client'; -import { createContext, useContext, useMemo, useState } from 'react'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components'; import { Markdown } from './Markdown'; +import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState'; +import { useIntersectionObserver } from 'usehooks-ts'; export type Tab = { key: Key; @@ -13,8 +15,7 @@ export type Tab = { type OpenAPITabsContextData = { items: Tab[]; - selectedKey: Key; - setSelectedKey: (key: Key) => void; + selectedTab: Tab; }; const OpenAPITabsContext = createContext(null); @@ -30,25 +31,66 @@ function useOpenAPITabsContext() { /** * The OpenAPI Tabs wrapper component. */ -export function OpenAPITabs(props: React.PropsWithChildren) { - const { children, items } = props; - - const [selectedKey, setSelectedKey] = useState(() => { - const firstItem = items[0]; - if (!firstItem) { - throw new Error('OpenAPITabs: at least one tab is required'); +export function OpenAPITabs( + props: React.PropsWithChildren, +) { + const { children, items, stateKey } = props; + const isVisible = stateKey + ? useIntersectionObserver({ + threshold: 0.1, + rootMargin: '200px', + }) + : true; + const defaultTab = items[0] as Tab; + const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState(); + const [selectedTabKey, setSelectedTabKey] = useState(() => { + if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) { + const tabFromState = syncedTabs.get(stateKey); + return tabFromState?.key ?? items[0]?.key; } - return firstItem.key; + return items[0]?.key; }); + const [selectedTab, setSelectedTab] = useState(defaultTab); - const contextValue = { items, selectedKey, setSelectedKey }; + const handleSelectionChange = (key: Key) => { + setSelectedTabKey(key); + if (stateKey) { + const tab = items.find((item) => item.key === key); + + if (!tab) { + return; + } + + setSyncedTabs((state) => { + const newState = new Map(state); + newState.set(stateKey, tab); + return newState; + }); + } + }; + + useEffect(() => { + if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) { + const tabFromState = syncedTabs.get(stateKey); + + if (!items.some((item) => item.key === tabFromState?.key)) { + return; + } + + if (tabFromState && tabFromState?.key !== selectedTab?.key) { + setSelectedTab(tabFromState); + } + } + }, [isVisible, stateKey, syncedTabs, selectedTabKey]); + + const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]); return ( {children} @@ -90,23 +132,21 @@ export function OpenAPITabsList() { * It renders the content of the selected tab. */ export function OpenAPITabsPanels() { - const { selectedKey, items } = useOpenAPITabsContext(); + const { selectedTab } = useOpenAPITabsContext(); - const tab = useMemo(() => items.find((tab) => tab.key === selectedKey), [items, selectedKey]); - - if (!tab) { + if (!selectedTab) { return null; } return ( - {tab.body} - {tab.description ? ( - + {selectedTab.body} + {selectedTab.description ? ( + ) : null} ); diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index 9c0642e92..9d9c26acd 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -1,5 +1,4 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; -import { noReference } from './utils'; import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters'; type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; @@ -50,13 +49,13 @@ export function generateMediaTypeExample( if (key) { const example = mediaType.examples[key]; if (example) { - return noReference(example).value; + return example.value; } } } if (mediaType.schema) { - return generateSchemaExample(noReference(mediaType.schema), options); + return generateSchemaExample(mediaType.schema, options); } return undefined; diff --git a/packages/react-openapi/src/resolveOpenAPIOperation.ts b/packages/react-openapi/src/resolveOpenAPIOperation.ts index 53435d240..43fda24b3 100644 --- a/packages/react-openapi/src/resolveOpenAPIOperation.ts +++ b/packages/react-openapi/src/resolveOpenAPIOperation.ts @@ -7,8 +7,8 @@ import { type OpenAPIV3_1, dereference, } from '@gitbook/openapi-parser'; -import { noReference } from './utils'; import { OpenAPIOperationData } from './types'; +import { checkIsReference } from './utils'; export { toJSON, fromJSON }; @@ -48,8 +48,8 @@ export async function resolveOpenAPIOperation( const securityKey = Object.keys(entry)[0]; if (securityKey) { const securityScheme = schema.components?.securitySchemes?.[securityKey]; - if (securityScheme) { - securities.push([securityKey, noReference(securityScheme)]); + if (securityScheme && !checkIsReference(securityScheme)) { + securities.push([securityKey, securityScheme]); } } } @@ -116,12 +116,13 @@ function getPathObject( function getPathObjectParameter( schema: OpenAPIV3.Document | OpenAPIV3_1.Document, path: string, -): OpenAPIV3.ParameterObject[] | OpenAPIV3_1.ParameterObject[] | null { +): + | (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] + | (OpenAPIV3.ParameterObject | OpenAPIV3_1.ReferenceObject)[] + | null { const pathObject = getPathObject(schema, path); if (pathObject?.parameters) { - return pathObject.parameters.map(noReference) as - | OpenAPIV3.ParameterObject[] - | OpenAPIV3_1.ParameterObject[]; + return pathObject.parameters; } return null; } diff --git a/packages/react-openapi/src/useSyncedTabsGlobalState.ts b/packages/react-openapi/src/useSyncedTabsGlobalState.ts new file mode 100644 index 000000000..fff4674bb --- /dev/null +++ b/packages/react-openapi/src/useSyncedTabsGlobalState.ts @@ -0,0 +1,23 @@ +'use client'; + +import { create } from 'zustand'; + +interface SyncedTabsState { + tabs: Map; + setTabs: (updater: (tabs: Map) => Map) => void; +} + +const useSyncedTabsStore = create>()((set) => ({ + tabs: new Map(), + setTabs: (updater) => + set((state) => ({ + tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity + })), +})); + +// Selector for better performance - only re-renders when tabs change +export function useSyncedTabsGlobalState() { + const tabs = useSyncedTabsStore((state) => state.tabs as Map); + const setTabs = useSyncedTabsStore((state) => state.setTabs as SyncedTabsState['setTabs']); + return [tabs, setTabs] as const; +} diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index bf4de4f1f..e9ca30330 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -1,13 +1,5 @@ import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser'; -export function noReference(input: T | OpenAPIV3.ReferenceObject): T { - if (checkIsReference(input)) { - throw new Error('Reference found'); - } - - return input; -} - export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject { return typeof input === 'object' && !!input && '$ref' in input; }