@gitbook/react-openapi 1.1.4 → 1.1.6
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 +21 -0
- package/dist/OpenAPICodeSample.d.ts +9 -0
- package/dist/OpenAPICodeSample.jsx +121 -41
- package/dist/OpenAPICodeSampleInteractive.d.ts +10 -0
- package/dist/OpenAPICodeSampleInteractive.jsx +78 -0
- package/dist/OpenAPICopyButton.d.ts +4 -0
- package/dist/OpenAPICopyButton.jsx +32 -0
- package/dist/OpenAPIOperation.jsx +1 -1
- package/dist/OpenAPIPath.d.ts +1 -2
- package/dist/OpenAPIPath.jsx +32 -30
- package/dist/OpenAPIResponseExample.jsx +5 -8
- package/dist/OpenAPISchemaName.jsx +7 -6
- package/dist/OpenAPITabs.d.ts +1 -1
- package/dist/OpenAPITabs.jsx +9 -2
- package/dist/ScalarApiButton.jsx +2 -2
- package/dist/code-samples.d.ts +1 -2
- package/dist/code-samples.js +2 -2
- package/dist/generateSchemaExample.d.ts +31 -2
- package/dist/generateSchemaExample.js +307 -24
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +11 -7
- package/package.json +3 -3
- package/src/OpenAPICodeSample.tsx +181 -51
- package/src/OpenAPICodeSampleInteractive.tsx +114 -0
- package/src/OpenAPICopyButton.tsx +54 -0
- package/src/OpenAPIOperation.tsx +1 -1
- package/src/OpenAPIPath.tsx +40 -42
- package/src/OpenAPIResponseExample.tsx +30 -33
- package/src/OpenAPISchemaName.tsx +11 -6
- package/src/OpenAPITabs.tsx +13 -4
- package/src/ScalarApiButton.tsx +2 -2
- package/src/code-samples.ts +3 -3
- package/src/generateSchemaExample.ts +412 -25
- package/src/resolveOpenAPIOperation.test.ts +6 -6
- package/src/utils.ts +13 -10
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.6",
|
|
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",
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
+
import {
|
|
3
|
+
OpenAPIMediaTypeExamplesBody,
|
|
4
|
+
OpenAPIMediaTypeExamplesSelector,
|
|
5
|
+
} from './OpenAPICodeSampleInteractive';
|
|
1
6
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
7
|
+
import { ScalarApiButton } from './ScalarApiButton';
|
|
2
8
|
import { StaticSection } from './StaticSection';
|
|
3
|
-
import { type
|
|
4
|
-
import {
|
|
9
|
+
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
|
|
10
|
+
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
|
5
11
|
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
|
6
12
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
7
13
|
import { getDefaultServerURL } from './util/server';
|
|
8
14
|
import { checkIsReference, createStateKey } from './utils';
|
|
9
15
|
|
|
16
|
+
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
19
|
* Display code samples to execute the operation.
|
|
12
20
|
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
|
|
@@ -14,6 +22,43 @@ import { checkIsReference, createStateKey } from './utils';
|
|
|
14
22
|
export function OpenAPICodeSample(props: {
|
|
15
23
|
data: OpenAPIOperationData;
|
|
16
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;
|
|
17
62
|
}) {
|
|
18
63
|
const { data, context } = props;
|
|
19
64
|
|
|
@@ -49,45 +94,139 @@ export function OpenAPICodeSample(props: {
|
|
|
49
94
|
const requestBody = !checkIsReference(data.operation.requestBody)
|
|
50
95
|
? data.operation.requestBody
|
|
51
96
|
: undefined;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(searchParams.size ? `?${searchParams.toString()}` : ''),
|
|
62
|
-
method: data.method,
|
|
63
|
-
body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1]) : undefined,
|
|
64
|
-
headers: {
|
|
65
|
-
...getSecurityHeaders(data.securities),
|
|
66
|
-
...headersObject,
|
|
67
|
-
...(requestBodyContent
|
|
68
|
-
? {
|
|
69
|
-
'Content-Type': requestBodyContent[0],
|
|
70
|
-
}
|
|
71
|
-
: undefined),
|
|
72
|
-
},
|
|
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,
|
|
73
106
|
};
|
|
74
107
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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,
|
|
125
|
+
}),
|
|
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
|
+
};
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
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: <OpenAPIMediaTypeExamplesBody data={data} renderers={renderers} />,
|
|
152
|
+
footer: (
|
|
153
|
+
<OpenAPICodeSampleFooter renderers={renderers} data={data} context={context} />
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
key: `default-${generator.id}`,
|
|
159
|
+
label: generator.label,
|
|
160
|
+
body: context.renderCodeBlock({
|
|
161
|
+
code: generator.generate({
|
|
162
|
+
url,
|
|
163
|
+
method: data.method,
|
|
164
|
+
body: undefined,
|
|
165
|
+
headers: genericHeaders,
|
|
166
|
+
}),
|
|
167
|
+
syntax: generator.syntax,
|
|
168
|
+
}),
|
|
169
|
+
footer: <OpenAPICodeSampleFooter data={data} renderers={[]} context={context} />,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface MediaTypeRenderer {
|
|
175
|
+
mediaType: string;
|
|
176
|
+
element: React.ReactNode;
|
|
177
|
+
examples: Array<{
|
|
178
|
+
example: OpenAPIV3.ExampleObject;
|
|
179
|
+
element: React.ReactNode;
|
|
180
|
+
}>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function OpenAPICodeSampleFooter(props: {
|
|
184
|
+
data: OpenAPIOperationData;
|
|
185
|
+
renderers: MediaTypeRenderer[];
|
|
186
|
+
context: OpenAPIContextProps;
|
|
187
|
+
}) {
|
|
188
|
+
const { data, context, renderers } = props;
|
|
189
|
+
const { method, path } = data;
|
|
190
|
+
const { specUrl } = context;
|
|
191
|
+
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
|
192
|
+
const hasMediaTypes = renderers.length > 0;
|
|
193
|
+
|
|
194
|
+
if (hideTryItPanel && !hasMediaTypes) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!validateHttpMethod(method)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="openapi-codesample-footer">
|
|
204
|
+
{hasMediaTypes ? (
|
|
205
|
+
<OpenAPIMediaTypeExamplesSelector data={data} renderers={renderers} />
|
|
206
|
+
) : (
|
|
207
|
+
<span />
|
|
208
|
+
)}
|
|
209
|
+
{!hideTryItPanel && <ScalarApiButton method={method} path={path} specUrl={specUrl} />}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get custom code samples for the operation.
|
|
216
|
+
*/
|
|
217
|
+
function getCustomCodeSamples(props: {
|
|
218
|
+
data: OpenAPIOperationData;
|
|
219
|
+
context: OpenAPIContextProps;
|
|
220
|
+
}) {
|
|
221
|
+
const { data, context } = props;
|
|
83
222
|
|
|
84
|
-
// Use custom samples if defined
|
|
85
223
|
let customCodeSamples: null | Array<{
|
|
86
224
|
key: string;
|
|
87
225
|
label: string;
|
|
88
226
|
body: React.ReactNode;
|
|
89
227
|
}> = null;
|
|
90
|
-
|
|
228
|
+
|
|
229
|
+
CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => {
|
|
91
230
|
const customSamples = data.operation[key];
|
|
92
231
|
if (customSamples && Array.isArray(customSamples)) {
|
|
93
232
|
customCodeSamples = customSamples
|
|
@@ -99,33 +238,20 @@ export function OpenAPICodeSample(props: {
|
|
|
99
238
|
);
|
|
100
239
|
})
|
|
101
240
|
.map((sample, index) => ({
|
|
102
|
-
key: `
|
|
241
|
+
key: `custom-sample-${sample.lang}-${index}`,
|
|
103
242
|
label: sample.label,
|
|
104
243
|
body: context.renderCodeBlock({
|
|
105
244
|
code: sample.source,
|
|
106
245
|
syntax: sample.lang,
|
|
107
246
|
}),
|
|
247
|
+
footer: (
|
|
248
|
+
<OpenAPICodeSampleFooter renderers={[]} data={data} context={context} />
|
|
249
|
+
),
|
|
108
250
|
}));
|
|
109
251
|
}
|
|
110
252
|
});
|
|
111
253
|
|
|
112
|
-
|
|
113
|
-
// If code samples are defined at the operation level, it will override the top-level setting
|
|
114
|
-
const codeSamplesDisabled =
|
|
115
|
-
data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
|
|
116
|
-
const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
|
|
117
|
-
|
|
118
|
-
if (samples.length === 0) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
|
|
124
|
-
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
|
|
125
|
-
<OpenAPITabsPanels />
|
|
126
|
-
</StaticSection>
|
|
127
|
-
</OpenAPITabs>
|
|
128
|
-
);
|
|
254
|
+
return customCodeSamples;
|
|
129
255
|
}
|
|
130
256
|
|
|
131
257
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
@@ -169,3 +295,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
|
169
295
|
}
|
|
170
296
|
}
|
|
171
297
|
}
|
|
298
|
+
|
|
299
|
+
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
|
|
300
|
+
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
|
301
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 type { OpenAPIOperationData } from './types';
|
|
7
|
+
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
|
8
|
+
|
|
9
|
+
function useMediaTypeState(data: OpenAPIOperationData, defaultKey: string) {
|
|
10
|
+
const { method, path } = data;
|
|
11
|
+
const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey));
|
|
12
|
+
if (typeof store.tabKey !== 'string') {
|
|
13
|
+
throw new Error('Media type key is not a string');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
mediaType: store.tabKey,
|
|
17
|
+
setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useMediaTypeSampleIndexState(data: OpenAPIOperationData, mediaType: string) {
|
|
22
|
+
const { method, path } = data;
|
|
23
|
+
const store = useStore(
|
|
24
|
+
getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
|
|
25
|
+
);
|
|
26
|
+
if (typeof store.tabKey !== 'number') {
|
|
27
|
+
throw new Error('Example key is not a number');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
index: store.tabKey,
|
|
31
|
+
setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function OpenAPIMediaTypeExamplesSelector(props: {
|
|
36
|
+
data: OpenAPIOperationData;
|
|
37
|
+
renderers: MediaTypeRenderer[];
|
|
38
|
+
}) {
|
|
39
|
+
const { data, renderers } = props;
|
|
40
|
+
if (!renderers[0]) {
|
|
41
|
+
throw new Error('No renderers provided');
|
|
42
|
+
}
|
|
43
|
+
const state = useMediaTypeState(data, renderers[0].mediaType);
|
|
44
|
+
const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0];
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="openapi-codesample-selectors">
|
|
48
|
+
<select
|
|
49
|
+
className={clsx('openapi-select')}
|
|
50
|
+
value={state.mediaType}
|
|
51
|
+
onChange={(e) => state.setMediaType(e.target.value)}
|
|
52
|
+
>
|
|
53
|
+
{renderers.map((renderer) => (
|
|
54
|
+
<option key={renderer.mediaType} value={renderer.mediaType}>
|
|
55
|
+
{renderer.mediaType}
|
|
56
|
+
</option>
|
|
57
|
+
))}
|
|
58
|
+
</select>
|
|
59
|
+
<ExamplesSelector data={data} renderer={selected} />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ExamplesSelector(props: {
|
|
65
|
+
data: OpenAPIOperationData;
|
|
66
|
+
renderer: MediaTypeRenderer;
|
|
67
|
+
}) {
|
|
68
|
+
const { data, renderer } = props;
|
|
69
|
+
const state = useMediaTypeSampleIndexState(data, renderer.mediaType);
|
|
70
|
+
if (renderer.examples.length < 2) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<select
|
|
76
|
+
className={clsx('openapi-select')}
|
|
77
|
+
value={String(state.index)}
|
|
78
|
+
onChange={(e) => state.setIndex(Number(e.target.value))}
|
|
79
|
+
>
|
|
80
|
+
{renderer.examples.map((example, index) => (
|
|
81
|
+
<option key={index} value={index}>
|
|
82
|
+
{example.example.summary || `Example ${index + 1}`}
|
|
83
|
+
</option>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function OpenAPIMediaTypeExamplesBody(props: {
|
|
90
|
+
data: OpenAPIOperationData;
|
|
91
|
+
renderers: MediaTypeRenderer[];
|
|
92
|
+
}) {
|
|
93
|
+
const { renderers, data } = props;
|
|
94
|
+
if (!renderers[0]) {
|
|
95
|
+
throw new Error('No renderers provided');
|
|
96
|
+
}
|
|
97
|
+
const mediaTypeState = useMediaTypeState(data, renderers[0].mediaType);
|
|
98
|
+
const selected =
|
|
99
|
+
renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0];
|
|
100
|
+
if (selected.examples.length === 0) {
|
|
101
|
+
return selected.element;
|
|
102
|
+
}
|
|
103
|
+
return <ExamplesBody data={data} renderer={selected} />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ExamplesBody(props: { data: OpenAPIOperationData; renderer: MediaTypeRenderer }) {
|
|
107
|
+
const { data, renderer } = props;
|
|
108
|
+
const exampleState = useMediaTypeSampleIndexState(data, renderer.mediaType);
|
|
109
|
+
const example = renderer.examples[exampleState.index] ?? renderer.examples[0];
|
|
110
|
+
if (!example) {
|
|
111
|
+
throw new Error(`No example found for index ${exampleState.index}`);
|
|
112
|
+
}
|
|
113
|
+
return example.element;
|
|
114
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
|
|
5
|
+
|
|
6
|
+
export function OpenAPICopyButton(
|
|
7
|
+
props: ButtonProps & {
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
) {
|
|
11
|
+
const { value } = props;
|
|
12
|
+
const { children, onPress, className } = props;
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleCopy = () => {
|
|
17
|
+
if (!value) return;
|
|
18
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
19
|
+
setIsOpen(true);
|
|
20
|
+
setCopied(true);
|
|
21
|
+
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
setCopied(false);
|
|
24
|
+
}, 2000);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
|
|
30
|
+
<Button
|
|
31
|
+
type="button"
|
|
32
|
+
preventFocusOnPress
|
|
33
|
+
onPress={(e) => {
|
|
34
|
+
handleCopy();
|
|
35
|
+
onPress?.(e);
|
|
36
|
+
}}
|
|
37
|
+
className={`openapi-copy-button ${className}`}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</Button>
|
|
42
|
+
|
|
43
|
+
<Tooltip
|
|
44
|
+
isOpen={isOpen}
|
|
45
|
+
onOpenChange={setIsOpen}
|
|
46
|
+
placement="top"
|
|
47
|
+
offset={4}
|
|
48
|
+
className="openapi-tooltip"
|
|
49
|
+
>
|
|
50
|
+
{copied ? 'Copied' : 'Copy to clipboard'}{' '}
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</TooltipTrigger>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/OpenAPIOperation.tsx
CHANGED
|
@@ -35,6 +35,7 @@ export function OpenAPIOperation(props: {
|
|
|
35
35
|
title: operation.summary,
|
|
36
36
|
})
|
|
37
37
|
: null}
|
|
38
|
+
<OpenAPIPath data={data} context={context} />
|
|
38
39
|
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
|
|
39
40
|
</div>
|
|
40
41
|
<div className="openapi-columns">
|
|
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
|
|
|
49
50
|
</div>
|
|
50
51
|
) : null}
|
|
51
52
|
<OpenAPIOperationDescription operation={operation} context={context} />
|
|
52
|
-
<OpenAPIPath data={data} context={context} />
|
|
53
53
|
<OpenAPISpec data={data} context={clientContext} />
|
|
54
54
|
</div>
|
|
55
55
|
<div className="openapi-column-preview">
|
package/src/OpenAPIPath.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type React from 'react';
|
|
3
|
-
import { ScalarApiButton } from './ScalarApiButton';
|
|
1
|
+
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
|
4
2
|
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
|
|
3
|
+
import { getDefaultServerURL } from './util/server';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Display the path of an operation.
|
|
@@ -10,63 +9,62 @@ export function OpenAPIPath(props: {
|
|
|
10
9
|
data: OpenAPIOperationData;
|
|
11
10
|
context: OpenAPIContextProps;
|
|
12
11
|
}) {
|
|
13
|
-
const { data
|
|
14
|
-
const { method, path } = data;
|
|
15
|
-
|
|
16
|
-
const
|
|
12
|
+
const { data } = props;
|
|
13
|
+
const { method, path, operation } = data;
|
|
14
|
+
|
|
15
|
+
const server = getDefaultServerURL(data.servers);
|
|
16
|
+
const formattedPath = formatPath(path);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="openapi-path">
|
|
20
20
|
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
|
|
22
|
+
<OpenAPICopyButton
|
|
23
|
+
value={server + path}
|
|
24
|
+
className="openapi-path-title"
|
|
25
|
+
data-deprecated={operation.deprecated}
|
|
26
|
+
>
|
|
27
|
+
<span className="openapi-path-server">{server}</span>
|
|
28
|
+
{formattedPath}
|
|
29
|
+
</OpenAPICopyButton>
|
|
27
30
|
</div>
|
|
28
31
|
);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Format the path to highlight placeholders
|
|
34
|
+
/**
|
|
35
|
+
* Format the path by wrapping placeholders in <span> tags.
|
|
36
|
+
*/
|
|
36
37
|
function formatPath(path: string) {
|
|
37
38
|
// Matches placeholders like {id}, {userId}, etc.
|
|
38
|
-
const regex = /\{(\w+)\}
|
|
39
|
+
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
|
39
40
|
|
|
40
41
|
const parts: (string | React.JSX.Element)[] = [];
|
|
41
42
|
let lastIndex = 0;
|
|
42
43
|
|
|
43
|
-
//
|
|
44
|
-
path.replace(regex, (match,
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
|
45
|
+
path.replace(regex, (match, _, offset) => {
|
|
46
|
+
if (offset > lastIndex) {
|
|
47
|
+
parts.push(path.slice(lastIndex, offset));
|
|
48
|
+
}
|
|
49
|
+
parts.push(
|
|
50
|
+
<span key={offset} className="openapi-path-variable">
|
|
51
|
+
{match}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
47
54
|
lastIndex = offset + match.length;
|
|
48
55
|
return match;
|
|
49
56
|
});
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Join parts with separators wrapped in <span>
|
|
55
|
-
const formattedPath = parts.reduce(
|
|
56
|
-
(acc, part, index) => {
|
|
57
|
-
if (typeof part === 'string' && index > 0 && part === '/') {
|
|
58
|
-
acc.push(
|
|
59
|
-
<span className="openapi-path-separator" key={`sep-${index}`}>
|
|
60
|
-
/
|
|
61
|
-
</span>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
58
|
+
if (lastIndex < path.length) {
|
|
59
|
+
parts.push(path.slice(lastIndex));
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
const formattedPath = parts.map((part, index) => {
|
|
63
|
+
if (typeof part === 'string') {
|
|
64
|
+
return <span key={index}>{part}</span>;
|
|
65
|
+
}
|
|
66
|
+
return part;
|
|
67
|
+
});
|
|
70
68
|
|
|
71
|
-
return
|
|
69
|
+
return formattedPath;
|
|
72
70
|
}
|