@gitbook/react-openapi 1.5.1 → 1.5.2
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 +9 -0
- package/dist/InteractiveSection.js +59 -0
- package/dist/Markdown.js +10 -0
- package/dist/OpenAPICodeSample.js +219 -0
- package/dist/OpenAPICodeSampleInteractive.js +66 -0
- package/dist/OpenAPICodeSampleSelector.js +45 -0
- package/dist/OpenAPICopyButton.js +39 -0
- package/dist/OpenAPIDisclosure.js +30 -0
- package/dist/OpenAPIDisclosureGroup.js +75 -0
- package/dist/OpenAPIExample.js +41 -0
- package/dist/OpenAPIMediaType.js +58 -0
- package/dist/OpenAPIOperation.d.ts +15 -0
- package/dist/OpenAPIOperation.js +30 -0
- package/dist/OpenAPIOperationContext.d.ts +20 -0
- package/dist/OpenAPIOperationContext.js +30 -0
- package/dist/OpenAPIPath.js +51 -0
- package/dist/OpenAPIPrefillContextProvider.d.ts +26 -0
- package/dist/OpenAPIPrefillContextProvider.js +25 -0
- package/dist/OpenAPIRequestBody.js +28 -0
- package/dist/OpenAPIRequestBodyHeaderType.js +23 -0
- package/dist/OpenAPIResponse.js +39 -0
- package/dist/OpenAPIResponseExample.js +75 -0
- package/dist/OpenAPIResponseExampleContent.js +61 -0
- package/dist/OpenAPIResponses.js +61 -0
- package/dist/OpenAPISchema.js +373 -0
- package/dist/OpenAPISchemaName.js +45 -0
- package/dist/OpenAPISchemaServer.js +13 -0
- package/dist/OpenAPISecurities.js +124 -0
- package/dist/OpenAPISelect.js +45 -0
- package/dist/OpenAPISpec.js +73 -0
- package/dist/OpenAPIWebhook.d.ts +15 -0
- package/dist/OpenAPIWebhook.js +28 -0
- package/dist/OpenAPIWebhookExample.js +40 -0
- package/dist/ScalarApiButton.js +87 -0
- package/dist/StaticSection.js +37 -0
- package/dist/code-samples.js +275 -0
- package/dist/common/OpenAPIColumnSpec.js +23 -0
- package/dist/common/OpenAPIOperationDescription.js +18 -0
- package/dist/common/OpenAPIStability.js +17 -0
- package/dist/common/OpenAPISummary.js +27 -0
- package/dist/contentTypeChecks.js +34 -0
- package/dist/context.d.ts +71 -0
- package/dist/context.js +29 -0
- package/dist/decycle.js +41 -0
- package/dist/dereference.js +24 -0
- package/dist/generateSchemaExample.js +198 -0
- package/dist/getDisclosureLabel.js +17 -0
- package/dist/getOrCreateStoreByKey.js +22 -0
- package/dist/index.d.ts +11 -662
- package/dist/index.js +9 -3871
- package/dist/json2xml.js +12 -0
- package/dist/resolveOpenAPIOperation.d.ts +15 -0
- package/dist/resolveOpenAPIOperation.js +102 -0
- package/dist/resolveOpenAPIWebhook.d.ts +15 -0
- package/dist/resolveOpenAPIWebhook.js +52 -0
- package/dist/schemas/OpenAPISchemaItem.js +26 -0
- package/dist/schemas/OpenAPISchemas.d.ts +19 -0
- package/dist/schemas/OpenAPISchemas.js +57 -0
- package/dist/schemas/resolveOpenAPISchemas.d.ts +15 -0
- package/dist/schemas/resolveOpenAPISchemas.js +17 -0
- package/dist/stringifyOpenAPI.js +14 -0
- package/dist/translate.js +43 -0
- package/dist/translations/de.js +48 -0
- package/dist/translations/en.d.ts +47 -0
- package/dist/translations/en.js +48 -0
- package/dist/translations/es.js +48 -0
- package/dist/translations/fr.js +48 -0
- package/dist/translations/index.d.ts +408 -0
- package/dist/translations/index.js +31 -0
- package/dist/translations/ja.js +48 -0
- package/dist/translations/nl.js +48 -0
- package/dist/translations/no.js +48 -0
- package/dist/translations/pt-br.js +48 -0
- package/dist/translations/types.d.ts +7 -0
- package/dist/translations/zh.js +48 -0
- package/dist/types.d.ts +37 -0
- package/dist/util/example.js +84 -0
- package/dist/util/server.js +38 -0
- package/dist/util/tryit-prefill.js +143 -0
- package/dist/utils.js +163 -0
- package/package.json +10 -7
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from "./StaticSection.js";
|
|
5
|
+
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from "./OpenAPISelect.js";
|
|
6
|
+
import { useDisclosureState } from "./node_modules/react-stately/dist/import.js";
|
|
7
|
+
import clsx from "classnames";
|
|
8
|
+
import { useRef } from "react";
|
|
9
|
+
import { mergeProps, useButton, useDisclosure, useFocusRing } from "react-aria";
|
|
10
|
+
|
|
11
|
+
//#region src/InteractiveSection.tsx
|
|
12
|
+
/**
|
|
13
|
+
* To optimize rendering, most of the components are server-components,
|
|
14
|
+
* and the interactiveness is mainly handled by a few key components like this one.
|
|
15
|
+
*/
|
|
16
|
+
function InteractiveSection(props) {
|
|
17
|
+
const { id, className, toggeable = false, defaultOpened = true, tabs = [], defaultTab = tabs[0]?.key, header, overlay, toggleIcon = "▶", selectIcon, stateKey = "interactive-section" } = props;
|
|
18
|
+
const state = useDisclosureState({ defaultExpanded: defaultOpened });
|
|
19
|
+
const panelRef = useRef(null);
|
|
20
|
+
const triggerRef = useRef(null);
|
|
21
|
+
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
|
22
|
+
const { buttonProps } = useButton(triggerProps, triggerRef);
|
|
23
|
+
const { isFocusVisible, focusProps } = useFocusRing();
|
|
24
|
+
const store = useSelectState(stateKey, defaultTab);
|
|
25
|
+
const selectedTab = tabs.find((tab) => tab.key === store.key) ?? tabs[0];
|
|
26
|
+
return <Section id={id} className={clsx("openapi-section", toggeable ? "openapi-section-toggeable" : null, className, toggeable ? `${className}-${state.isExpanded ? "opened" : "closed"}` : null)}>
|
|
27
|
+
{header ? <SectionHeader onClick={() => {
|
|
28
|
+
if (toggeable) state.toggle();
|
|
29
|
+
}} className={className}>
|
|
30
|
+
<SectionHeaderContent className={className}>
|
|
31
|
+
{selectedTab?.body && toggeable ? <button {...mergeProps(buttonProps, focusProps)} ref={triggerRef} className={clsx("openapi-section-toggle", `${className}-toggle`)} style={{ outline: isFocusVisible ? "2px solid rgb(var(--primary-color-500) / 0.4)" : "none" }}>
|
|
32
|
+
{toggleIcon}
|
|
33
|
+
</button> : null}
|
|
34
|
+
{header}
|
|
35
|
+
</SectionHeaderContent>
|
|
36
|
+
{}
|
|
37
|
+
<div className={clsx("openapi-section-header-controls", `${className}-header-controls`)} onClick={(event) => {
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
}}>
|
|
40
|
+
{tabs.length > 0 ? <OpenAPISelect stateKey={stateKey} items={tabs} onSelectionChange={() => {
|
|
41
|
+
state.expand();
|
|
42
|
+
}} icon={selectIcon} placement="bottom end">
|
|
43
|
+
{tabs.map((tab) => <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
|
|
44
|
+
{tab.label}
|
|
45
|
+
</OpenAPISelectItem>)}
|
|
46
|
+
</OpenAPISelect> : null}
|
|
47
|
+
</div>
|
|
48
|
+
</SectionHeader> : null}
|
|
49
|
+
{(!toggeable || state.isExpanded) && selectedTab?.body ? <SectionBody ref={panelRef} {...panelProps} className={className}>
|
|
50
|
+
{selectedTab?.body}
|
|
51
|
+
</SectionBody> : null}
|
|
52
|
+
{overlay ? <div className={clsx("openapi-section-overlay", `${className}-overlay`)}>
|
|
53
|
+
{overlay}
|
|
54
|
+
</div> : null}
|
|
55
|
+
</Section>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { InteractiveSection };
|
package/dist/Markdown.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import clsx from "classnames";
|
|
2
|
+
|
|
3
|
+
//#region src/Markdown.tsx
|
|
4
|
+
function Markdown(props) {
|
|
5
|
+
const { source, className } = props;
|
|
6
|
+
return <div className={clsx("openapi-markdown", className)} dangerouslySetInnerHTML={{ __html: source }} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Markdown };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { stringifyOpenAPI } from "./stringifyOpenAPI.js";
|
|
2
|
+
import { checkIsReference, extractOperationSecurityInfo } from "./utils.js";
|
|
3
|
+
import { getOpenAPIClientContext } from "./context.js";
|
|
4
|
+
import { generateMediaTypeExamples, generateSchemaExample } from "./generateSchemaExample.js";
|
|
5
|
+
import { OpenAPIMediaTypeExamplesBody, OpenAPIMediaTypeExamplesSelector } from "./OpenAPICodeSampleInteractive.js";
|
|
6
|
+
import { getDefaultServerURL } from "./util/server.js";
|
|
7
|
+
import { OpenAPICodeSampleBody } from "./OpenAPICodeSampleSelector.js";
|
|
8
|
+
import { resolvePrefillCodePlaceholderFromSecurityScheme, resolveURLWithPrefillCodePlaceholdersFromServer } from "./util/tryit-prefill.js";
|
|
9
|
+
import { ScalarApiButton } from "./ScalarApiButton.js";
|
|
10
|
+
import { codeSampleGenerators, parseHostAndPath } from "./code-samples.js";
|
|
11
|
+
|
|
12
|
+
//#region src/OpenAPICodeSample.tsx
|
|
13
|
+
const CUSTOM_CODE_SAMPLES_KEYS = [
|
|
14
|
+
"x-custom-examples",
|
|
15
|
+
"x-code-samples",
|
|
16
|
+
"x-codeSamples"
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Display code samples to execute the operation.
|
|
20
|
+
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
|
|
21
|
+
*/
|
|
22
|
+
function OpenAPICodeSample(props) {
|
|
23
|
+
const { data, context } = props;
|
|
24
|
+
if (data.operation["x-codeSamples"] === false) return null;
|
|
25
|
+
const customCodeSamples = getCustomCodeSamples(props);
|
|
26
|
+
if (data["x-codeSamples"] === false && !customCodeSamples) return null;
|
|
27
|
+
const samples = customCodeSamples ?? generateCodeSamples(props);
|
|
28
|
+
if (samples.length === 0) return null;
|
|
29
|
+
return <OpenAPICodeSampleBody context={getOpenAPIClientContext(context)} data={data} items={samples} selectIcon={context.icons.chevronDown} />;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate code samples for the operation.
|
|
33
|
+
*/
|
|
34
|
+
function generateCodeSamples(props) {
|
|
35
|
+
const { data, context } = props;
|
|
36
|
+
const searchParams = new URLSearchParams();
|
|
37
|
+
const headersObject = {};
|
|
38
|
+
(Array.isArray(data.operation.parameters) ? data.operation.parameters : []).forEach((param) => {
|
|
39
|
+
if (!param) return;
|
|
40
|
+
if (param.in === "header" && param.required) {
|
|
41
|
+
const example = param.schema ? generateSchemaExample(param.schema, { mode: "write" }) : void 0;
|
|
42
|
+
if (example !== void 0 && param.name) headersObject[param.name] = typeof example !== "string" ? stringifyOpenAPI(example) : example;
|
|
43
|
+
} else if (param.in === "query" && param.required) {
|
|
44
|
+
const example = param.schema ? generateSchemaExample(param.schema, { mode: "write" }) : void 0;
|
|
45
|
+
if (example !== void 0 && param.name) searchParams.append(param.name, String(Array.isArray(example) ? example[0] : example));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const requestBody = !checkIsReference(data.operation.requestBody) ? data.operation.requestBody : void 0;
|
|
49
|
+
const defaultServerUrl = getDefaultServerURL(data.servers);
|
|
50
|
+
let serverUrlPath = defaultServerUrl ? parseHostAndPath(defaultServerUrl).path : "";
|
|
51
|
+
serverUrlPath = serverUrlPath === "/" ? "" : serverUrlPath;
|
|
52
|
+
const serverUrlOrigin = (data.servers[0] ? resolveURLWithPrefillCodePlaceholdersFromServer(data.servers[0], defaultServerUrl) : defaultServerUrl).replaceAll(serverUrlPath, "");
|
|
53
|
+
const path = serverUrlPath + data.path + (searchParams.size ? `?${searchParams.toString()}` : "");
|
|
54
|
+
const genericHeaders = {
|
|
55
|
+
...getSecurityHeaders({
|
|
56
|
+
securityRequirement: data.operation.security,
|
|
57
|
+
securities: data.securities
|
|
58
|
+
}),
|
|
59
|
+
...headersObject
|
|
60
|
+
};
|
|
61
|
+
const mediaTypeRendererFactories = Object.entries(requestBody?.content ?? {}).map(([mediaType, mediaTypeObject]) => {
|
|
62
|
+
return (generator) => {
|
|
63
|
+
const mediaTypeHeaders = {
|
|
64
|
+
...genericHeaders,
|
|
65
|
+
"Content-Type": mediaType
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
mediaType,
|
|
69
|
+
element: context.renderCodeBlock({
|
|
70
|
+
code: generator.generate({
|
|
71
|
+
url: {
|
|
72
|
+
origin: serverUrlOrigin,
|
|
73
|
+
path
|
|
74
|
+
},
|
|
75
|
+
method: data.method,
|
|
76
|
+
body: void 0,
|
|
77
|
+
headers: mediaTypeHeaders
|
|
78
|
+
}),
|
|
79
|
+
syntax: generator.syntax
|
|
80
|
+
}),
|
|
81
|
+
examples: generateMediaTypeExamples(mediaTypeObject, { mode: "write" }).map((example) => ({
|
|
82
|
+
example,
|
|
83
|
+
element: context.renderCodeBlock({
|
|
84
|
+
code: generator.generate({
|
|
85
|
+
url: {
|
|
86
|
+
origin: serverUrlOrigin,
|
|
87
|
+
path
|
|
88
|
+
},
|
|
89
|
+
method: data.method,
|
|
90
|
+
body: example.value,
|
|
91
|
+
headers: mediaTypeHeaders
|
|
92
|
+
}),
|
|
93
|
+
syntax: generator.syntax
|
|
94
|
+
})
|
|
95
|
+
}))
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return codeSampleGenerators.map((generator) => {
|
|
100
|
+
if (mediaTypeRendererFactories.length > 0) {
|
|
101
|
+
const renderers = mediaTypeRendererFactories.map((generate) => generate(generator));
|
|
102
|
+
return {
|
|
103
|
+
key: `default-${generator.id}`,
|
|
104
|
+
label: generator.label,
|
|
105
|
+
body: <OpenAPIMediaTypeExamplesBody method={data.method} path={data.path} renderers={renderers} blockKey={context.blockKey} />,
|
|
106
|
+
footer: <OpenAPICodeSampleFooter renderers={renderers} data={data} context={context} />
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
key: `default-${generator.id}`,
|
|
111
|
+
label: generator.label,
|
|
112
|
+
body: context.renderCodeBlock({
|
|
113
|
+
code: generator.generate({
|
|
114
|
+
url: {
|
|
115
|
+
origin: serverUrlOrigin,
|
|
116
|
+
path
|
|
117
|
+
},
|
|
118
|
+
method: data.method,
|
|
119
|
+
body: void 0,
|
|
120
|
+
headers: genericHeaders
|
|
121
|
+
}),
|
|
122
|
+
syntax: generator.syntax
|
|
123
|
+
}),
|
|
124
|
+
footer: <OpenAPICodeSampleFooter data={data} renderers={[]} context={context} />
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function OpenAPICodeSampleFooter(props) {
|
|
129
|
+
const { data, context, renderers } = props;
|
|
130
|
+
const { method, path, securities, servers } = data;
|
|
131
|
+
const { specUrl } = context;
|
|
132
|
+
const hideTryItPanel = data["x-hideTryItPanel"] || data.operation["x-hideTryItPanel"];
|
|
133
|
+
const hasMultipleMediaTypes = renderers.length > 1 || renderers.some((renderer) => renderer.examples.length > 0);
|
|
134
|
+
if (hideTryItPanel && !hasMultipleMediaTypes) return null;
|
|
135
|
+
if (!validateHttpMethod(method)) return null;
|
|
136
|
+
return <div className="openapi-codesample-footer">
|
|
137
|
+
{hasMultipleMediaTypes ? <OpenAPIMediaTypeExamplesSelector method={data.method} path={data.path} renderers={renderers} selectIcon={context.icons.chevronDown} blockKey={context.blockKey} /> : <span />}
|
|
138
|
+
{!hideTryItPanel && <ScalarApiButton context={getOpenAPIClientContext(context)} method={method} path={path} securities={securities} servers={servers} specUrl={specUrl} />}
|
|
139
|
+
</div>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get custom code samples for the operation.
|
|
143
|
+
*/
|
|
144
|
+
function getCustomCodeSamples(props) {
|
|
145
|
+
const { data, context } = props;
|
|
146
|
+
let customCodeSamples = null;
|
|
147
|
+
CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => {
|
|
148
|
+
const customSamples = data.operation[key];
|
|
149
|
+
if (customSamples && Array.isArray(customSamples)) customCodeSamples = customSamples.filter((sample) => {
|
|
150
|
+
return typeof sample.source === "string" && typeof sample.lang === "string";
|
|
151
|
+
}).map((sample, index) => ({
|
|
152
|
+
key: `custom-sample-${sample.lang}-${index}`,
|
|
153
|
+
label: sample.label || sample.lang,
|
|
154
|
+
body: context.renderCodeBlock({
|
|
155
|
+
code: sample.source,
|
|
156
|
+
syntax: sample.lang
|
|
157
|
+
}),
|
|
158
|
+
footer: <OpenAPICodeSampleFooter renderers={[]} data={data} context={context} />
|
|
159
|
+
}));
|
|
160
|
+
});
|
|
161
|
+
return customCodeSamples;
|
|
162
|
+
}
|
|
163
|
+
function getSecurityHeaders(args) {
|
|
164
|
+
const { securityRequirement, securities } = args;
|
|
165
|
+
const operationSecurityInfo = extractOperationSecurityInfo({
|
|
166
|
+
securityRequirement,
|
|
167
|
+
securities
|
|
168
|
+
});
|
|
169
|
+
if (operationSecurityInfo.length === 0) return {};
|
|
170
|
+
const selectedSecurity = operationSecurityInfo.at(0);
|
|
171
|
+
if (!selectedSecurity) return {};
|
|
172
|
+
const headers = {};
|
|
173
|
+
for (const security of selectedSecurity.schemes) switch (security.type) {
|
|
174
|
+
case "http": {
|
|
175
|
+
let scheme = security.scheme;
|
|
176
|
+
const format = resolvePrefillCodePlaceholderFromSecurityScheme({
|
|
177
|
+
security,
|
|
178
|
+
defaultPlaceholderValue: scheme?.includes("basic") ? "username:password" : "YOUR_SECRET_TOKEN"
|
|
179
|
+
});
|
|
180
|
+
if (scheme?.includes("bearer")) scheme = "Bearer";
|
|
181
|
+
else if (scheme?.includes("basic")) scheme = "Basic";
|
|
182
|
+
else if (scheme?.includes("token")) scheme = "Token";
|
|
183
|
+
headers.Authorization = `${scheme} ${format}`;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "apiKey": {
|
|
187
|
+
if (security.in !== "header") break;
|
|
188
|
+
const name = security.name ?? "Authorization";
|
|
189
|
+
headers[name] = resolvePrefillCodePlaceholderFromSecurityScheme({
|
|
190
|
+
security,
|
|
191
|
+
defaultPlaceholderValue: "YOUR_API_KEY"
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "oauth2":
|
|
196
|
+
headers.Authorization = `Bearer ${resolvePrefillCodePlaceholderFromSecurityScheme({
|
|
197
|
+
security,
|
|
198
|
+
defaultPlaceholderValue: "YOUR_OAUTH2_TOKEN"
|
|
199
|
+
})}`;
|
|
200
|
+
break;
|
|
201
|
+
default: break;
|
|
202
|
+
}
|
|
203
|
+
return headers;
|
|
204
|
+
}
|
|
205
|
+
function validateHttpMethod(method) {
|
|
206
|
+
return [
|
|
207
|
+
"get",
|
|
208
|
+
"post",
|
|
209
|
+
"put",
|
|
210
|
+
"delete",
|
|
211
|
+
"patch",
|
|
212
|
+
"head",
|
|
213
|
+
"options",
|
|
214
|
+
"trace"
|
|
215
|
+
].includes(method);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//#endregion
|
|
219
|
+
export { OpenAPICodeSample };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { createStateKey } from "./utils.js";
|
|
5
|
+
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from "./OpenAPISelect.js";
|
|
6
|
+
import clsx from "classnames";
|
|
7
|
+
|
|
8
|
+
//#region src/OpenAPICodeSampleInteractive.tsx
|
|
9
|
+
function OpenAPIMediaTypeExamplesSelector(props) {
|
|
10
|
+
const { method, path, renderers, selectIcon, blockKey } = props;
|
|
11
|
+
if (!renderers[0]) throw new Error("No renderers provided");
|
|
12
|
+
const stateKey = createStateKey("request-body-media-type", blockKey);
|
|
13
|
+
const state = useSelectState(stateKey, renderers[0].mediaType);
|
|
14
|
+
const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0];
|
|
15
|
+
return <div className="openapi-codesample-selectors">
|
|
16
|
+
<MediaTypeSelector selectIcon={selectIcon} stateKey={stateKey} renderers={renderers} />
|
|
17
|
+
<ExamplesSelector selectIcon={selectIcon} method={method} path={path} renderer={selected} />
|
|
18
|
+
</div>;
|
|
19
|
+
}
|
|
20
|
+
function MediaTypeSelector(props) {
|
|
21
|
+
const { renderers, stateKey, selectIcon } = props;
|
|
22
|
+
if (renderers.length < 2) return null;
|
|
23
|
+
const items = renderers.map((renderer) => ({
|
|
24
|
+
key: renderer.mediaType,
|
|
25
|
+
label: renderer.mediaType
|
|
26
|
+
}));
|
|
27
|
+
return <OpenAPISelect className={clsx("openapi-select")} items={renderers.map((renderer) => ({
|
|
28
|
+
key: renderer.mediaType,
|
|
29
|
+
label: renderer.mediaType
|
|
30
|
+
}))} icon={selectIcon} stateKey={stateKey} placement="bottom start">
|
|
31
|
+
{items.map((item) => <OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
|
32
|
+
{item.label}
|
|
33
|
+
</OpenAPISelectItem>)}
|
|
34
|
+
</OpenAPISelect>;
|
|
35
|
+
}
|
|
36
|
+
function ExamplesSelector(props) {
|
|
37
|
+
const { method, path, renderer, selectIcon } = props;
|
|
38
|
+
if (renderer.examples.length < 2) return null;
|
|
39
|
+
const items = renderer.examples.map((example, index) => ({
|
|
40
|
+
key: index,
|
|
41
|
+
label: example.example.summary || `Example ${index + 1}`
|
|
42
|
+
}));
|
|
43
|
+
return <OpenAPISelect items={items} icon={selectIcon} stateKey={`media-type-sample-${renderer.mediaType}-${method}-${path}`} placement="bottom start">
|
|
44
|
+
{items.map((item) => <OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
|
45
|
+
{item.label}
|
|
46
|
+
</OpenAPISelectItem>)}
|
|
47
|
+
</OpenAPISelect>;
|
|
48
|
+
}
|
|
49
|
+
function OpenAPIMediaTypeExamplesBody(props) {
|
|
50
|
+
const { renderers, method, path, blockKey } = props;
|
|
51
|
+
if (!renderers[0]) throw new Error("No renderers provided");
|
|
52
|
+
const mediaTypeState = useSelectState(createStateKey("request-body-media-type", blockKey), renderers[0].mediaType);
|
|
53
|
+
const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0];
|
|
54
|
+
if (selected.examples.length === 0) return selected.element;
|
|
55
|
+
return <ExamplesBody method={method} path={path} renderer={selected} />;
|
|
56
|
+
}
|
|
57
|
+
function ExamplesBody(props) {
|
|
58
|
+
const { method, path, renderer } = props;
|
|
59
|
+
const exampleState = useSelectState(`media-type-sample-${renderer.mediaType}-${method}-${path}`, renderer.mediaType);
|
|
60
|
+
const example = renderer.examples[Number(exampleState.key)] ?? renderer.examples[0];
|
|
61
|
+
if (!example) throw new Error(`No example found for key ${exampleState.key}`);
|
|
62
|
+
return example.element;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { OpenAPIMediaTypeExamplesBody, OpenAPIMediaTypeExamplesSelector };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { StaticSection } from "./StaticSection.js";
|
|
5
|
+
import { getOrCreateStoreByKey } from "./getOrCreateStoreByKey.js";
|
|
6
|
+
import { OpenAPISelect, OpenAPISelectItem } from "./OpenAPISelect.js";
|
|
7
|
+
import { OpenAPIPath } from "./OpenAPIPath.js";
|
|
8
|
+
import { useCallback } from "react";
|
|
9
|
+
import { useStore } from "zustand";
|
|
10
|
+
|
|
11
|
+
//#region src/OpenAPICodeSampleSelector.tsx
|
|
12
|
+
function useCodeSampleState(initialKey = "default") {
|
|
13
|
+
const store = useStore(getOrCreateStoreByKey("codesample", initialKey));
|
|
14
|
+
return {
|
|
15
|
+
key: store.key,
|
|
16
|
+
setKey: useCallback((key) => store.setKey(key), [store.setKey])
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function OpenAPICodeSampleHeader(props) {
|
|
20
|
+
const { data, items, selectIcon, context } = props;
|
|
21
|
+
return <>
|
|
22
|
+
<OpenAPIPath context={context} canCopy={false} withServer={false} data={data} />
|
|
23
|
+
{items.length > 1 ? <OpenAPISelect icon={selectIcon} items={items} stateKey="codesample" placement="bottom end">
|
|
24
|
+
{items.map((item) => <OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
|
25
|
+
{item.label}
|
|
26
|
+
</OpenAPISelectItem>)}
|
|
27
|
+
</OpenAPISelect> : items[0] ? <span className="openapi-codesample-label">{items[0].label}</span> : null}
|
|
28
|
+
</>;
|
|
29
|
+
}
|
|
30
|
+
function OpenAPICodeSampleBody(props) {
|
|
31
|
+
const { items, data, selectIcon, context } = props;
|
|
32
|
+
if (!items[0]) throw new Error("No items provided");
|
|
33
|
+
const state = useCodeSampleState(items[0]?.key);
|
|
34
|
+
const selected = items.find((item) => item.key === state.key) || items[0];
|
|
35
|
+
if (!selected) return null;
|
|
36
|
+
return <StaticSection header={<OpenAPICodeSampleHeader context={context} selectIcon={selectIcon} data={data} items={items} />} className="openapi-codesample">
|
|
37
|
+
<div id={selected.key} className="openapi-codesample-panel">
|
|
38
|
+
{selected.body ? selected.body : null}
|
|
39
|
+
{selected.footer ? selected.footer : null}
|
|
40
|
+
</div>
|
|
41
|
+
</StaticSection>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { OpenAPICodeSampleBody };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { t } from "./translate.js";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Button, Tooltip, TooltipTrigger } from "react-aria-components";
|
|
7
|
+
|
|
8
|
+
//#region src/OpenAPICopyButton.tsx
|
|
9
|
+
function OpenAPICopyButton(props) {
|
|
10
|
+
const { value, label, children, onPress, className, context, withTooltip = true } = props;
|
|
11
|
+
const [copied, setCopied] = useState(false);
|
|
12
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
13
|
+
const handleCopy = () => {
|
|
14
|
+
if (!value) return;
|
|
15
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
16
|
+
setIsOpen(true);
|
|
17
|
+
setCopied(true);
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
setCopied(false);
|
|
20
|
+
setIsOpen(false);
|
|
21
|
+
}, 2e3);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
return <TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} isDisabled={!withTooltip} closeDelay={200} delay={200}>
|
|
25
|
+
<Button type="button" preventFocusOnPress onPress={(e) => {
|
|
26
|
+
handleCopy();
|
|
27
|
+
onPress?.(e);
|
|
28
|
+
}} className={`openapi-copy-button ${className}`} {...props}>
|
|
29
|
+
{children}
|
|
30
|
+
</Button>
|
|
31
|
+
|
|
32
|
+
<Tooltip isOpen={isOpen} onOpenChange={setIsOpen} placement="top" offset={4} className="openapi-tooltip">
|
|
33
|
+
{copied ? t(context.translation, "copied") : label || t(context.translation, "copy_to_clipboard")}
|
|
34
|
+
</Tooltip>
|
|
35
|
+
</TooltipTrigger>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { OpenAPICopyButton };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import clsx from "classnames";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Button, Disclosure, DisclosurePanel } from "react-aria-components";
|
|
7
|
+
|
|
8
|
+
//#region src/OpenAPIDisclosure.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Display an interactive OpenAPI disclosure.
|
|
11
|
+
*/
|
|
12
|
+
function OpenAPIDisclosure(props) {
|
|
13
|
+
const { icon, header, label, children, className } = props;
|
|
14
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
15
|
+
return <Disclosure className={clsx("openapi-disclosure", className)} isExpanded={isExpanded} onExpandedChange={setIsExpanded}>
|
|
16
|
+
<Button slot="trigger" className="openapi-disclosure-trigger" style={({ isFocusVisible }) => ({ outline: isFocusVisible ? "2px solid rgb(var(--primary-color-500) / 0.4)" : "none" })}>
|
|
17
|
+
{header}
|
|
18
|
+
<div className="openapi-disclosure-trigger-label">
|
|
19
|
+
<span>{typeof label === "function" ? label(isExpanded) : label}</span>
|
|
20
|
+
{icon}
|
|
21
|
+
</div>
|
|
22
|
+
</Button>
|
|
23
|
+
<DisclosurePanel className="openapi-disclosure-panel">
|
|
24
|
+
{isExpanded ? children : null}
|
|
25
|
+
</DisclosurePanel>
|
|
26
|
+
</Disclosure>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { OpenAPIDisclosure };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from "./OpenAPISelect.js";
|
|
5
|
+
import { useDisclosureGroupState, useDisclosureState } from "./node_modules/react-stately/dist/import.js";
|
|
6
|
+
import { createContext, useContext, useRef } from "react";
|
|
7
|
+
import { mergeProps, useButton, useDisclosure, useFocusRing, useId as useId$1 } from "react-aria";
|
|
8
|
+
|
|
9
|
+
//#region src/OpenAPIDisclosureGroup.tsx
|
|
10
|
+
const DisclosureGroupStateContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* Display an interactive OpenAPI disclosure group.
|
|
13
|
+
*/
|
|
14
|
+
function OpenAPIDisclosureGroup(props) {
|
|
15
|
+
const { icon, groups, selectStateKey, selectIcon } = props;
|
|
16
|
+
const state = useDisclosureGroupState(props);
|
|
17
|
+
return <DisclosureGroupStateContext.Provider value={state}>
|
|
18
|
+
{groups.map((group) => <DisclosureItem selectStateKey={selectStateKey} selectIcon={selectIcon} icon={icon} key={group.key} group={group} />)}
|
|
19
|
+
</DisclosureGroupStateContext.Provider>;
|
|
20
|
+
}
|
|
21
|
+
function DisclosureItem(props) {
|
|
22
|
+
const { icon, group, selectStateKey, selectIcon } = props;
|
|
23
|
+
const defaultId = useId$1();
|
|
24
|
+
const id = group.key || defaultId;
|
|
25
|
+
const groupState = useContext(DisclosureGroupStateContext);
|
|
26
|
+
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
|
27
|
+
const state = useDisclosureState({
|
|
28
|
+
isExpanded,
|
|
29
|
+
onExpandedChange() {
|
|
30
|
+
if (groupState) groupState.toggleKey(id);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const panelRef = useRef(null);
|
|
34
|
+
const triggerRef = useRef(null);
|
|
35
|
+
const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
|
|
36
|
+
const { buttonProps: triggerProps, panelProps } = useDisclosure({
|
|
37
|
+
...props,
|
|
38
|
+
isExpanded,
|
|
39
|
+
isDisabled
|
|
40
|
+
}, state, panelRef);
|
|
41
|
+
const { buttonProps } = useButton(triggerProps, triggerRef);
|
|
42
|
+
const { isFocusVisible, focusProps } = useFocusRing();
|
|
43
|
+
const store = useSelectState(selectStateKey, group.tabs?.[0]?.key || "");
|
|
44
|
+
const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];
|
|
45
|
+
return <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
|
|
46
|
+
<div slot="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} aria-disabled={isDisabled} style={{ outline: isFocusVisible ? "2px solid rgb(var(--primary-color-500)/0.4)" : "none" }} className="openapi-disclosure-group-trigger">
|
|
47
|
+
<div className="openapi-disclosure-group-icon">
|
|
48
|
+
{icon || <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
|
|
49
|
+
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
50
|
+
</svg>}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="openapi-disclosure-group-label">
|
|
54
|
+
{group.label}
|
|
55
|
+
|
|
56
|
+
{group.tabs ? <div className="openapi-disclosure-group-mediatype" onClick={(e) => e.stopPropagation()}>
|
|
57
|
+
{group.tabs?.length > 1 ? <OpenAPISelect icon={selectIcon} stateKey={selectStateKey} onSelectionChange={() => {
|
|
58
|
+
state.expand();
|
|
59
|
+
}} items={group.tabs} placement="bottom end">
|
|
60
|
+
{group.tabs.map((tab) => <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
|
|
61
|
+
{tab.label}
|
|
62
|
+
</OpenAPISelectItem>)}
|
|
63
|
+
</OpenAPISelect> : group.tabs[0]?.label ? <span>{group.tabs[0].label}</span> : null}
|
|
64
|
+
</div> : null}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{state.isExpanded && selectedTab && <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}>
|
|
69
|
+
{selectedTab.body}
|
|
70
|
+
</div>}
|
|
71
|
+
</div>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { OpenAPIDisclosureGroup };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { json2xml } from "./json2xml.js";
|
|
2
|
+
import { stringifyOpenAPI } from "./stringifyOpenAPI.js";
|
|
3
|
+
import { t } from "./translate.js";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
|
|
6
|
+
//#region src/OpenAPIExample.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Display an example.
|
|
9
|
+
*/
|
|
10
|
+
function OpenAPIExample(props) {
|
|
11
|
+
const { example, context, syntax } = props;
|
|
12
|
+
const code = stringifyExample({
|
|
13
|
+
example,
|
|
14
|
+
syntax
|
|
15
|
+
});
|
|
16
|
+
if (code === null) return <OpenAPIEmptyExample context={context} />;
|
|
17
|
+
return context.renderCodeBlock({
|
|
18
|
+
code,
|
|
19
|
+
syntax
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function stringifyExample(args) {
|
|
23
|
+
const { example, syntax } = args;
|
|
24
|
+
if (!example.value) return null;
|
|
25
|
+
if (typeof example.value === "string") return example.value;
|
|
26
|
+
if (syntax === "xml") return json2xml(example.value);
|
|
27
|
+
if (syntax === "yaml") return yaml.dump(example.value).replace(/'/g, "").replace(/\\n/g, "\n");
|
|
28
|
+
return stringifyOpenAPI(example.value, null, 2);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Empty response example.
|
|
32
|
+
*/
|
|
33
|
+
function OpenAPIEmptyExample(props) {
|
|
34
|
+
const { context } = props;
|
|
35
|
+
return <pre className="openapi-example-empty">
|
|
36
|
+
<p>{t(context.translation, "no_content")}</p>
|
|
37
|
+
</pre>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { OpenAPIEmptyExample, OpenAPIExample };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { OpenAPIEmptyExample } from "./OpenAPIExample.js";
|
|
5
|
+
import { StaticSection } from "./StaticSection.js";
|
|
6
|
+
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from "./OpenAPISelect.js";
|
|
7
|
+
|
|
8
|
+
//#region src/OpenAPIMediaType.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Get the state of the response examples select.
|
|
11
|
+
*/
|
|
12
|
+
function useMediaTypesState(stateKey, initialKey = "default") {
|
|
13
|
+
return useSelectState(stateKey, initialKey);
|
|
14
|
+
}
|
|
15
|
+
function useMediaTypeExamplesState(stateKey, initialKey = "default") {
|
|
16
|
+
return useSelectState(stateKey, initialKey);
|
|
17
|
+
}
|
|
18
|
+
function OpenAPIMediaTypeContent(props) {
|
|
19
|
+
const { stateKey, items, selectIcon, context } = props;
|
|
20
|
+
const state = useMediaTypesState(stateKey, items[0]?.key);
|
|
21
|
+
const examples = items.find((item) => item.key === state.key)?.examples ?? [];
|
|
22
|
+
if (!items.length && !examples.length) return null;
|
|
23
|
+
return <StaticSection footer={items.length > 1 || examples.length > 1 ? <OpenAPIMediaTypeFooter items={items} examples={examples} selectIcon={selectIcon} stateKey={stateKey} /> : null} className="openapi-response-media-types-examples">
|
|
24
|
+
<OpenAPIMediaTypeBody context={context} stateKey={stateKey} items={items} examples={examples} />
|
|
25
|
+
</StaticSection>;
|
|
26
|
+
}
|
|
27
|
+
function OpenAPIMediaTypeFooter(props) {
|
|
28
|
+
const { items, examples, stateKey, selectIcon } = props;
|
|
29
|
+
return <>
|
|
30
|
+
{items.length > 1 && <OpenAPISelect icon={selectIcon} items={items} stateKey={stateKey} placement="bottom start">
|
|
31
|
+
{items.map((item) => <OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
|
32
|
+
<span>{item.label}</span>
|
|
33
|
+
</OpenAPISelectItem>)}
|
|
34
|
+
</OpenAPISelect>}
|
|
35
|
+
|
|
36
|
+
{examples && examples.length > 1 ? <OpenAPISelect icon={selectIcon} items={examples} stateKey={`${stateKey}-examples`} placement="bottom start">
|
|
37
|
+
{examples.map((example) => <OpenAPISelectItem key={example.key} id={example.key} value={example}>
|
|
38
|
+
<span>{example.label}</span>
|
|
39
|
+
</OpenAPISelectItem>)}
|
|
40
|
+
</OpenAPISelect> : null}
|
|
41
|
+
</>;
|
|
42
|
+
}
|
|
43
|
+
function OpenAPIMediaTypeBody(props) {
|
|
44
|
+
const { stateKey, items, examples, context } = props;
|
|
45
|
+
const state = useMediaTypesState(stateKey, items[0]?.key);
|
|
46
|
+
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
|
|
47
|
+
const exampleState = useMediaTypeExamplesState(`${stateKey}-examples`, selectedItem?.examples?.[0]?.key);
|
|
48
|
+
if (!selectedItem) return null;
|
|
49
|
+
if (examples) {
|
|
50
|
+
const selectedExample = examples.find((example) => example.key === exampleState.key) ?? examples[0];
|
|
51
|
+
if (!selectedExample) return <OpenAPIEmptyExample context={context} />;
|
|
52
|
+
return selectedExample.body;
|
|
53
|
+
}
|
|
54
|
+
return selectedItem.body;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { OpenAPIMediaTypeContent };
|