@gitbook/react-openapi 1.1.5 → 1.1.7
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 +34 -0
- package/dist/InteractiveSection.d.ts +0 -2
- package/dist/InteractiveSection.jsx +3 -4
- package/dist/OpenAPICodeSample.d.ts +9 -0
- package/dist/OpenAPICodeSample.jsx +117 -58
- package/dist/OpenAPICodeSampleInteractive.d.ts +11 -0
- package/dist/OpenAPICodeSampleInteractive.jsx +85 -0
- package/dist/OpenAPICopyButton.d.ts +7 -0
- package/dist/OpenAPICopyButton.jsx +6 -6
- package/dist/OpenAPIOperation.jsx +21 -1
- package/dist/OpenAPIPath.jsx +2 -2
- package/dist/OpenAPIRequestBody.jsx +1 -1
- package/dist/OpenAPIResponse.jsx +1 -1
- package/dist/OpenAPIResponses.jsx +2 -2
- package/dist/OpenAPISchema.d.ts +5 -14
- package/dist/OpenAPISchema.jsx +79 -28
- package/dist/OpenAPISchemaName.jsx +8 -6
- package/dist/OpenAPISchemaServer.d.ts +12 -0
- package/dist/OpenAPISchemaServer.jsx +8 -0
- package/dist/OpenAPISpec.d.ts +0 -6
- package/dist/OpenAPISpec.jsx +5 -11
- package/dist/OpenAPITabs.jsx +3 -11
- package/dist/code-samples.d.ts +1 -2
- package/dist/code-samples.js +46 -11
- package/dist/decycle.d.ts +2 -0
- package/dist/decycle.js +70 -0
- package/dist/generateSchemaExample.d.ts +31 -2
- package/dist/generateSchemaExample.js +307 -24
- package/dist/schemas/OpenAPISchemas.jsx +1 -1
- package/dist/schemas/resolveOpenAPISchemas.d.ts +2 -6
- package/dist/schemas/resolveOpenAPISchemas.js +1 -21
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types.d.ts +2 -5
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +11 -7
- package/package.json +3 -3
- package/src/InteractiveSection.tsx +2 -6
- package/src/OpenAPICodeSample.tsx +187 -78
- package/src/OpenAPICodeSampleInteractive.tsx +139 -0
- package/src/OpenAPICopyButton.tsx +17 -4
- package/src/OpenAPIOperation.tsx +39 -2
- package/src/OpenAPIPath.tsx +2 -2
- package/src/OpenAPIRequestBody.tsx +1 -1
- package/src/OpenAPIResponse.tsx +4 -4
- package/src/OpenAPIResponses.tsx +1 -5
- package/src/OpenAPISchema.tsx +152 -58
- package/src/OpenAPISchemaName.tsx +14 -6
- package/src/OpenAPISchemaServer.tsx +34 -0
- package/src/OpenAPISpec.tsx +13 -11
- package/src/OpenAPITabs.tsx +3 -13
- package/src/code-samples.test.ts +69 -1
- package/src/code-samples.ts +48 -12
- package/src/decycle.ts +68 -0
- package/src/generateSchemaExample.ts +412 -25
- package/src/resolveOpenAPIOperation.test.ts +6 -6
- package/src/schemas/OpenAPISchemas.tsx +1 -1
- package/src/schemas/resolveOpenAPISchemas.ts +3 -31
- package/src/types.ts +6 -6
- package/src/utils.ts +13 -10
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenAPICustomOperationProperties, OpenAPICustomSpecProperties, OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
1
|
+
import type { OpenAPICustomOperationProperties, OpenAPICustomSpecProperties, OpenAPISchema, OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
2
|
export interface OpenAPIContextProps extends OpenAPIClientContext {
|
|
3
3
|
/**
|
|
4
4
|
* Render a code block.
|
|
@@ -13,6 +13,7 @@ export interface OpenAPIContextProps extends OpenAPIClientContext {
|
|
|
13
13
|
renderHeading: (props: {
|
|
14
14
|
deprecated: boolean;
|
|
15
15
|
title: string;
|
|
16
|
+
stability?: string;
|
|
16
17
|
}) => React.ReactNode;
|
|
17
18
|
/**
|
|
18
19
|
* Render the document of the operation.
|
|
@@ -51,10 +52,6 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
|
|
51
52
|
/** Securities that should be used for this operation */
|
|
52
53
|
securities: [string, OpenAPIV3.SecuritySchemeObject][];
|
|
53
54
|
}
|
|
54
|
-
export type OpenAPISchema = {
|
|
55
|
-
name: string;
|
|
56
|
-
schema: OpenAPIV3.SchemaObject;
|
|
57
|
-
};
|
|
58
55
|
export interface OpenAPISchemasData {
|
|
59
56
|
/** Components schemas to be used for schemas */
|
|
60
57
|
schemas: OpenAPISchema[];
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
|
2
|
-
export declare function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject
|
|
2
|
+
export declare function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject;
|
|
3
3
|
export declare function createStateKey(key: string, scope?: string): string;
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the description of an object.
|
package/dist/utils.js
CHANGED
|
@@ -26,16 +26,20 @@ function hasDescription(object) {
|
|
|
26
26
|
* Resolve the description of an object.
|
|
27
27
|
*/
|
|
28
28
|
export function resolveDescription(object) {
|
|
29
|
-
//
|
|
29
|
+
// Resolve description from the object first
|
|
30
|
+
if (hasDescription(object)) {
|
|
31
|
+
return 'x-gitbook-description-html' in object &&
|
|
32
|
+
typeof object['x-gitbook-description-html'] === 'string'
|
|
33
|
+
? object['x-gitbook-description-html'].trim()
|
|
34
|
+
: typeof object.description === 'string'
|
|
35
|
+
? object.description.trim()
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
// If the object has no description, try to resolve it from the items
|
|
30
39
|
if ('items' in object && typeof object.items === 'object' && hasDescription(object.items)) {
|
|
31
40
|
return resolveDescription(object.items);
|
|
32
41
|
}
|
|
33
|
-
return
|
|
34
|
-
typeof object['x-gitbook-description-html'] === 'string'
|
|
35
|
-
? object['x-gitbook-description-html'].trim()
|
|
36
|
-
: typeof object.description === 'string'
|
|
37
|
-
? object.description.trim()
|
|
38
|
-
: undefined;
|
|
42
|
+
return undefined;
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Extract descriptions from an object.
|
package/package.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"default": "./dist/index.js"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"version": "1.1.
|
|
11
|
+
"version": "1.1.7",
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@gitbook/openapi-parser": "workspace:*",
|
|
15
|
-
"@scalar/api-client-react": "^1.
|
|
16
|
-
"@scalar/oas-utils": "^0.2.
|
|
15
|
+
"@scalar/api-client-react": "^1.2.5",
|
|
16
|
+
"@scalar/oas-utils": "^0.2.120",
|
|
17
17
|
"clsx": "^2.1.1",
|
|
18
18
|
"flatted": "^3.2.9",
|
|
19
19
|
"json-xml-parse": "^1.3.0",
|
|
@@ -32,8 +32,6 @@ export function InteractiveSection(props: {
|
|
|
32
32
|
defaultTab?: string;
|
|
33
33
|
/** Content of the header */
|
|
34
34
|
header?: React.ReactNode;
|
|
35
|
-
/** Body of the section */
|
|
36
|
-
children?: React.ReactNode;
|
|
37
35
|
/** Children to display within the container */
|
|
38
36
|
overlay?: React.ReactNode;
|
|
39
37
|
}) {
|
|
@@ -45,7 +43,6 @@ export function InteractiveSection(props: {
|
|
|
45
43
|
tabs = [],
|
|
46
44
|
defaultTab = tabs[0]?.key,
|
|
47
45
|
header,
|
|
48
|
-
children,
|
|
49
46
|
overlay,
|
|
50
47
|
toggleIcon = '▶',
|
|
51
48
|
} = props;
|
|
@@ -83,7 +80,7 @@ export function InteractiveSection(props: {
|
|
|
83
80
|
className={className}
|
|
84
81
|
>
|
|
85
82
|
<SectionHeaderContent className={className}>
|
|
86
|
-
{
|
|
83
|
+
{selectedTab?.body && toggeable ? (
|
|
87
84
|
<button
|
|
88
85
|
{...mergeProps(buttonProps, focusProps)}
|
|
89
86
|
ref={triggerRef}
|
|
@@ -131,9 +128,8 @@ export function InteractiveSection(props: {
|
|
|
131
128
|
</div>
|
|
132
129
|
</SectionHeader>
|
|
133
130
|
) : null}
|
|
134
|
-
{(!toggeable || state.isExpanded) &&
|
|
131
|
+
{(!toggeable || state.isExpanded) && selectedTab?.body ? (
|
|
135
132
|
<SectionBody ref={panelRef} {...panelProps} className={className}>
|
|
136
|
-
{children}
|
|
137
133
|
{selectedTab?.body}
|
|
138
134
|
</SectionBody>
|
|
139
135
|
) : null}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
+
import {
|
|
3
|
+
OpenAPIMediaTypeExamplesBody,
|
|
4
|
+
OpenAPIMediaTypeExamplesSelector,
|
|
5
|
+
} from './OpenAPICodeSampleInteractive';
|
|
2
6
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
3
7
|
import { ScalarApiButton } from './ScalarApiButton';
|
|
4
8
|
import { StaticSection } from './StaticSection';
|
|
5
|
-
import { type
|
|
6
|
-
import {
|
|
9
|
+
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
|
|
10
|
+
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
|
7
11
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
8
12
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
9
13
|
import { getDefaultServerURL } from './util/server';
|
|
10
14
|
import { checkIsReference, createStateKey } from './utils';
|
|
11
15
|
|
|
16
|
+
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* Display code samples to execute the operation.
|
|
14
20
|
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
|
|
@@ -16,6 +22,43 @@ import { checkIsReference, createStateKey } from './utils';
|
|
|
16
22
|
export function OpenAPICodeSample(props: {
|
|
17
23
|
data: OpenAPIOperationData;
|
|
18
24
|
context: OpenAPIContextProps;
|
|
25
|
+
}) {
|
|
26
|
+
const { data } = props;
|
|
27
|
+
|
|
28
|
+
// If code samples are disabled at operation level, we don't display the code samples.
|
|
29
|
+
if (data.operation['x-codeSamples'] === false) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const customCodeSamples = getCustomCodeSamples(props);
|
|
34
|
+
|
|
35
|
+
// If code samples are disabled at the top-level and not custom code samples are defined,
|
|
36
|
+
// we don't display the code samples.
|
|
37
|
+
if (data['x-codeSamples'] === false && !customCodeSamples) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const samples = customCodeSamples ?? generateCodeSamples(props);
|
|
42
|
+
|
|
43
|
+
if (samples.length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
|
49
|
+
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
|
|
50
|
+
<OpenAPITabsPanels />
|
|
51
|
+
</StaticSection>
|
|
52
|
+
</OpenAPITabs>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate code samples for the operation.
|
|
58
|
+
*/
|
|
59
|
+
function generateCodeSamples(props: {
|
|
60
|
+
data: OpenAPIOperationData;
|
|
61
|
+
context: OpenAPIContextProps;
|
|
19
62
|
}) {
|
|
20
63
|
const { data, context } = props;
|
|
21
64
|
|
|
@@ -51,97 +94,111 @@ export function OpenAPICodeSample(props: {
|
|
|
51
94
|
const requestBody = !checkIsReference(data.operation.requestBody)
|
|
52
95
|
? data.operation.requestBody
|
|
53
96
|
: undefined;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(searchParams.size ? `?${searchParams.toString()}` : ''),
|
|
64
|
-
method: data.method,
|
|
65
|
-
body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined,
|
|
66
|
-
headers: {
|
|
67
|
-
...getSecurityHeaders(data.securities),
|
|
68
|
-
...headersObject,
|
|
69
|
-
...(requestBodyContent
|
|
70
|
-
? {
|
|
71
|
-
'Content-Type': requestBodyContent[0],
|
|
72
|
-
}
|
|
73
|
-
: undefined),
|
|
74
|
-
},
|
|
97
|
+
|
|
98
|
+
const url =
|
|
99
|
+
getDefaultServerURL(data.servers) +
|
|
100
|
+
data.path +
|
|
101
|
+
(searchParams.size ? `?${searchParams.toString()}` : '');
|
|
102
|
+
|
|
103
|
+
const genericHeaders = {
|
|
104
|
+
...getSecurityHeaders(data.securities),
|
|
105
|
+
...headersObject,
|
|
75
106
|
};
|
|
76
107
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const customSamples = data.operation[key];
|
|
95
|
-
if (customSamples && Array.isArray(customSamples)) {
|
|
96
|
-
customCodeSamples = customSamples
|
|
97
|
-
.filter((sample) => {
|
|
98
|
-
return (
|
|
99
|
-
typeof sample.label === 'string' &&
|
|
100
|
-
typeof sample.source === 'string' &&
|
|
101
|
-
typeof sample.lang === 'string'
|
|
102
|
-
);
|
|
103
|
-
})
|
|
104
|
-
.map((sample, index) => ({
|
|
105
|
-
key: `redocly-${sample.lang}-${index}`,
|
|
106
|
-
label: sample.label,
|
|
107
|
-
body: context.renderCodeBlock({
|
|
108
|
-
code: sample.source,
|
|
109
|
-
syntax: sample.lang,
|
|
108
|
+
const mediaTypeRendererFactories = Object.entries(requestBody?.content ?? {}).map(
|
|
109
|
+
([mediaType, mediaTypeObject]) => {
|
|
110
|
+
return (generator: CodeSampleGenerator) => {
|
|
111
|
+
const mediaTypeHeaders = {
|
|
112
|
+
...genericHeaders,
|
|
113
|
+
'Content-Type': mediaType,
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
mediaType,
|
|
117
|
+
element: context.renderCodeBlock({
|
|
118
|
+
code: generator.generate({
|
|
119
|
+
url,
|
|
120
|
+
method: data.method,
|
|
121
|
+
body: undefined,
|
|
122
|
+
headers: mediaTypeHeaders,
|
|
123
|
+
}),
|
|
124
|
+
syntax: generator.syntax,
|
|
110
125
|
}),
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
examples: generateMediaTypeExamples(mediaTypeObject, {
|
|
127
|
+
mode: 'write',
|
|
128
|
+
}).map((example) => ({
|
|
129
|
+
example,
|
|
130
|
+
element: context.renderCodeBlock({
|
|
131
|
+
code: generator.generate({
|
|
132
|
+
url,
|
|
133
|
+
method: data.method,
|
|
134
|
+
body: example.value,
|
|
135
|
+
headers: mediaTypeHeaders,
|
|
136
|
+
}),
|
|
137
|
+
syntax: generator.syntax,
|
|
138
|
+
}),
|
|
139
|
+
})),
|
|
140
|
+
} satisfies MediaTypeRenderer;
|
|
141
|
+
};
|
|
113
142
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Code samples can be disabled at the top-level or at the operation level
|
|
117
|
-
// If code samples are defined at the operation level, it will override the top-level setting
|
|
118
|
-
const codeSamplesDisabled =
|
|
119
|
-
data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
|
|
120
|
-
const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
|
|
143
|
+
);
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
return codeSampleGenerators.map((generator) => {
|
|
146
|
+
if (mediaTypeRendererFactories.length > 0) {
|
|
147
|
+
const renderers = mediaTypeRendererFactories.map((generate) => generate(generator));
|
|
148
|
+
return {
|
|
149
|
+
key: `default-${generator.id}`,
|
|
150
|
+
label: generator.label,
|
|
151
|
+
body: (
|
|
152
|
+
<OpenAPIMediaTypeExamplesBody
|
|
153
|
+
method={data.method}
|
|
154
|
+
path={data.path}
|
|
155
|
+
renderers={renderers}
|
|
156
|
+
/>
|
|
157
|
+
),
|
|
158
|
+
footer: (
|
|
159
|
+
<OpenAPICodeSampleFooter renderers={renderers} data={data} context={context} />
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
key: `default-${generator.id}`,
|
|
165
|
+
label: generator.label,
|
|
166
|
+
body: context.renderCodeBlock({
|
|
167
|
+
code: generator.generate({
|
|
168
|
+
url,
|
|
169
|
+
method: data.method,
|
|
170
|
+
body: undefined,
|
|
171
|
+
headers: genericHeaders,
|
|
172
|
+
}),
|
|
173
|
+
syntax: generator.syntax,
|
|
174
|
+
}),
|
|
175
|
+
footer: <OpenAPICodeSampleFooter data={data} renderers={[]} context={context} />,
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
125
179
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
export interface MediaTypeRenderer {
|
|
181
|
+
mediaType: string;
|
|
182
|
+
element: React.ReactNode;
|
|
183
|
+
examples: Array<{
|
|
184
|
+
example: OpenAPIV3.ExampleObject;
|
|
185
|
+
element: React.ReactNode;
|
|
186
|
+
}>;
|
|
133
187
|
}
|
|
134
188
|
|
|
135
189
|
function OpenAPICodeSampleFooter(props: {
|
|
136
190
|
data: OpenAPIOperationData;
|
|
191
|
+
renderers: MediaTypeRenderer[];
|
|
137
192
|
context: OpenAPIContextProps;
|
|
138
193
|
}) {
|
|
139
|
-
const { data, context } = props;
|
|
194
|
+
const { data, context, renderers } = props;
|
|
140
195
|
const { method, path } = data;
|
|
141
196
|
const { specUrl } = context;
|
|
142
197
|
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
|
198
|
+
const hasMultipleMediaTypes =
|
|
199
|
+
renderers.length > 1 || renderers.some((renderer) => renderer.examples.length > 0);
|
|
143
200
|
|
|
144
|
-
if (hideTryItPanel) {
|
|
201
|
+
if (hideTryItPanel && !hasMultipleMediaTypes) {
|
|
145
202
|
return null;
|
|
146
203
|
}
|
|
147
204
|
|
|
@@ -151,11 +208,63 @@ function OpenAPICodeSampleFooter(props: {
|
|
|
151
208
|
|
|
152
209
|
return (
|
|
153
210
|
<div className="openapi-codesample-footer">
|
|
154
|
-
|
|
211
|
+
{hasMultipleMediaTypes ? (
|
|
212
|
+
<OpenAPIMediaTypeExamplesSelector
|
|
213
|
+
method={data.method}
|
|
214
|
+
path={data.path}
|
|
215
|
+
renderers={renderers}
|
|
216
|
+
/>
|
|
217
|
+
) : (
|
|
218
|
+
<span />
|
|
219
|
+
)}
|
|
220
|
+
{!hideTryItPanel && <ScalarApiButton method={method} path={path} specUrl={specUrl} />}
|
|
155
221
|
</div>
|
|
156
222
|
);
|
|
157
223
|
}
|
|
158
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Get custom code samples for the operation.
|
|
227
|
+
*/
|
|
228
|
+
function getCustomCodeSamples(props: {
|
|
229
|
+
data: OpenAPIOperationData;
|
|
230
|
+
context: OpenAPIContextProps;
|
|
231
|
+
}) {
|
|
232
|
+
const { data, context } = props;
|
|
233
|
+
|
|
234
|
+
let customCodeSamples: null | Array<{
|
|
235
|
+
key: string;
|
|
236
|
+
label: string;
|
|
237
|
+
body: React.ReactNode;
|
|
238
|
+
}> = null;
|
|
239
|
+
|
|
240
|
+
CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => {
|
|
241
|
+
const customSamples = data.operation[key];
|
|
242
|
+
if (customSamples && Array.isArray(customSamples)) {
|
|
243
|
+
customCodeSamples = customSamples
|
|
244
|
+
.filter((sample) => {
|
|
245
|
+
return (
|
|
246
|
+
typeof sample.label === 'string' &&
|
|
247
|
+
typeof sample.source === 'string' &&
|
|
248
|
+
typeof sample.lang === 'string'
|
|
249
|
+
);
|
|
250
|
+
})
|
|
251
|
+
.map((sample, index) => ({
|
|
252
|
+
key: `custom-sample-${sample.lang}-${index}`,
|
|
253
|
+
label: sample.label,
|
|
254
|
+
body: context.renderCodeBlock({
|
|
255
|
+
code: sample.source,
|
|
256
|
+
syntax: sample.lang,
|
|
257
|
+
}),
|
|
258
|
+
footer: (
|
|
259
|
+
<OpenAPICodeSampleFooter renderers={[]} data={data} context={context} />
|
|
260
|
+
),
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return customCodeSamples;
|
|
266
|
+
}
|
|
267
|
+
|
|
159
268
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
160
269
|
[key: string]: string;
|
|
161
270
|
} {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { useStore } from 'zustand';
|
|
5
|
+
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
|
6
|
+
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
|
7
|
+
|
|
8
|
+
type MediaTypeState = {
|
|
9
|
+
mediaType: string;
|
|
10
|
+
setMediaType: (mediaType: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function useMediaTypeState(
|
|
14
|
+
data: { method: string; path: string },
|
|
15
|
+
defaultKey: string
|
|
16
|
+
): MediaTypeState {
|
|
17
|
+
const { method, path } = data;
|
|
18
|
+
const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey));
|
|
19
|
+
if (typeof store.tabKey !== 'string') {
|
|
20
|
+
throw new Error('Media type key is not a string');
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
mediaType: store.tabKey,
|
|
24
|
+
setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) {
|
|
29
|
+
const { method, path } = data;
|
|
30
|
+
const store = useStore(
|
|
31
|
+
getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
|
|
32
|
+
);
|
|
33
|
+
if (typeof store.tabKey !== 'number') {
|
|
34
|
+
throw new Error('Example key is not a number');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
index: store.tabKey,
|
|
38
|
+
setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function OpenAPIMediaTypeExamplesSelector(props: {
|
|
43
|
+
method: string;
|
|
44
|
+
path: string;
|
|
45
|
+
renderers: MediaTypeRenderer[];
|
|
46
|
+
}) {
|
|
47
|
+
const { method, path, renderers } = props;
|
|
48
|
+
if (!renderers[0]) {
|
|
49
|
+
throw new Error('No renderers provided');
|
|
50
|
+
}
|
|
51
|
+
const state = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
|
52
|
+
const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0];
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="openapi-codesample-selectors">
|
|
56
|
+
<MediaTypeSelector state={state} renderers={renderers} />
|
|
57
|
+
<ExamplesSelector method={method} path={path} renderer={selected} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function MediaTypeSelector(props: {
|
|
63
|
+
state: MediaTypeState;
|
|
64
|
+
renderers: MediaTypeRenderer[];
|
|
65
|
+
}) {
|
|
66
|
+
const { renderers, state } = props;
|
|
67
|
+
|
|
68
|
+
if (renderers.length < 2) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<select
|
|
74
|
+
className={clsx('openapi-select')}
|
|
75
|
+
value={state.mediaType}
|
|
76
|
+
onChange={(e) => state.setMediaType(e.target.value)}
|
|
77
|
+
>
|
|
78
|
+
{renderers.map((renderer) => (
|
|
79
|
+
<option key={renderer.mediaType} value={renderer.mediaType}>
|
|
80
|
+
{renderer.mediaType}
|
|
81
|
+
</option>
|
|
82
|
+
))}
|
|
83
|
+
</select>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ExamplesSelector(props: {
|
|
88
|
+
method: string;
|
|
89
|
+
path: string;
|
|
90
|
+
renderer: MediaTypeRenderer;
|
|
91
|
+
}) {
|
|
92
|
+
const { method, path, renderer } = props;
|
|
93
|
+
const state = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
|
94
|
+
if (renderer.examples.length < 2) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<select
|
|
100
|
+
className={clsx('openapi-select')}
|
|
101
|
+
value={String(state.index)}
|
|
102
|
+
onChange={(e) => state.setIndex(Number(e.target.value))}
|
|
103
|
+
>
|
|
104
|
+
{renderer.examples.map((example, index) => (
|
|
105
|
+
<option key={index} value={index}>
|
|
106
|
+
{example.example.summary || `Example ${index + 1}`}
|
|
107
|
+
</option>
|
|
108
|
+
))}
|
|
109
|
+
</select>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function OpenAPIMediaTypeExamplesBody(props: {
|
|
114
|
+
method: string;
|
|
115
|
+
path: string;
|
|
116
|
+
renderers: MediaTypeRenderer[];
|
|
117
|
+
}) {
|
|
118
|
+
const { renderers, method, path } = props;
|
|
119
|
+
if (!renderers[0]) {
|
|
120
|
+
throw new Error('No renderers provided');
|
|
121
|
+
}
|
|
122
|
+
const mediaTypeState = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
|
123
|
+
const selected =
|
|
124
|
+
renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0];
|
|
125
|
+
if (selected.examples.length === 0) {
|
|
126
|
+
return selected.element;
|
|
127
|
+
}
|
|
128
|
+
return <ExamplesBody method={method} path={path} renderer={selected} />;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) {
|
|
132
|
+
const { method, path, renderer } = props;
|
|
133
|
+
const exampleState = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
|
134
|
+
const example = renderer.examples[exampleState.index] ?? renderer.examples[0];
|
|
135
|
+
if (!example) {
|
|
136
|
+
throw new Error(`No example found for index ${exampleState.index}`);
|
|
137
|
+
}
|
|
138
|
+
return example.element;
|
|
139
|
+
}
|
|
@@ -6,10 +6,16 @@ import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-co
|
|
|
6
6
|
export function OpenAPICopyButton(
|
|
7
7
|
props: ButtonProps & {
|
|
8
8
|
value: string;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
label?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether to show a tooltip.
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
withTooltip?: boolean;
|
|
9
16
|
}
|
|
10
17
|
) {
|
|
11
|
-
const { value } = props;
|
|
12
|
-
const { children, onPress, className } = props;
|
|
18
|
+
const { value, label, children, onPress, className, withTooltip = true } = props;
|
|
13
19
|
const [copied, setCopied] = useState(false);
|
|
14
20
|
const [isOpen, setIsOpen] = useState(false);
|
|
15
21
|
|
|
@@ -21,12 +27,19 @@ export function OpenAPICopyButton(
|
|
|
21
27
|
|
|
22
28
|
setTimeout(() => {
|
|
23
29
|
setCopied(false);
|
|
30
|
+
setIsOpen(false);
|
|
24
31
|
}, 2000);
|
|
25
32
|
});
|
|
26
33
|
};
|
|
27
34
|
|
|
28
35
|
return (
|
|
29
|
-
<TooltipTrigger
|
|
36
|
+
<TooltipTrigger
|
|
37
|
+
isOpen={isOpen}
|
|
38
|
+
onOpenChange={setIsOpen}
|
|
39
|
+
isDisabled={!withTooltip}
|
|
40
|
+
closeDelay={200}
|
|
41
|
+
delay={200}
|
|
42
|
+
>
|
|
30
43
|
<Button
|
|
31
44
|
type="button"
|
|
32
45
|
preventFocusOnPress
|
|
@@ -47,7 +60,7 @@ export function OpenAPICopyButton(
|
|
|
47
60
|
offset={4}
|
|
48
61
|
className="openapi-tooltip"
|
|
49
62
|
>
|
|
50
|
-
{copied ? 'Copied' : 'Copy to clipboard'}
|
|
63
|
+
{copied ? 'Copied' : label || 'Copy to clipboard'}
|
|
51
64
|
</Tooltip>
|
|
52
65
|
</TooltipTrigger>
|
|
53
66
|
);
|