@gitbook/react-openapi 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/InteractiveSection.d.ts +0 -2
- package/dist/InteractiveSection.jsx +8 -44
- package/dist/OpenAPICodeSample.jsx +9 -11
- package/dist/OpenAPIRequestBody.d.ts +2 -2
- package/dist/OpenAPIRequestBody.jsx +5 -2
- package/dist/OpenAPIResponse.jsx +4 -14
- package/dist/OpenAPIResponseExample.jsx +5 -5
- package/dist/OpenAPIResponses.jsx +2 -3
- package/dist/OpenAPISchema.jsx +15 -25
- package/dist/OpenAPISpec.jsx +4 -4
- package/dist/OpenAPITabs.d.ts +1 -0
- package/dist/OpenAPITabs.jsx +52 -16
- package/dist/generateSchemaExample.js +2 -3
- package/dist/resolveOpenAPIOperation.js +4 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/useSyncedTabsGlobalState.d.ts +1 -0
- package/dist/useSyncedTabsGlobalState.js +16 -0
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +0 -6
- package/package.json +3 -2
- package/src/InteractiveSection.tsx +4 -41
- package/src/OpenAPICodeSample.tsx +9 -11
- package/src/OpenAPIRequestBody.tsx +7 -3
- package/src/OpenAPIResponse.tsx +4 -18
- package/src/OpenAPIResponseExample.tsx +5 -5
- package/src/OpenAPIResponses.tsx +2 -7
- package/src/OpenAPISchema.tsx +16 -27
- package/src/OpenAPISpec.tsx +4 -7
- package/src/OpenAPITabs.tsx +63 -23
- package/src/generateSchemaExample.ts +2 -3
- package/src/resolveOpenAPIOperation.ts +8 -7
- package/src/useSyncedTabsGlobalState.ts +23 -0
- package/src/utils.ts +0 -8
- package/dist/fetchOpenAPIOperation.d.ts +0 -27
- package/dist/fetchOpenAPIOperation.js +0 -195
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useSyncedTabsGlobalState<T>(): readonly [Map<string, T>, (updater: (tabs: Map<string, T>) => Map<string, T>) => void];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
var useSyncedTabsStore = create()(function (set) { return ({
|
|
4
|
+
tabs: new Map(),
|
|
5
|
+
setTabs: function (updater) {
|
|
6
|
+
return set(function (state) { return ({
|
|
7
|
+
tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity
|
|
8
|
+
}); });
|
|
9
|
+
},
|
|
10
|
+
}); });
|
|
11
|
+
// Selector for better performance - only re-renders when tabs change
|
|
12
|
+
export function useSyncedTabsGlobalState() {
|
|
13
|
+
var tabs = useSyncedTabsStore(function (state) { return state.tabs; });
|
|
14
|
+
var setTabs = useSyncedTabsStore(function (state) { return state.setTabs; });
|
|
15
|
+
return [tabs, setTabs];
|
|
16
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
-
export declare function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T;
|
|
3
2
|
export declare function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject;
|
|
4
3
|
export declare function createStateKey(key: string, scope?: string): string;
|
|
5
4
|
/**
|
package/dist/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"default": "./dist/index.js"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"version": "1.0.
|
|
11
|
+
"version": "1.0.1",
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@gitbook/openapi-parser": "workspace:*",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"flatted": "^3.2.9",
|
|
19
19
|
"react-aria-components": "^1.6.0",
|
|
20
20
|
"react-aria": "^3.37.0",
|
|
21
|
-
"usehooks-ts": "^3.1.0"
|
|
21
|
+
"usehooks-ts": "^3.1.0",
|
|
22
|
+
"zustand": "^5.0.3"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"bun-types": "^1.1.20",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import {
|
|
4
|
+
import { useRef, useState } from 'react';
|
|
5
5
|
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
|
6
6
|
import { useDisclosureState } from 'react-stately';
|
|
7
7
|
|
|
@@ -11,30 +11,6 @@ interface InteractiveSectionTab {
|
|
|
11
11
|
body: React.ReactNode;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
let globalState: Record<string, string> = {};
|
|
15
|
-
const listeners = new Set<() => void>();
|
|
16
|
-
|
|
17
|
-
function useSyncedTabsGlobalState() {
|
|
18
|
-
const subscribe = useCallback((callback: () => void) => {
|
|
19
|
-
listeners.add(callback);
|
|
20
|
-
return () => listeners.delete(callback);
|
|
21
|
-
}, []);
|
|
22
|
-
|
|
23
|
-
const getSnapshot = useCallback(() => globalState, []);
|
|
24
|
-
|
|
25
|
-
const setSyncedTabs = useCallback(
|
|
26
|
-
(updater: (tabs: Record<string, string>) => Record<string, string>) => {
|
|
27
|
-
globalState = updater(globalState);
|
|
28
|
-
listeners.forEach((listener) => listener());
|
|
29
|
-
},
|
|
30
|
-
[],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const tabs = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
34
|
-
|
|
35
|
-
return [tabs, setSyncedTabs] as const;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
14
|
/**
|
|
39
15
|
* To optimize rendering, most of the components are server-components,
|
|
40
16
|
* and the interactiveness is mainly handled by a few key components like this one.
|
|
@@ -59,8 +35,6 @@ export function InteractiveSection(props: {
|
|
|
59
35
|
children?: React.ReactNode;
|
|
60
36
|
/** Children to display within the container */
|
|
61
37
|
overlay?: React.ReactNode;
|
|
62
|
-
/** An optional key referencing a value in global state */
|
|
63
|
-
stateKey?: string;
|
|
64
38
|
}) {
|
|
65
39
|
const {
|
|
66
40
|
id,
|
|
@@ -73,16 +47,11 @@ export function InteractiveSection(props: {
|
|
|
73
47
|
children,
|
|
74
48
|
overlay,
|
|
75
49
|
toggleIcon = '▶',
|
|
76
|
-
stateKey,
|
|
77
50
|
} = props;
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
stateKey && stateKey in syncedTabs
|
|
81
|
-
? tabs.find((tab) => tab.key === syncedTabs[stateKey])
|
|
82
|
-
: undefined;
|
|
83
|
-
const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab);
|
|
51
|
+
|
|
52
|
+
const [selectedTabKey, setSelectedTab] = useState(defaultTab);
|
|
84
53
|
const selectedTab: InteractiveSectionTab | undefined =
|
|
85
|
-
|
|
54
|
+
tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
|
86
55
|
|
|
87
56
|
const state = useDisclosureState({
|
|
88
57
|
defaultExpanded: defaultOpened,
|
|
@@ -153,12 +122,6 @@ export function InteractiveSection(props: {
|
|
|
153
122
|
value={selectedTab?.key ?? ''}
|
|
154
123
|
onChange={(event) => {
|
|
155
124
|
setSelectedTab(event.target.value);
|
|
156
|
-
if (stateKey) {
|
|
157
|
-
setSyncedTabs((state) => ({
|
|
158
|
-
...state,
|
|
159
|
-
[stateKey]: event.target.value,
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
125
|
state.expand();
|
|
163
126
|
}}
|
|
164
127
|
>
|
|
@@ -3,9 +3,10 @@ import { generateMediaTypeExample, generateSchemaExample } from './generateSchem
|
|
|
3
3
|
import { InteractiveSection } from './InteractiveSection';
|
|
4
4
|
import { getServersURL } from './OpenAPIServerURL';
|
|
5
5
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
6
|
-
import {
|
|
6
|
+
import { createStateKey } from './utils';
|
|
7
7
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
8
8
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
9
|
+
import { checkIsReference } from './utils';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Display code samples to execute the operation.
|
|
@@ -20,24 +21,19 @@ export function OpenAPICodeSample(props: {
|
|
|
20
21
|
const searchParams = new URLSearchParams();
|
|
21
22
|
const headersObject: { [k: string]: string } = {};
|
|
22
23
|
|
|
23
|
-
data.operation.parameters?.forEach((
|
|
24
|
-
const param = noReference(rawParam);
|
|
24
|
+
data.operation.parameters?.forEach((param) => {
|
|
25
25
|
if (!param) {
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
if (param.in === 'header' && param.required) {
|
|
30
|
-
const example = param.schema
|
|
31
|
-
? generateSchemaExample(noReference(param.schema))
|
|
32
|
-
: undefined;
|
|
30
|
+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
|
|
33
31
|
if (example !== undefined && param.name) {
|
|
34
32
|
headersObject[param.name] =
|
|
35
33
|
typeof example !== 'string' ? stringifyOpenAPI(example) : example;
|
|
36
34
|
}
|
|
37
35
|
} else if (param.in === 'query' && param.required) {
|
|
38
|
-
const example = param.schema
|
|
39
|
-
? generateSchemaExample(noReference(param.schema))
|
|
40
|
-
: undefined;
|
|
36
|
+
const example = param.schema ? generateSchemaExample(param.schema) : undefined;
|
|
41
37
|
if (example !== undefined && param.name) {
|
|
42
38
|
searchParams.append(
|
|
43
39
|
param.name,
|
|
@@ -47,7 +43,9 @@ export function OpenAPICodeSample(props: {
|
|
|
47
43
|
}
|
|
48
44
|
});
|
|
49
45
|
|
|
50
|
-
const requestBody =
|
|
46
|
+
const requestBody = !checkIsReference(data.operation.requestBody)
|
|
47
|
+
? data.operation.requestBody
|
|
48
|
+
: undefined;
|
|
51
49
|
const requestBodyContentEntries = requestBody?.content
|
|
52
50
|
? Object.entries(requestBody.content)
|
|
53
51
|
: undefined;
|
|
@@ -115,7 +113,7 @@ export function OpenAPICodeSample(props: {
|
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
return (
|
|
118
|
-
<OpenAPITabs items={samples}>
|
|
116
|
+
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
|
119
117
|
<InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
|
|
120
118
|
<OpenAPITabsPanels />
|
|
121
119
|
</InteractiveSection>
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
2
|
import { OpenAPIRootSchema } from './OpenAPISchema';
|
|
3
|
-
import { noReference } from './utils';
|
|
4
3
|
import type { OpenAPIClientContext } from './types';
|
|
5
4
|
import { InteractiveSection } from './InteractiveSection';
|
|
5
|
+
import { checkIsReference } from './utils';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Display an interactive request body.
|
|
9
9
|
*/
|
|
10
10
|
export function OpenAPIRequestBody(props: {
|
|
11
|
-
requestBody: OpenAPIV3.RequestBodyObject;
|
|
11
|
+
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
|
|
12
12
|
context: OpenAPIClientContext;
|
|
13
13
|
}) {
|
|
14
14
|
const { requestBody, context } = props;
|
|
15
15
|
|
|
16
|
+
if (checkIsReference(requestBody)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
return (
|
|
17
21
|
<InteractiveSection
|
|
18
22
|
header="Body"
|
|
@@ -24,7 +28,7 @@ export function OpenAPIRequestBody(props: {
|
|
|
24
28
|
label: contentType,
|
|
25
29
|
body: (
|
|
26
30
|
<OpenAPIRootSchema
|
|
27
|
-
schema={
|
|
31
|
+
schema={mediaTypeObject.schema ?? {}}
|
|
28
32
|
context={context}
|
|
29
33
|
/>
|
|
30
34
|
),
|
package/src/OpenAPIResponse.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
2
|
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveDescription } from './utils';
|
|
4
4
|
import type { OpenAPIClientContext } from './types';
|
|
5
5
|
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ export function OpenAPIResponse(props: {
|
|
|
14
14
|
}) {
|
|
15
15
|
const { response, context, mediaType } = props;
|
|
16
16
|
const headers = Object.entries(response.headers ?? {}).map(
|
|
17
|
-
([name, header]) => [name,
|
|
17
|
+
([name, header]) => [name, header ?? {}] as const,
|
|
18
18
|
);
|
|
19
19
|
const content = Object.entries(mediaType.schema ?? {});
|
|
20
20
|
|
|
@@ -31,7 +31,7 @@ export function OpenAPIResponse(props: {
|
|
|
31
31
|
<OpenAPISchemaProperties
|
|
32
32
|
properties={headers.map(([name, header]) => ({
|
|
33
33
|
propertyName: name,
|
|
34
|
-
schema:
|
|
34
|
+
schema: header.schema ?? {},
|
|
35
35
|
required: header.required,
|
|
36
36
|
}))}
|
|
37
37
|
context={context}
|
|
@@ -43,7 +43,7 @@ export function OpenAPIResponse(props: {
|
|
|
43
43
|
id={`response-${context.blockKey}`}
|
|
44
44
|
properties={[
|
|
45
45
|
{
|
|
46
|
-
schema:
|
|
46
|
+
schema: mediaType.schema ?? {},
|
|
47
47
|
},
|
|
48
48
|
]}
|
|
49
49
|
context={context}
|
|
@@ -52,17 +52,3 @@ export function OpenAPIResponse(props: {
|
|
|
52
52
|
</div>
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
function handleUnresolvedReference(
|
|
57
|
-
input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
58
|
-
): OpenAPIV3.SchemaObject {
|
|
59
|
-
const isReference = checkIsReference(input);
|
|
60
|
-
|
|
61
|
-
if (isReference || input === undefined) {
|
|
62
|
-
// If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it.
|
|
63
|
-
// Instead we render `any`
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return input;
|
|
68
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
2
|
import { generateSchemaExample } from './generateSchemaExample';
|
|
3
3
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
4
|
-
import { checkIsReference,
|
|
4
|
+
import { checkIsReference, createStateKey, resolveDescription } from './utils';
|
|
5
5
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
6
6
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
7
7
|
import { InteractiveSection } from './InteractiveSection';
|
|
@@ -40,7 +40,7 @@ export function OpenAPIResponseExample(props: {
|
|
|
40
40
|
|
|
41
41
|
const examples = responses
|
|
42
42
|
.map(([key, value]) => {
|
|
43
|
-
const responseObject =
|
|
43
|
+
const responseObject = value;
|
|
44
44
|
const mediaTypeObject = (() => {
|
|
45
45
|
if (!responseObject.content) {
|
|
46
46
|
return null;
|
|
@@ -68,7 +68,7 @@ export function OpenAPIResponseExample(props: {
|
|
|
68
68
|
const key = Object.keys(examples)[0];
|
|
69
69
|
if (key) {
|
|
70
70
|
// @TODO handle multiple examples
|
|
71
|
-
const firstExample =
|
|
71
|
+
const firstExample = examples[key];
|
|
72
72
|
if (firstExample) {
|
|
73
73
|
return firstExample;
|
|
74
74
|
}
|
|
@@ -79,7 +79,7 @@ export function OpenAPIResponseExample(props: {
|
|
|
79
79
|
return { value: example };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const schema =
|
|
82
|
+
const schema = mediaTypeObject.schema;
|
|
83
83
|
if (!schema) {
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
@@ -115,7 +115,7 @@ export function OpenAPIResponseExample(props: {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
|
-
<OpenAPITabs items={examples}>
|
|
118
|
+
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
|
|
119
119
|
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
|
|
120
120
|
<OpenAPITabsPanels />
|
|
121
121
|
</InteractiveSection>
|
package/src/OpenAPIResponses.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
|
2
|
-
import { createStateKey, resolveDescription } from './utils';
|
|
3
2
|
import { OpenAPIResponse } from './OpenAPIResponse';
|
|
4
3
|
import { OpenAPIClientContext } from './types';
|
|
5
4
|
import { InteractiveSection } from './InteractiveSection';
|
|
@@ -16,18 +15,14 @@ export function OpenAPIResponses(props: {
|
|
|
16
15
|
const { responses, context } = props;
|
|
17
16
|
|
|
18
17
|
return (
|
|
19
|
-
<InteractiveSection
|
|
20
|
-
stateKey={createStateKey('response', context.blockKey)}
|
|
21
|
-
header="Responses"
|
|
22
|
-
className="openapi-responses"
|
|
23
|
-
>
|
|
18
|
+
<InteractiveSection header="Responses" className="openapi-responses">
|
|
24
19
|
<OpenAPIDisclosureGroup
|
|
25
20
|
allowsMultipleExpanded
|
|
26
21
|
icon={context.icons.chevronRight}
|
|
27
22
|
groups={Object.entries(responses).map(
|
|
28
23
|
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
|
29
24
|
const content = Object.entries(response.content ?? {});
|
|
30
|
-
const description =
|
|
25
|
+
const description = response.description;
|
|
31
26
|
|
|
32
27
|
return {
|
|
33
28
|
id: statusCode,
|
package/src/OpenAPISchema.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { useId } from 'react';
|
|
|
5
5
|
import { InteractiveSection } from './InteractiveSection';
|
|
6
6
|
import { Markdown } from './Markdown';
|
|
7
7
|
import type { OpenAPIClientContext } from './types';
|
|
8
|
-
import { checkIsReference,
|
|
8
|
+
import { checkIsReference, resolveDescription } from './utils';
|
|
9
9
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
10
10
|
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
|
11
11
|
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
|
|
@@ -51,15 +51,15 @@ export function OpenAPISchemaProperty(
|
|
|
51
51
|
return (
|
|
52
52
|
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
|
|
53
53
|
<OpenAPISchemaPresentation {...props} />
|
|
54
|
-
|
|
55
|
-
{
|
|
54
|
+
{properties && properties.length > 0 ? (
|
|
55
|
+
<OpenAPIDisclosure context={context}>
|
|
56
56
|
<OpenAPISchemaProperties
|
|
57
57
|
properties={properties}
|
|
58
58
|
circularRefs={circularRefs}
|
|
59
59
|
context={context}
|
|
60
60
|
/>
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
</OpenAPIDisclosure>
|
|
62
|
+
) : null}
|
|
63
63
|
</InteractiveSection>
|
|
64
64
|
);
|
|
65
65
|
}
|
|
@@ -70,7 +70,7 @@ export function OpenAPISchemaProperty(
|
|
|
70
70
|
<OpenAPISchemaPresentation {...props} />
|
|
71
71
|
{alternatives[0].map((alternative, index) => (
|
|
72
72
|
<OpenAPISchemaAlternative
|
|
73
|
-
key={index}
|
|
73
|
+
key={`alternative-${index}`}
|
|
74
74
|
schema={alternative}
|
|
75
75
|
circularRefs={circularRefs}
|
|
76
76
|
context={context}
|
|
@@ -278,9 +278,9 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
|
|
|
278
278
|
function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
|
|
279
279
|
if (schema.allOf) {
|
|
280
280
|
return schema.allOf.reduce((acc, subSchema) => {
|
|
281
|
-
const properties = getSchemaProperties(
|
|
281
|
+
const properties = getSchemaProperties(subSchema) ?? [
|
|
282
282
|
{
|
|
283
|
-
schema:
|
|
283
|
+
schema: subSchema,
|
|
284
284
|
},
|
|
285
285
|
];
|
|
286
286
|
return [...acc, ...properties];
|
|
@@ -289,7 +289,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
|
|
|
289
289
|
|
|
290
290
|
// check array AND schema.items as this is sometimes null despite what the type indicates
|
|
291
291
|
if (schema.type === 'array' && !!schema.items) {
|
|
292
|
-
const items =
|
|
292
|
+
const items = schema.items;
|
|
293
293
|
const itemProperties = getSchemaProperties(items);
|
|
294
294
|
if (itemProperties) {
|
|
295
295
|
return itemProperties;
|
|
@@ -307,12 +307,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
|
|
|
307
307
|
const result: OpenAPISchemaPropertyEntry[] = [];
|
|
308
308
|
|
|
309
309
|
if (schema.properties) {
|
|
310
|
-
Object.entries(schema.properties).forEach(([propertyName,
|
|
311
|
-
const isReference = checkIsReference(rawPropertySchema);
|
|
312
|
-
const propertySchema: OpenAPIV3.SchemaObject = isReference
|
|
313
|
-
? { propertyName }
|
|
314
|
-
: rawPropertySchema;
|
|
315
|
-
|
|
310
|
+
Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
|
|
316
311
|
result.push({
|
|
317
312
|
propertyName,
|
|
318
313
|
required: Array.isArray(schema.required)
|
|
@@ -324,7 +319,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
|
|
|
324
319
|
}
|
|
325
320
|
|
|
326
321
|
if (schema.additionalProperties) {
|
|
327
|
-
const additionalProperties =
|
|
322
|
+
const additionalProperties = schema.additionalProperties;
|
|
328
323
|
|
|
329
324
|
result.push({
|
|
330
325
|
propertyName: 'Other properties',
|
|
@@ -348,17 +343,11 @@ export function getSchemaAlternatives(
|
|
|
348
343
|
const downAncestors = new Set(ancestors).add(schema);
|
|
349
344
|
|
|
350
345
|
if (schema.anyOf) {
|
|
351
|
-
return [
|
|
352
|
-
flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors),
|
|
353
|
-
noReference(schema.discriminator),
|
|
354
|
-
];
|
|
346
|
+
return [flattenAlternatives('anyOf', schema.anyOf, downAncestors), schema.discriminator];
|
|
355
347
|
}
|
|
356
348
|
|
|
357
349
|
if (schema.oneOf) {
|
|
358
|
-
return [
|
|
359
|
-
flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors),
|
|
360
|
-
noReference(schema.discriminator),
|
|
361
|
-
];
|
|
350
|
+
return [flattenAlternatives('oneOf', schema.oneOf, downAncestors), schema.discriminator];
|
|
362
351
|
}
|
|
363
352
|
|
|
364
353
|
if (schema.allOf) {
|
|
@@ -396,8 +385,8 @@ export function getSchemaTitle(
|
|
|
396
385
|
|
|
397
386
|
// Try using the discriminator
|
|
398
387
|
if (discriminator?.propertyName && schema.properties) {
|
|
399
|
-
const discriminatorProperty =
|
|
400
|
-
if (discriminatorProperty) {
|
|
388
|
+
const discriminatorProperty = schema.properties[discriminator.propertyName];
|
|
389
|
+
if (discriminatorProperty && !checkIsReference(discriminatorProperty)) {
|
|
401
390
|
if (discriminatorProperty.enum) {
|
|
402
391
|
return discriminatorProperty.enum.map((value) => value.toString()).join(' | ');
|
|
403
392
|
}
|
|
@@ -411,7 +400,7 @@ export function getSchemaTitle(
|
|
|
411
400
|
type = 'enum';
|
|
412
401
|
// check array AND schema.items as this is sometimes null despite what the type indicates
|
|
413
402
|
} else if (schema.type === 'array' && !!schema.items) {
|
|
414
|
-
type = `${getSchemaTitle(
|
|
403
|
+
type = `${getSchemaTitle(schema.items)}[]`;
|
|
415
404
|
} else if (Array.isArray(schema.type)) {
|
|
416
405
|
type = schema.type.join(' | ');
|
|
417
406
|
} else if (schema.type || schema.properties) {
|
package/src/OpenAPISpec.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { OpenAPIResponses } from './OpenAPIResponses';
|
|
|
8
8
|
import { OpenAPISchemaProperties } from './OpenAPISchema';
|
|
9
9
|
import { OpenAPISecurities } from './OpenAPISecurities';
|
|
10
10
|
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
|
|
11
|
-
import {
|
|
11
|
+
import { resolveDescription } from './utils';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Client component to render the spec for the request and response.
|
|
@@ -49,7 +49,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
|
|
49
49
|
example: parameter.example,
|
|
50
50
|
// Deprecated can be defined at the parameter level
|
|
51
51
|
deprecated: parameter.deprecated,
|
|
52
|
-
...(
|
|
52
|
+
...(parameter.schema ?? {}),
|
|
53
53
|
},
|
|
54
54
|
required: parameter.required,
|
|
55
55
|
};
|
|
@@ -61,13 +61,10 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
|
|
61
61
|
})}
|
|
62
62
|
|
|
63
63
|
{operation.requestBody ? (
|
|
64
|
-
<OpenAPIRequestBody
|
|
65
|
-
requestBody={noReference(operation.requestBody)}
|
|
66
|
-
context={context}
|
|
67
|
-
/>
|
|
64
|
+
<OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
|
|
68
65
|
) : null}
|
|
69
66
|
{operation.responses ? (
|
|
70
|
-
<OpenAPIResponses responses={
|
|
67
|
+
<OpenAPIResponses responses={operation.responses} context={context} />
|
|
71
68
|
) : null}
|
|
72
69
|
</>
|
|
73
70
|
);
|
package/src/OpenAPITabs.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useMemo, useState } from 'react';
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
|
|
5
5
|
import { Markdown } from './Markdown';
|
|
6
|
+
import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
|
|
7
|
+
import { useIntersectionObserver } from 'usehooks-ts';
|
|
6
8
|
|
|
7
9
|
export type Tab = {
|
|
8
10
|
key: Key;
|
|
@@ -13,8 +15,7 @@ export type Tab = {
|
|
|
13
15
|
|
|
14
16
|
type OpenAPITabsContextData = {
|
|
15
17
|
items: Tab[];
|
|
16
|
-
|
|
17
|
-
setSelectedKey: (key: Key) => void;
|
|
18
|
+
selectedTab: Tab;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
|
|
@@ -30,25 +31,66 @@ function useOpenAPITabsContext() {
|
|
|
30
31
|
/**
|
|
31
32
|
* The OpenAPI Tabs wrapper component.
|
|
32
33
|
*/
|
|
33
|
-
export function OpenAPITabs(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
export function OpenAPITabs(
|
|
35
|
+
props: React.PropsWithChildren<TabsProps & { items: Tab[]; stateKey?: string }>,
|
|
36
|
+
) {
|
|
37
|
+
const { children, items, stateKey } = props;
|
|
38
|
+
const isVisible = stateKey
|
|
39
|
+
? useIntersectionObserver({
|
|
40
|
+
threshold: 0.1,
|
|
41
|
+
rootMargin: '200px',
|
|
42
|
+
})
|
|
43
|
+
: true;
|
|
44
|
+
const defaultTab = items[0] as Tab;
|
|
45
|
+
const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<Tab>();
|
|
46
|
+
const [selectedTabKey, setSelectedTabKey] = useState(() => {
|
|
47
|
+
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
|
|
48
|
+
const tabFromState = syncedTabs.get(stateKey);
|
|
49
|
+
return tabFromState?.key ?? items[0]?.key;
|
|
40
50
|
}
|
|
41
|
-
return
|
|
51
|
+
return items[0]?.key;
|
|
42
52
|
});
|
|
53
|
+
const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
|
|
54
|
+
|
|
55
|
+
const handleSelectionChange = (key: Key) => {
|
|
56
|
+
setSelectedTabKey(key);
|
|
57
|
+
if (stateKey) {
|
|
58
|
+
const tab = items.find((item) => item.key === key);
|
|
59
|
+
|
|
60
|
+
if (!tab) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setSyncedTabs((state) => {
|
|
65
|
+
const newState = new Map(state);
|
|
66
|
+
newState.set(stateKey, tab);
|
|
67
|
+
return newState;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
43
71
|
|
|
44
|
-
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
|
|
74
|
+
const tabFromState = syncedTabs.get(stateKey);
|
|
75
|
+
|
|
76
|
+
if (!items.some((item) => item.key === tabFromState?.key)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (tabFromState && tabFromState?.key !== selectedTab?.key) {
|
|
81
|
+
setSelectedTab(tabFromState);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, [isVisible, stateKey, syncedTabs, selectedTabKey]);
|
|
85
|
+
|
|
86
|
+
const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
|
|
45
87
|
|
|
46
88
|
return (
|
|
47
89
|
<OpenAPITabsContext.Provider value={contextValue}>
|
|
48
90
|
<Tabs
|
|
49
91
|
className="openapi-tabs"
|
|
50
|
-
onSelectionChange={
|
|
51
|
-
selectedKey={
|
|
92
|
+
onSelectionChange={handleSelectionChange}
|
|
93
|
+
selectedKey={selectedTab?.key}
|
|
52
94
|
>
|
|
53
95
|
{children}
|
|
54
96
|
</Tabs>
|
|
@@ -90,23 +132,21 @@ export function OpenAPITabsList() {
|
|
|
90
132
|
* It renders the content of the selected tab.
|
|
91
133
|
*/
|
|
92
134
|
export function OpenAPITabsPanels() {
|
|
93
|
-
const {
|
|
94
|
-
|
|
95
|
-
const tab = useMemo(() => items.find((tab) => tab.key === selectedKey), [items, selectedKey]);
|
|
135
|
+
const { selectedTab } = useOpenAPITabsContext();
|
|
96
136
|
|
|
97
|
-
if (!
|
|
137
|
+
if (!selectedTab) {
|
|
98
138
|
return null;
|
|
99
139
|
}
|
|
100
140
|
|
|
101
141
|
return (
|
|
102
142
|
<TabPanel
|
|
103
|
-
key={`TabPanel-${
|
|
104
|
-
id={
|
|
143
|
+
key={`TabPanel-${selectedTab.key}`}
|
|
144
|
+
id={selectedTab.key.toString()}
|
|
105
145
|
className="openapi-tabs-panel"
|
|
106
146
|
>
|
|
107
|
-
{
|
|
108
|
-
{
|
|
109
|
-
<Markdown source={
|
|
147
|
+
{selectedTab.body}
|
|
148
|
+
{selectedTab.description ? (
|
|
149
|
+
<Markdown source={selectedTab.description} className="openapi-tabs-footer" />
|
|
110
150
|
) : null}
|
|
111
151
|
</TabPanel>
|
|
112
152
|
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
-
import { noReference } from './utils';
|
|
3
2
|
import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
|
|
4
3
|
|
|
5
4
|
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
|
|
@@ -50,13 +49,13 @@ export function generateMediaTypeExample(
|
|
|
50
49
|
if (key) {
|
|
51
50
|
const example = mediaType.examples[key];
|
|
52
51
|
if (example) {
|
|
53
|
-
return
|
|
52
|
+
return example.value;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
if (mediaType.schema) {
|
|
59
|
-
return generateSchemaExample(
|
|
58
|
+
return generateSchemaExample(mediaType.schema, options);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
return undefined;
|