@gitbook/react-openapi 1.1.4 → 1.1.5
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 +6 -0
- package/dist/OpenAPICodeSample.jsx +21 -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/OpenAPITabs.d.ts +1 -1
- package/dist/OpenAPITabs.jsx +9 -2
- package/dist/ScalarApiButton.jsx +2 -2
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/OpenAPICodeSample.tsx +32 -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/OpenAPITabs.tsx +13 -4
- package/src/ScalarApiButton.tsx +2 -2
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
1
2
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
3
|
+
import { ScalarApiButton } from './ScalarApiButton';
|
|
2
4
|
import { StaticSection } from './StaticSection';
|
|
3
5
|
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
|
|
4
6
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
|
@@ -79,6 +81,7 @@ export function OpenAPICodeSample(props: {
|
|
|
79
81
|
code: generator.generate(input),
|
|
80
82
|
syntax: generator.syntax,
|
|
81
83
|
}),
|
|
84
|
+
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
|
82
85
|
}));
|
|
83
86
|
|
|
84
87
|
// Use custom samples if defined
|
|
@@ -105,6 +108,7 @@ export function OpenAPICodeSample(props: {
|
|
|
105
108
|
code: sample.source,
|
|
106
109
|
syntax: sample.lang,
|
|
107
110
|
}),
|
|
111
|
+
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
|
|
108
112
|
}));
|
|
109
113
|
}
|
|
110
114
|
});
|
|
@@ -128,6 +132,30 @@ export function OpenAPICodeSample(props: {
|
|
|
128
132
|
);
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function OpenAPICodeSampleFooter(props: {
|
|
136
|
+
data: OpenAPIOperationData;
|
|
137
|
+
context: OpenAPIContextProps;
|
|
138
|
+
}) {
|
|
139
|
+
const { data, context } = props;
|
|
140
|
+
const { method, path } = data;
|
|
141
|
+
const { specUrl } = context;
|
|
142
|
+
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
|
143
|
+
|
|
144
|
+
if (hideTryItPanel) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!validateHttpMethod(method)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="openapi-codesample-footer">
|
|
154
|
+
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
132
160
|
[key: string]: string;
|
|
133
161
|
} {
|
|
@@ -169,3 +197,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
|
|
|
169
197
|
}
|
|
170
198
|
}
|
|
171
199
|
}
|
|
200
|
+
|
|
201
|
+
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
|
|
202
|
+
return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
|
|
203
|
+
}
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
|
2
|
+
import { Markdown } from './Markdown';
|
|
2
3
|
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
|
3
4
|
import { StaticSection } from './StaticSection';
|
|
4
5
|
import { generateSchemaExample } from './generateSchemaExample';
|
|
@@ -39,44 +40,40 @@ export function OpenAPIResponseExample(props: {
|
|
|
39
40
|
return Number(a) - Number(b);
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
const tabs = responses
|
|
43
|
-
|
|
44
|
-
const description = resolveDescription(responseObject);
|
|
45
|
-
|
|
46
|
-
if (checkIsReference(responseObject)) {
|
|
47
|
-
return {
|
|
48
|
-
key: key,
|
|
49
|
-
label: key,
|
|
50
|
-
description,
|
|
51
|
-
body: (
|
|
52
|
-
<OpenAPIExample
|
|
53
|
-
example={getExampleFromReference(responseObject)}
|
|
54
|
-
context={context}
|
|
55
|
-
syntax="json"
|
|
56
|
-
/>
|
|
57
|
-
),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
key: key,
|
|
64
|
-
label: key,
|
|
65
|
-
description,
|
|
66
|
-
body: <OpenAPIEmptyResponseExample />,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
43
|
+
const tabs = responses.map(([key, responseObject]) => {
|
|
44
|
+
const description = resolveDescription(responseObject);
|
|
69
45
|
|
|
46
|
+
if (checkIsReference(responseObject)) {
|
|
70
47
|
return {
|
|
71
48
|
key: key,
|
|
72
49
|
label: key,
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
body: (
|
|
51
|
+
<OpenAPIExample
|
|
52
|
+
example={getExampleFromReference(responseObject)}
|
|
53
|
+
context={context}
|
|
54
|
+
syntax="json"
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
75
58
|
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
key: key,
|
|
64
|
+
label: key,
|
|
65
|
+
body: <OpenAPIEmptyResponseExample />,
|
|
66
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
key: key,
|
|
72
|
+
label: key,
|
|
73
|
+
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
|
74
|
+
footer: description ? <Markdown source={description} /> : undefined,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
80
77
|
|
|
81
78
|
if (tabs.length === 0) {
|
|
82
79
|
return null;
|
package/src/OpenAPITabs.tsx
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
|
|
5
5
|
import { useEventCallback } from 'usehooks-ts';
|
|
6
|
-
import { Markdown } from './Markdown';
|
|
7
6
|
import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
|
|
8
7
|
|
|
9
8
|
export type TabItem = {
|
|
10
9
|
key: Key;
|
|
11
10
|
label: string;
|
|
12
11
|
body: React.ReactNode;
|
|
13
|
-
|
|
12
|
+
footer?: React.ReactNode;
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
type OpenAPITabsContextData = {
|
|
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
|
|
|
140
139
|
return (
|
|
141
140
|
<TabPanel key={key} id={key} className="openapi-tabs-panel">
|
|
142
141
|
{selectedTab.body}
|
|
143
|
-
{selectedTab.
|
|
144
|
-
<
|
|
142
|
+
{selectedTab.footer ? (
|
|
143
|
+
<OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
|
|
145
144
|
) : null}
|
|
146
145
|
</TabPanel>
|
|
147
146
|
);
|
|
148
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The OpenAPI Tabs panel footer component.
|
|
151
|
+
* This component should be used as a child of the OpenAPITabs component.
|
|
152
|
+
*/
|
|
153
|
+
function OpenAPITabsPanelFooter(props: { children: React.ReactNode }) {
|
|
154
|
+
const { children } = props;
|
|
155
|
+
|
|
156
|
+
return <div className="openapi-tabs-footer">{children}</div>;
|
|
157
|
+
}
|
package/src/ScalarApiButton.tsx
CHANGED
|
@@ -27,14 +27,14 @@ export function ScalarApiButton(props: {
|
|
|
27
27
|
setIsOpen(true);
|
|
28
28
|
}}
|
|
29
29
|
>
|
|
30
|
-
|
|
30
|
+
Test it
|
|
31
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
|
|
31
32
|
<path
|
|
32
33
|
stroke="currentColor"
|
|
33
34
|
strokeWidth="1.5"
|
|
34
35
|
d="M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z"
|
|
35
36
|
/>
|
|
36
37
|
</svg>
|
|
37
|
-
Test it
|
|
38
38
|
</button>
|
|
39
39
|
|
|
40
40
|
{isOpen &&
|