mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Fix schema validation error (#2820)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Sync tabs across all OpenAPI blocks
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Support for OpenAPI references
|
||||
@@ -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=="],
|
||||
|
||||
@@ -20,5 +20,6 @@ export async function createFileSystem(input: {
|
||||
const { filesystem } = await load(input.value, {
|
||||
plugins: [fetchURLs({ rootURL: input.rootURL })],
|
||||
});
|
||||
|
||||
return filesystem;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
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<string, string>) => Record<string, string>) => {
|
||||
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();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<OpenAPITabs items={samples}>
|
||||
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
||||
<InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
|
||||
@@ -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 (
|
||||
<InteractiveSection
|
||||
header="Body"
|
||||
@@ -24,7 +28,7 @@ export function OpenAPIRequestBody(props: {
|
||||
label: contentType,
|
||||
body: (
|
||||
<OpenAPIRootSchema
|
||||
schema={noReference(mediaTypeObject.schema) ?? {}}
|
||||
schema={mediaTypeObject.schema ?? {}}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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: {
|
||||
<OpenAPISchemaProperties
|
||||
properties={headers.map(([name, header]) => ({
|
||||
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: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<OpenAPITabs items={examples}>
|
||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
|
||||
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
|
||||
<OpenAPITabsPanels />
|
||||
</InteractiveSection>
|
||||
|
||||
@@ -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 (
|
||||
<InteractiveSection
|
||||
stateKey={createStateKey('response', context.blockKey)}
|
||||
header="Responses"
|
||||
className="openapi-responses"
|
||||
>
|
||||
<InteractiveSection header="Responses" className="openapi-responses">
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={Object.entries(responses).map(
|
||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||
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}
|
||||
</span>
|
||||
{description ? (
|
||||
<div className="openapi-markdown openapi-response-description">
|
||||
{htmlToText(description)}
|
||||
</div>
|
||||
<Markdown
|
||||
source={description}
|
||||
className="openapi-response-description"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
@@ -64,8 +61,3 @@ export function OpenAPIResponses(props: {
|
||||
</InteractiveSection>
|
||||
);
|
||||
}
|
||||
|
||||
function htmlToText(html: string): string {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.textContent?.trim() || '';
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<OpenAPISchemaPresentation {...props} />
|
||||
{alternatives[0].map((alternative, index) => (
|
||||
<OpenAPISchemaAlternative
|
||||
key={index}
|
||||
key={`alternative-${index}`}
|
||||
schema={alternative}
|
||||
circularRefs={circularRefs}
|
||||
context={context}
|
||||
@@ -278,9 +278,9 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
||||
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
|
||||
if (schema.allOf) {
|
||||
return schema.allOf.reduce((acc, subSchema) => {
|
||||
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) {
|
||||
|
||||
@@ -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 ? (
|
||||
<OpenAPIRequestBody
|
||||
requestBody={noReference(operation.requestBody)}
|
||||
context={context}
|
||||
/>
|
||||
<OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
|
||||
) : null}
|
||||
{operation.responses ? (
|
||||
<OpenAPIResponses responses={noReference(operation.responses)} context={context} />
|
||||
<OpenAPIResponses responses={operation.responses} context={context} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<OpenAPITabsContextData | null>(null);
|
||||
@@ -30,25 +31,66 @@ function useOpenAPITabsContext() {
|
||||
/**
|
||||
* The OpenAPI Tabs wrapper component.
|
||||
*/
|
||||
export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
|
||||
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<TabsProps & { items: Tab[]; stateKey?: string }>,
|
||||
) {
|
||||
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<Tab>();
|
||||
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<Tab>(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 (
|
||||
<OpenAPITabsContext.Provider value={contextValue}>
|
||||
<Tabs
|
||||
className="openapi-tabs"
|
||||
onSelectionChange={setSelectedKey}
|
||||
selectedKey={selectedKey}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
selectedKey={selectedTab?.key}
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
@@ -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 (
|
||||
<TabPanel
|
||||
key={`TabPanel-${tab.key}`}
|
||||
id={tab.key.toString()}
|
||||
key={`TabPanel-${selectedTab.key}`}
|
||||
id={selectedTab.key.toString()}
|
||||
className="openapi-tabs-panel"
|
||||
>
|
||||
{tab.body}
|
||||
{tab.description ? (
|
||||
<Markdown source={tab.description} className="openapi-tabs-footer" />
|
||||
{selectedTab.body}
|
||||
{selectedTab.description ? (
|
||||
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
||||
) : null}
|
||||
</TabPanel>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface SyncedTabsState<T> {
|
||||
tabs: Map<string, T>;
|
||||
setTabs: (updater: (tabs: Map<string, T>) => Map<string, T>) => void;
|
||||
}
|
||||
|
||||
const useSyncedTabsStore = create<SyncedTabsState<any>>()((set) => ({
|
||||
tabs: new Map<string, any>(),
|
||||
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<T>() {
|
||||
const tabs = useSyncedTabsStore((state) => state.tabs as Map<string, T>);
|
||||
const setTabs = useSyncedTabsStore((state) => state.setTabs as SyncedTabsState<T>['setTabs']);
|
||||
return [tabs, setTabs] as const;
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
|
||||
export function noReference<T>(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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user