Skip to content

Commit e987108

Browse files
committed
Fix OpenAPI spec rendering
1 parent ae78fc5 commit e987108

File tree

11 files changed

+110
-89
lines changed

11 files changed

+110
-89
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/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: 71 additions & 42 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,43 @@ 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}
52+
if (properties) {
53+
return (
54+
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
55+
<OpenAPISchemaProperties
56+
properties={properties}
5957
circularRefs={circularRefs}
6058
context={context}
6159
/>
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-
);
60+
</OpenAPIDisclosure>
61+
);
62+
}
63+
64+
const ancestors = new Set(circularRefs.keys());
65+
const alternatives = getSchemaAlternatives(schema, ancestors);
66+
67+
if (alternatives) {
68+
return alternatives.map((schema, index) => (
69+
<OpenAPISchemaAlternative
70+
key={index}
71+
schema={schema}
72+
circularRefs={circularRefs}
73+
context={context}
74+
/>
75+
));
76+
}
77+
78+
return null;
7479
})()}
7580
</div>
7681
);
@@ -85,18 +90,25 @@ export function OpenAPISchemaProperties(props: {
8590
circularRefs?: CircularRefsIds;
8691
context: OpenAPIClientContext;
8792
}) {
88-
const { id, properties, circularRefs, context } = props;
93+
const {
94+
id,
95+
properties,
96+
circularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
97+
context,
98+
} = props;
8999

90100
return (
91101
<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-
))}
102+
{properties.map((property, index) => {
103+
return (
104+
<OpenAPISchemaProperty
105+
key={index}
106+
circularRefs={circularRefs}
107+
property={property}
108+
context={context}
109+
/>
110+
);
111+
})}
100112
</div>
101113
);
102114
}
@@ -107,20 +119,36 @@ export function OpenAPISchemaProperties(props: {
107119
export function OpenAPIRootSchema(props: {
108120
schema: OpenAPIV3.SchemaObject;
109121
context: OpenAPIClientContext;
122+
circularRefs?: CircularRefsIds;
110123
}) {
111-
const { schema, context } = props;
124+
const {
125+
schema,
126+
context,
127+
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
128+
} = props;
112129

130+
const id = useId();
113131
const properties = getSchemaProperties(schema);
114132

115133
if (properties?.length) {
116-
return <OpenAPISchemaProperties properties={properties} context={context} />;
134+
const circularRefs = new Map(parentCircularRefs);
135+
circularRefs.set(schema, id);
136+
137+
return (
138+
<OpenAPISchemaProperties
139+
properties={properties}
140+
circularRefs={circularRefs}
141+
context={context}
142+
/>
143+
);
117144
}
118145

119146
return (
120147
<OpenAPISchemaProperty
121148
className="openapi-schema-root"
122149
property={{ schema }}
123150
context={context}
151+
circularRefs={parentCircularRefs}
124152
/>
125153
);
126154
}
@@ -136,6 +164,7 @@ function OpenAPISchemaAlternative(props: {
136164
context: OpenAPIClientContext;
137165
}) {
138166
const { schema, circularRefs, context } = props;
167+
139168
const description = resolveDescription(schema);
140169
const properties = getSchemaProperties(schema);
141170

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)