Skip to content

Commit 7ea8f7c

Browse files
committed
Fix OpenAPI spec rendering
1 parent ae78fc5 commit 7ea8f7c

File tree

12 files changed

+111
-96
lines changed

12 files changed

+111
-96
lines changed

.changeset/stupid-peaches-work.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gitbook/react-openapi": patch
3+
"gitbook": patch
4+
---
5+
6+
Fix spec properties rendering and missing keys

packages/gitbook/src/components/DocumentView/Blocks.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
5454
const { nodes, blockStyle, isOffscreen: defaultIsOffscreen = false, ...contextProps } = props;
5555

5656
let isOffscreen = defaultIsOffscreen;
57-
return nodes.map((node) => {
57+
return nodes.map((node, index) => {
5858
isOffscreen =
5959
isOffscreen ||
6060
isBlockOffscreen({
@@ -65,7 +65,7 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
6565

6666
return (
6767
<Block
68-
key={node.key}
68+
key={node.key || `${node.type}-${index}`}
6969
block={node}
7070
style={[
7171
'mx-auto w-full decoration-primary/6',

packages/gitbook/src/components/DocumentView/Inlines.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ export function Inlines<T extends DocumentInline | DocumentText>(
2424
) {
2525
const { nodes, document, ancestorInlines, ...contextProps } = props;
2626

27-
return nodes.map((node) => {
27+
return nodes.map((node, index) => {
28+
const key = node.key || `key-${index}`;
29+
2830
if (node.object === 'text') {
29-
return <Text key={node.key} text={node} />;
31+
return <Text key={key} text={node} />;
3032
}
3133

3234
return (
3335
<Inline
34-
key={node.key}
36+
key={key}
3537
inline={node}
3638
document={document}
3739
ancestorInlines={ancestorInlines}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,6 @@
166166
@apply pb-3;
167167
}
168168

169-
.openapi-schema > .openapi-schema-properties {
170-
@apply mt-3;
171-
}
172-
173169
/* Schema Presentation */
174170
.openapi-schema-presentation {
175171
@apply flex flex-col gap-1.5 font-normal;

packages/react-openapi/src/InteractiveSection.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ export function InteractiveSection(props: {
3232
defaultTab?: string;
3333
/** Content of the header */
3434
header?: React.ReactNode;
35-
/** Body of the section */
36-
children?: React.ReactNode;
3735
/** Children to display within the container */
3836
overlay?: React.ReactNode;
3937
}) {
@@ -45,7 +43,6 @@ export function InteractiveSection(props: {
4543
tabs = [],
4644
defaultTab = tabs[0]?.key,
4745
header,
48-
children,
4946
overlay,
5047
toggleIcon = '▶',
5148
} = props;
@@ -83,7 +80,7 @@ export function InteractiveSection(props: {
8380
className={className}
8481
>
8582
<SectionHeaderContent className={className}>
86-
{(children || selectedTab?.body) && toggeable ? (
83+
{selectedTab?.body && toggeable ? (
8784
<button
8885
{...mergeProps(buttonProps, focusProps)}
8986
ref={triggerRef}
@@ -131,9 +128,8 @@ export function InteractiveSection(props: {
131128
</div>
132129
</SectionHeader>
133130
) : null}
134-
{(!toggeable || state.isExpanded) && (children || selectedTab?.body) ? (
131+
{(!toggeable || state.isExpanded) && selectedTab?.body ? (
135132
<SectionBody ref={panelRef} {...panelProps} className={className}>
136-
{children}
137133
{selectedTab?.body}
138134
</SectionBody>
139135
) : null}

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ function OpenAPICodeSampleFooter(props: {
189189
const { method, path } = data;
190190
const { specUrl } = context;
191191
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
192-
const hasMediaTypes = renderers.length > 0;
192+
const hasMultipleMediaTypes = renderers.length > 1;
193193

194-
if (hideTryItPanel && !hasMediaTypes) {
194+
if (hideTryItPanel && !hasMultipleMediaTypes) {
195195
return null;
196196
}
197197

@@ -201,7 +201,7 @@ function OpenAPICodeSampleFooter(props: {
201201

202202
return (
203203
<div className="openapi-codesample-footer">
204-
{hasMediaTypes ? (
204+
{hasMultipleMediaTypes ? (
205205
<OpenAPIMediaTypeExamplesSelector data={data} renderers={renderers} />
206206
) : (
207207
<span />

packages/react-openapi/src/OpenAPIPath.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function formatPath(path: string) {
4747
parts.push(path.slice(lastIndex, offset));
4848
}
4949
parts.push(
50-
<span key={offset} className="openapi-path-variable">
50+
<span key={`offset-${offset}`} className="openapi-path-variable">
5151
{match}
5252
</span>
5353
);
@@ -61,7 +61,7 @@ function formatPath(path: string) {
6161

6262
const formattedPath = parts.map((part, index) => {
6363
if (typeof part === 'string') {
64-
return <span key={index}>{part}</span>;
64+
return <span key={`part-${index}`}>{part}</span>;
6565
}
6666
return part;
6767
});

packages/react-openapi/src/OpenAPIResponse.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ export function OpenAPIResponse(props: {
2929
{headers.length > 0 ? (
3030
<OpenAPIDisclosure context={context} label="Headers">
3131
<OpenAPISchemaProperties
32-
properties={headers.map(([name, header]) => {
33-
return parameterToProperty({ name, ...header });
34-
})}
32+
properties={headers.map(([name, header]) =>
33+
parameterToProperty({ name, ...header })
34+
)}
3535
context={context}
3636
/>
3737
</OpenAPIDisclosure>

packages/react-openapi/src/OpenAPIResponses.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ export function OpenAPIResponses(props: {
2727
return {
2828
id: statusCode,
2929
label: (
30-
<div
31-
className="openapi-response-tab-content"
32-
key={`response-${statusCode}`}
33-
>
30+
<div className="openapi-response-tab-content">
3431
<span className="openapi-response-statuscode">
3532
{statusCode}
3633
</span>
@@ -47,7 +44,6 @@ export function OpenAPIResponses(props: {
4744
label: contentType,
4845
body: (
4946
<OpenAPIResponse
50-
key={`$response-${statusCode}-${contentType}`}
5147
response={response}
5248
mediaType={mediaType}
5349
context={context}

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
'use client';
2+
// This component does not use any client feature but we don't want to
3+
// render it server-side because it has recursion.
4+
15
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
26
import { useId } from 'react';
37

@@ -22,15 +26,10 @@ interface OpenAPISchemaPropertyEntry {
2226
function OpenAPISchemaProperty(props: {
2327
property: OpenAPISchemaPropertyEntry;
2428
context: OpenAPIClientContext;
25-
circularRefs?: CircularRefsIds;
29+
circularRefs: CircularRefsIds;
2630
className?: string;
2731
}) {
28-
const {
29-
property,
30-
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
31-
context,
32-
className,
33-
} = props;
32+
const { circularRefs: parentCircularRefs, context, className, property } = props;
3433

3534
const { schema } = property;
3635

@@ -40,37 +39,41 @@ function OpenAPISchemaProperty(props: {
4039
<div id={id} className={clsx('openapi-schema', className)}>
4140
<OpenAPISchemaPresentation property={property} />
4241
{(() => {
43-
const parentCircularRef = parentCircularRefs.get(schema);
44-
42+
const circularRefId = parentCircularRefs.get(schema);
4543
// Avoid recursing infinitely, and instead render a link to the parent schema
46-
if (parentCircularRef) {
47-
return <OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />;
44+
if (circularRefId) {
45+
return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />;
4846
}
4947

50-
const circularRefs = parentCircularRefs.set(schema, id);
48+
const circularRefs = new Map(parentCircularRefs);
49+
circularRefs.set(schema, id);
50+
5151
const properties = getSchemaProperties(schema);
52-
const alternatives = getSchemaAlternatives(schema, new Set(circularRefs.keys()));
53-
return (
54-
<>
55-
{alternatives?.map((schema, index) => (
56-
<OpenAPISchemaAlternative
57-
key={index}
58-
schema={schema}
59-
circularRefs={circularRefs}
60-
context={context}
61-
/>
62-
))}
63-
{properties?.length ? (
64-
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
65-
<OpenAPISchemaProperties
66-
properties={properties}
67-
circularRefs={circularRefs}
68-
context={context}
69-
/>
70-
</OpenAPIDisclosure>
71-
) : null}
72-
</>
73-
);
52+
if (properties) {
53+
return (
54+
<OpenAPISchemaProperties
55+
properties={properties}
56+
circularRefs={circularRefs}
57+
context={context}
58+
/>
59+
);
60+
}
61+
62+
const ancestors = new Set(circularRefs.keys());
63+
const alternatives = getSchemaAlternatives(schema, ancestors);
64+
65+
if (alternatives) {
66+
return alternatives.map((schema, index) => (
67+
<OpenAPISchemaAlternative
68+
key={index}
69+
schema={schema}
70+
circularRefs={circularRefs}
71+
context={context}
72+
/>
73+
));
74+
}
75+
76+
return null;
7477
})()}
7578
</div>
7679
);
@@ -85,18 +88,25 @@ export function OpenAPISchemaProperties(props: {
8588
circularRefs?: CircularRefsIds;
8689
context: OpenAPIClientContext;
8790
}) {
88-
const { id, properties, circularRefs, context } = props;
91+
const {
92+
id,
93+
properties,
94+
circularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
95+
context,
96+
} = props;
8997

9098
return (
9199
<div id={id} className="openapi-schema-properties">
92-
{properties.map((property, index) => (
93-
<OpenAPISchemaProperty
94-
key={index}
95-
circularRefs={circularRefs}
96-
property={property}
97-
context={context}
98-
/>
99-
))}
100+
{properties.map((property, index) => {
101+
return (
102+
<OpenAPISchemaProperty
103+
key={index}
104+
circularRefs={circularRefs}
105+
property={property}
106+
context={context}
107+
/>
108+
);
109+
})}
100110
</div>
101111
);
102112
}
@@ -107,20 +117,36 @@ export function OpenAPISchemaProperties(props: {
107117
export function OpenAPIRootSchema(props: {
108118
schema: OpenAPIV3.SchemaObject;
109119
context: OpenAPIClientContext;
120+
circularRefs?: CircularRefsIds;
110121
}) {
111-
const { schema, context } = props;
122+
const {
123+
schema,
124+
context,
125+
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
126+
} = props;
112127

128+
const id = useId();
113129
const properties = getSchemaProperties(schema);
114130

115131
if (properties?.length) {
116-
return <OpenAPISchemaProperties properties={properties} context={context} />;
132+
const circularRefs = new Map(parentCircularRefs);
133+
circularRefs.set(schema, id);
134+
135+
return (
136+
<OpenAPISchemaProperties
137+
properties={properties}
138+
circularRefs={circularRefs}
139+
context={context}
140+
/>
141+
);
117142
}
118143

119144
return (
120145
<OpenAPISchemaProperty
121146
className="openapi-schema-root"
122147
property={{ schema }}
123148
context={context}
149+
circularRefs={parentCircularRefs}
124150
/>
125151
);
126152
}
@@ -136,6 +162,7 @@ function OpenAPISchemaAlternative(props: {
136162
context: OpenAPIClientContext;
137163
}) {
138164
const { schema, circularRefs, context } = props;
165+
139166
const description = resolveDescription(schema);
140167
const properties = getSchemaProperties(schema);
141168

packages/react-openapi/src/OpenAPISpec.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ import { StaticSection } from './StaticSection';
88
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
99
import { parameterToProperty } from './utils';
1010

11-
/**
12-
* Client component to render the spec for the request and response.
13-
*
14-
* We use a client component as rendering recursive JSON schema in the server is expensive
15-
* (the entire schema is rendered at once, while the client component only renders the visible part)
16-
*/
1711
export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAPIClientContext }) {
1812
const { data, context } = props;
1913

@@ -25,13 +19,13 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
2519
return (
2620
<>
2721
{securities.length > 0 ? (
28-
<OpenAPISecurities securities={securities} context={context} />
22+
<OpenAPISecurities key="securities" securities={securities} context={context} />
2923
) : null}
3024

3125
{parameterGroups.map((group) => {
3226
return (
3327
<StaticSection
34-
key={group.key}
28+
key={`parameter-${group.key}`}
3529
className="openapi-parameters"
3630
header={group.label}
3731
>
@@ -44,10 +38,18 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
4438
})}
4539

4640
{operation.requestBody ? (
47-
<OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
41+
<OpenAPIRequestBody
42+
key="body"
43+
requestBody={operation.requestBody}
44+
context={context}
45+
/>
4846
) : null}
4947
{operation.responses ? (
50-
<OpenAPIResponses responses={operation.responses} context={context} />
48+
<OpenAPIResponses
49+
key="responses"
50+
responses={operation.responses}
51+
context={context}
52+
/>
5153
) : null}
5254
</>
5355
);

0 commit comments

Comments
 (0)