Skip to content

Commit 0759b59

Browse files
committed
Merge branch 'main' into v2-site-linking
2 parents 9a88148 + f5516d4 commit 0759b59

File tree

10 files changed

+341
-101
lines changed

10 files changed

+341
-101
lines changed

.changeset/green-lizards-change.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gitbook/react-openapi": patch
3+
"gitbook": patch
4+
---
5+
6+
Support body examples

.changeset/new-squids-approve.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gitbook/react-openapi": patch
3+
"gitbook": patch
4+
---
5+
6+
Mark properties as optional if not required

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

+9-1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@
201201
@apply text-warning-subtle text-[0.813rem];
202202
}
203203

204+
.openapi-schema-optional {
205+
@apply text-info-subtle text-[0.813rem];
206+
}
207+
204208
.openapi-schema-readonly {
205209
@apply text-primary-subtle/9 text-[0.813rem];
206210
}
@@ -392,7 +396,11 @@
392396
}
393397

394398
.openapi-codesample-footer {
395-
@apply flex w-full justify-end;
399+
@apply flex gap-3 w-full justify-between flex-wrap;
400+
}
401+
402+
.openapi-codesample-selectors {
403+
@apply flex flex-row items-center gap-3 flex-wrap;
396404
}
397405

398406
/* Path */

packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
7070
href={`#${section.id}`}
7171
className={tcls(
7272
'relative',
73-
'flex',
74-
'flex-row',
75-
'items-baseline',
7673
'z-10',
7774
'text-sm',
7875

@@ -126,7 +123,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
126123
>
127124
{section.tag ? (
128125
<span
129-
className={`openapi-method openapi-method-${section.tag.toLowerCase()}`}
126+
className={`-mt-0.5 openapi-method openapi-method-${section.tag.toLowerCase()}`}
130127
>
131128
{section.tag}
132129
</span>

packages/react-openapi/src/OpenAPICodeSample.tsx

+174-78
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,64 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2+
import {
3+
OpenAPIMediaTypeExamplesBody,
4+
OpenAPIMediaTypeExamplesSelector,
5+
} from './OpenAPICodeSampleInteractive';
26
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
37
import { ScalarApiButton } from './ScalarApiButton';
48
import { StaticSection } from './StaticSection';
5-
import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
6-
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
9+
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
10+
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
711
import { stringifyOpenAPI } from './stringifyOpenAPI';
812
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
913
import { getDefaultServerURL } from './util/server';
1014
import { checkIsReference, createStateKey } from './utils';
1115

16+
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
17+
1218
/**
1319
* Display code samples to execute the operation.
1420
* It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/)
1521
*/
1622
export function OpenAPICodeSample(props: {
1723
data: OpenAPIOperationData;
1824
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;
1962
}) {
2063
const { data, context } = props;
2164

@@ -51,97 +94,102 @@ export function OpenAPICodeSample(props: {
5194
const requestBody = !checkIsReference(data.operation.requestBody)
5295
? data.operation.requestBody
5396
: undefined;
54-
const requestBodyContentEntries = requestBody?.content
55-
? Object.entries(requestBody.content)
56-
: undefined;
57-
const requestBodyContent = requestBodyContentEntries?.[0];
58-
59-
const input: CodeSampleInput = {
60-
url:
61-
getDefaultServerURL(data.servers) +
62-
data.path +
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,
75106
};
76107

77-
const autoCodeSamples = codeSampleGenerators.map((generator) => ({
78-
key: `default-${generator.id}`,
79-
label: generator.label,
80-
body: context.renderCodeBlock({
81-
code: generator.generate(input),
82-
syntax: generator.syntax,
83-
}),
84-
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
85-
}));
86-
87-
// Use custom samples if defined
88-
let customCodeSamples: null | Array<{
89-
key: string;
90-
label: string;
91-
body: React.ReactNode;
92-
}> = null;
93-
(['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => {
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,
110125
}),
111-
footer: <OpenAPICodeSampleFooter data={data} context={context} />,
112-
}));
126+
examples: generateMediaTypeExamples(mediaTypeObject).map((example) => ({
127+
example,
128+
element: context.renderCodeBlock({
129+
code: generator.generate({
130+
url,
131+
method: data.method,
132+
body: example.value,
133+
headers: mediaTypeHeaders,
134+
}),
135+
syntax: generator.syntax,
136+
}),
137+
})),
138+
} satisfies MediaTypeRenderer;
139+
};
113140
}
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 : []);
141+
);
121142

122-
if (samples.length === 0) {
123-
return null;
124-
}
143+
return codeSampleGenerators.map((generator) => {
144+
if (mediaTypeRendererFactories.length > 0) {
145+
const renderers = mediaTypeRendererFactories.map((generate) => generate(generator));
146+
return {
147+
key: `default-${generator.id}`,
148+
label: generator.label,
149+
body: <OpenAPIMediaTypeExamplesBody data={data} renderers={renderers} />,
150+
footer: (
151+
<OpenAPICodeSampleFooter renderers={renderers} data={data} context={context} />
152+
),
153+
};
154+
}
155+
return {
156+
key: `default-${generator.id}`,
157+
label: generator.label,
158+
body: context.renderCodeBlock({
159+
code: generator.generate({
160+
url,
161+
method: data.method,
162+
body: undefined,
163+
headers: genericHeaders,
164+
}),
165+
syntax: generator.syntax,
166+
}),
167+
footer: <OpenAPICodeSampleFooter data={data} renderers={[]} context={context} />,
168+
};
169+
});
170+
}
125171

126-
return (
127-
<OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
128-
<StaticSection header={<OpenAPITabsList />} className="openapi-codesample">
129-
<OpenAPITabsPanels />
130-
</StaticSection>
131-
</OpenAPITabs>
132-
);
172+
export interface MediaTypeRenderer {
173+
mediaType: string;
174+
element: React.ReactNode;
175+
examples: Array<{
176+
example: OpenAPIV3.ExampleObject;
177+
element: React.ReactNode;
178+
}>;
133179
}
134180

135181
function OpenAPICodeSampleFooter(props: {
136182
data: OpenAPIOperationData;
183+
renderers: MediaTypeRenderer[];
137184
context: OpenAPIContextProps;
138185
}) {
139-
const { data, context } = props;
186+
const { data, context, renderers } = props;
140187
const { method, path } = data;
141188
const { specUrl } = context;
142189
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
190+
const hasMediaTypes = renderers.length > 0;
143191

144-
if (hideTryItPanel) {
192+
if (hideTryItPanel && !hasMediaTypes) {
145193
return null;
146194
}
147195

@@ -151,11 +199,59 @@ function OpenAPICodeSampleFooter(props: {
151199

152200
return (
153201
<div className="openapi-codesample-footer">
154-
<ScalarApiButton method={method} path={path} specUrl={specUrl} />
202+
{hasMediaTypes ? (
203+
<OpenAPIMediaTypeExamplesSelector data={data} renderers={renderers} />
204+
) : (
205+
<span />
206+
)}
207+
{!hideTryItPanel && <ScalarApiButton method={method} path={path} specUrl={specUrl} />}
155208
</div>
156209
);
157210
}
158211

212+
/**
213+
* Get custom code samples for the operation.
214+
*/
215+
function getCustomCodeSamples(props: {
216+
data: OpenAPIOperationData;
217+
context: OpenAPIContextProps;
218+
}) {
219+
const { data, context } = props;
220+
221+
let customCodeSamples: null | Array<{
222+
key: string;
223+
label: string;
224+
body: React.ReactNode;
225+
}> = null;
226+
227+
CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => {
228+
const customSamples = data.operation[key];
229+
if (customSamples && Array.isArray(customSamples)) {
230+
customCodeSamples = customSamples
231+
.filter((sample) => {
232+
return (
233+
typeof sample.label === 'string' &&
234+
typeof sample.source === 'string' &&
235+
typeof sample.lang === 'string'
236+
);
237+
})
238+
.map((sample, index) => ({
239+
key: `custom-sample-${sample.lang}-${index}`,
240+
label: sample.label,
241+
body: context.renderCodeBlock({
242+
code: sample.source,
243+
syntax: sample.lang,
244+
}),
245+
footer: (
246+
<OpenAPICodeSampleFooter renderers={[]} data={data} context={context} />
247+
),
248+
}));
249+
}
250+
});
251+
252+
return customCodeSamples;
253+
}
254+
159255
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
160256
[key: string]: string;
161257
} {

0 commit comments

Comments
 (0)