@gitbook/react-openapi 1.0.0 → 1.0.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/InteractiveSection.d.ts +0 -2
  3. package/dist/InteractiveSection.jsx +8 -44
  4. package/dist/OpenAPICodeSample.jsx +9 -11
  5. package/dist/OpenAPIRequestBody.d.ts +2 -2
  6. package/dist/OpenAPIRequestBody.jsx +5 -2
  7. package/dist/OpenAPIResponse.jsx +4 -14
  8. package/dist/OpenAPIResponseExample.jsx +5 -5
  9. package/dist/OpenAPIResponses.jsx +2 -3
  10. package/dist/OpenAPISchema.jsx +15 -25
  11. package/dist/OpenAPISpec.jsx +4 -4
  12. package/dist/OpenAPITabs.d.ts +1 -0
  13. package/dist/OpenAPITabs.jsx +52 -16
  14. package/dist/generateSchemaExample.js +2 -3
  15. package/dist/resolveOpenAPIOperation.js +4 -4
  16. package/dist/tsconfig.build.tsbuildinfo +1 -1
  17. package/dist/useSyncedTabsGlobalState.d.ts +1 -0
  18. package/dist/useSyncedTabsGlobalState.js +16 -0
  19. package/dist/utils.d.ts +0 -1
  20. package/dist/utils.js +0 -6
  21. package/package.json +3 -2
  22. package/src/InteractiveSection.tsx +4 -41
  23. package/src/OpenAPICodeSample.tsx +9 -11
  24. package/src/OpenAPIRequestBody.tsx +7 -3
  25. package/src/OpenAPIResponse.tsx +4 -18
  26. package/src/OpenAPIResponseExample.tsx +5 -5
  27. package/src/OpenAPIResponses.tsx +2 -7
  28. package/src/OpenAPISchema.tsx +16 -27
  29. package/src/OpenAPISpec.tsx +4 -7
  30. package/src/OpenAPITabs.tsx +63 -23
  31. package/src/generateSchemaExample.ts +2 -3
  32. package/src/resolveOpenAPIOperation.ts +8 -7
  33. package/src/useSyncedTabsGlobalState.ts +23 -0
  34. package/src/utils.ts +0 -8
  35. package/dist/fetchOpenAPIOperation.d.ts +0 -27
  36. package/dist/fetchOpenAPIOperation.js +0 -195
  37. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1 @@
1
+ export declare function useSyncedTabsGlobalState<T>(): readonly [Map<string, T>, (updater: (tabs: Map<string, T>) => Map<string, T>) => void];
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { create } from 'zustand';
3
+ var useSyncedTabsStore = create()(function (set) { return ({
4
+ tabs: new Map(),
5
+ setTabs: function (updater) {
6
+ return set(function (state) { return ({
7
+ tabs: updater(new Map(state.tabs)), // Ensure a new Map is created for reactivity
8
+ }); });
9
+ },
10
+ }); });
11
+ // Selector for better performance - only re-renders when tabs change
12
+ export function useSyncedTabsGlobalState() {
13
+ var tabs = useSyncedTabsStore(function (state) { return state.tabs; });
14
+ var setTabs = useSyncedTabsStore(function (state) { return state.setTabs; });
15
+ return [tabs, setTabs];
16
+ }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
2
- export declare function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T;
3
2
  export declare function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject;
4
3
  export declare function createStateKey(key: string, scope?: string): string;
5
4
  /**
package/dist/utils.js CHANGED
@@ -1,9 +1,3 @@
1
- export function noReference(input) {
2
- if (checkIsReference(input)) {
3
- throw new Error('Reference found');
4
- }
5
- return input;
6
- }
7
1
  export function checkIsReference(input) {
8
2
  return typeof input === 'object' && !!input && '$ref' in input;
9
3
  }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "default": "./dist/index.js"
9
9
  }
10
10
  },
11
- "version": "1.0.0",
11
+ "version": "1.0.1",
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
14
  "@gitbook/openapi-parser": "workspace:*",
@@ -18,7 +18,8 @@
18
18
  "flatted": "^3.2.9",
19
19
  "react-aria-components": "^1.6.0",
20
20
  "react-aria": "^3.37.0",
21
- "usehooks-ts": "^3.1.0"
21
+ "usehooks-ts": "^3.1.0",
22
+ "zustand": "^5.0.3"
22
23
  },
23
24
  "devDependencies": {
24
25
  "bun-types": "^1.1.20",
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import clsx from 'clsx';
4
- import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
4
+ import { useRef, useState } from 'react';
5
5
  import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
6
6
  import { useDisclosureState } from 'react-stately';
7
7
 
@@ -11,30 +11,6 @@ interface InteractiveSectionTab {
11
11
  body: React.ReactNode;
12
12
  }
13
13
 
14
- let globalState: Record<string, string> = {};
15
- const listeners = new Set<() => void>();
16
-
17
- function useSyncedTabsGlobalState() {
18
- const subscribe = useCallback((callback: () => void) => {
19
- listeners.add(callback);
20
- return () => listeners.delete(callback);
21
- }, []);
22
-
23
- const getSnapshot = useCallback(() => globalState, []);
24
-
25
- const setSyncedTabs = useCallback(
26
- (updater: (tabs: Record<string, string>) => Record<string, string>) => {
27
- globalState = updater(globalState);
28
- listeners.forEach((listener) => listener());
29
- },
30
- [],
31
- );
32
-
33
- const tabs = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
34
-
35
- return [tabs, setSyncedTabs] as const;
36
- }
37
-
38
14
  /**
39
15
  * To optimize rendering, most of the components are server-components,
40
16
  * and the interactiveness is mainly handled by a few key components like this one.
@@ -59,8 +35,6 @@ export function InteractiveSection(props: {
59
35
  children?: React.ReactNode;
60
36
  /** Children to display within the container */
61
37
  overlay?: React.ReactNode;
62
- /** An optional key referencing a value in global state */
63
- stateKey?: string;
64
38
  }) {
65
39
  const {
66
40
  id,
@@ -73,16 +47,11 @@ export function InteractiveSection(props: {
73
47
  children,
74
48
  overlay,
75
49
  toggleIcon = '▶',
76
- stateKey,
77
50
  } = props;
78
- const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState();
79
- const tabFromState =
80
- stateKey && stateKey in syncedTabs
81
- ? tabs.find((tab) => tab.key === syncedTabs[stateKey])
82
- : undefined;
83
- const [selectedTabKey, setSelectedTab] = useState(tabFromState?.key ?? defaultTab);
51
+
52
+ const [selectedTabKey, setSelectedTab] = useState(defaultTab);
84
53
  const selectedTab: InteractiveSectionTab | undefined =
85
- tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
54
+ tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
86
55
 
87
56
  const state = useDisclosureState({
88
57
  defaultExpanded: defaultOpened,
@@ -153,12 +122,6 @@ export function InteractiveSection(props: {
153
122
  value={selectedTab?.key ?? ''}
154
123
  onChange={(event) => {
155
124
  setSelectedTab(event.target.value);
156
- if (stateKey) {
157
- setSyncedTabs((state) => ({
158
- ...state,
159
- [stateKey]: event.target.value,
160
- }));
161
- }
162
125
  state.expand();
163
126
  }}
164
127
  >
@@ -3,9 +3,10 @@ import { generateMediaTypeExample, generateSchemaExample } from './generateSchem
3
3
  import { InteractiveSection } from './InteractiveSection';
4
4
  import { getServersURL } from './OpenAPIServerURL';
5
5
  import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
6
- import { noReference } from './utils';
6
+ import { createStateKey } from './utils';
7
7
  import { stringifyOpenAPI } from './stringifyOpenAPI';
8
8
  import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
9
+ import { checkIsReference } from './utils';
9
10
 
10
11
  /**
11
12
  * Display code samples to execute the operation.
@@ -20,24 +21,19 @@ export function OpenAPICodeSample(props: {
20
21
  const searchParams = new URLSearchParams();
21
22
  const headersObject: { [k: string]: string } = {};
22
23
 
23
- data.operation.parameters?.forEach((rawParam) => {
24
- const param = noReference(rawParam);
24
+ data.operation.parameters?.forEach((param) => {
25
25
  if (!param) {
26
26
  return;
27
27
  }
28
28
 
29
29
  if (param.in === 'header' && param.required) {
30
- const example = param.schema
31
- ? generateSchemaExample(noReference(param.schema))
32
- : undefined;
30
+ const example = param.schema ? generateSchemaExample(param.schema) : undefined;
33
31
  if (example !== undefined && param.name) {
34
32
  headersObject[param.name] =
35
33
  typeof example !== 'string' ? stringifyOpenAPI(example) : example;
36
34
  }
37
35
  } else if (param.in === 'query' && param.required) {
38
- const example = param.schema
39
- ? generateSchemaExample(noReference(param.schema))
40
- : undefined;
36
+ const example = param.schema ? generateSchemaExample(param.schema) : undefined;
41
37
  if (example !== undefined && param.name) {
42
38
  searchParams.append(
43
39
  param.name,
@@ -47,7 +43,9 @@ export function OpenAPICodeSample(props: {
47
43
  }
48
44
  });
49
45
 
50
- const requestBody = noReference(data.operation.requestBody);
46
+ const requestBody = !checkIsReference(data.operation.requestBody)
47
+ ? data.operation.requestBody
48
+ : undefined;
51
49
  const requestBodyContentEntries = requestBody?.content
52
50
  ? Object.entries(requestBody.content)
53
51
  : undefined;
@@ -115,7 +113,7 @@ export function OpenAPICodeSample(props: {
115
113
  }
116
114
 
117
115
  return (
118
- <OpenAPITabs items={samples}>
116
+ <OpenAPITabs stateKey={createStateKey('codesample')} items={samples}>
119
117
  <InteractiveSection header={<OpenAPITabsList />} className="openapi-codesample">
120
118
  <OpenAPITabsPanels />
121
119
  </InteractiveSection>
@@ -1,18 +1,22 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
2
  import { OpenAPIRootSchema } from './OpenAPISchema';
3
- import { noReference } from './utils';
4
3
  import type { OpenAPIClientContext } from './types';
5
4
  import { InteractiveSection } from './InteractiveSection';
5
+ import { checkIsReference } from './utils';
6
6
 
7
7
  /**
8
8
  * Display an interactive request body.
9
9
  */
10
10
  export function OpenAPIRequestBody(props: {
11
- requestBody: OpenAPIV3.RequestBodyObject;
11
+ requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
12
12
  context: OpenAPIClientContext;
13
13
  }) {
14
14
  const { requestBody, context } = props;
15
15
 
16
+ if (checkIsReference(requestBody)) {
17
+ return null;
18
+ }
19
+
16
20
  return (
17
21
  <InteractiveSection
18
22
  header="Body"
@@ -24,7 +28,7 @@ export function OpenAPIRequestBody(props: {
24
28
  label: contentType,
25
29
  body: (
26
30
  <OpenAPIRootSchema
27
- schema={noReference(mediaTypeObject.schema) ?? {}}
31
+ schema={mediaTypeObject.schema ?? {}}
28
32
  context={context}
29
33
  />
30
34
  ),
@@ -1,6 +1,6 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
2
  import { OpenAPISchemaProperties } from './OpenAPISchema';
3
- import { checkIsReference, noReference, resolveDescription } from './utils';
3
+ import { resolveDescription } from './utils';
4
4
  import type { OpenAPIClientContext } from './types';
5
5
  import { OpenAPIDisclosure } from './OpenAPIDisclosure';
6
6
 
@@ -14,7 +14,7 @@ export function OpenAPIResponse(props: {
14
14
  }) {
15
15
  const { response, context, mediaType } = props;
16
16
  const headers = Object.entries(response.headers ?? {}).map(
17
- ([name, header]) => [name, noReference(header) ?? {}] as const,
17
+ ([name, header]) => [name, header ?? {}] as const,
18
18
  );
19
19
  const content = Object.entries(mediaType.schema ?? {});
20
20
 
@@ -31,7 +31,7 @@ export function OpenAPIResponse(props: {
31
31
  <OpenAPISchemaProperties
32
32
  properties={headers.map(([name, header]) => ({
33
33
  propertyName: name,
34
- schema: noReference(header.schema) ?? {},
34
+ schema: header.schema ?? {},
35
35
  required: header.required,
36
36
  }))}
37
37
  context={context}
@@ -43,7 +43,7 @@ export function OpenAPIResponse(props: {
43
43
  id={`response-${context.blockKey}`}
44
44
  properties={[
45
45
  {
46
- schema: handleUnresolvedReference(mediaType.schema) ?? {},
46
+ schema: mediaType.schema ?? {},
47
47
  },
48
48
  ]}
49
49
  context={context}
@@ -52,17 +52,3 @@ export function OpenAPIResponse(props: {
52
52
  </div>
53
53
  );
54
54
  }
55
-
56
- function handleUnresolvedReference(
57
- input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
58
- ): OpenAPIV3.SchemaObject {
59
- const isReference = checkIsReference(input);
60
-
61
- if (isReference || input === undefined) {
62
- // If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it.
63
- // Instead we render `any`
64
- return {};
65
- }
66
-
67
- return input;
68
- }
@@ -1,7 +1,7 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
2
  import { generateSchemaExample } from './generateSchemaExample';
3
3
  import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
4
- import { checkIsReference, noReference, resolveDescription } from './utils';
4
+ import { checkIsReference, createStateKey, resolveDescription } from './utils';
5
5
  import { stringifyOpenAPI } from './stringifyOpenAPI';
6
6
  import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
7
7
  import { InteractiveSection } from './InteractiveSection';
@@ -40,7 +40,7 @@ export function OpenAPIResponseExample(props: {
40
40
 
41
41
  const examples = responses
42
42
  .map(([key, value]) => {
43
- const responseObject = noReference(value);
43
+ const responseObject = value;
44
44
  const mediaTypeObject = (() => {
45
45
  if (!responseObject.content) {
46
46
  return null;
@@ -68,7 +68,7 @@ export function OpenAPIResponseExample(props: {
68
68
  const key = Object.keys(examples)[0];
69
69
  if (key) {
70
70
  // @TODO handle multiple examples
71
- const firstExample = noReference(examples[key]);
71
+ const firstExample = examples[key];
72
72
  if (firstExample) {
73
73
  return firstExample;
74
74
  }
@@ -79,7 +79,7 @@ export function OpenAPIResponseExample(props: {
79
79
  return { value: example };
80
80
  }
81
81
 
82
- const schema = noReference(mediaTypeObject.schema);
82
+ const schema = mediaTypeObject.schema;
83
83
  if (!schema) {
84
84
  return null;
85
85
  }
@@ -115,7 +115,7 @@ export function OpenAPIResponseExample(props: {
115
115
  }
116
116
 
117
117
  return (
118
- <OpenAPITabs items={examples}>
118
+ <OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
119
119
  <InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
120
120
  <OpenAPITabsPanels />
121
121
  </InteractiveSection>
@@ -1,5 +1,4 @@
1
1
  import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
2
- import { createStateKey, resolveDescription } from './utils';
3
2
  import { OpenAPIResponse } from './OpenAPIResponse';
4
3
  import { OpenAPIClientContext } from './types';
5
4
  import { InteractiveSection } from './InteractiveSection';
@@ -16,18 +15,14 @@ export function OpenAPIResponses(props: {
16
15
  const { responses, context } = props;
17
16
 
18
17
  return (
19
- <InteractiveSection
20
- stateKey={createStateKey('response', context.blockKey)}
21
- header="Responses"
22
- className="openapi-responses"
23
- >
18
+ <InteractiveSection header="Responses" className="openapi-responses">
24
19
  <OpenAPIDisclosureGroup
25
20
  allowsMultipleExpanded
26
21
  icon={context.icons.chevronRight}
27
22
  groups={Object.entries(responses).map(
28
23
  ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
29
24
  const content = Object.entries(response.content ?? {});
30
- const description = resolveDescription(response);
25
+ const description = response.description;
31
26
 
32
27
  return {
33
28
  id: statusCode,
@@ -5,7 +5,7 @@ import { useId } from 'react';
5
5
  import { InteractiveSection } from './InteractiveSection';
6
6
  import { Markdown } from './Markdown';
7
7
  import type { OpenAPIClientContext } from './types';
8
- import { checkIsReference, noReference, resolveDescription } from './utils';
8
+ import { checkIsReference, resolveDescription } from './utils';
9
9
  import { stringifyOpenAPI } from './stringifyOpenAPI';
10
10
  import { OpenAPISchemaName } from './OpenAPISchemaName';
11
11
  import { OpenAPIDisclosure } from './OpenAPIDisclosure';
@@ -51,15 +51,15 @@ export function OpenAPISchemaProperty(
51
51
  return (
52
52
  <InteractiveSection id={id} className={clsx('openapi-schema', className)}>
53
53
  <OpenAPISchemaPresentation {...props} />
54
- <OpenAPIDisclosure context={context}>
55
- {properties && properties.length > 0 ? (
54
+ {properties && properties.length > 0 ? (
55
+ <OpenAPIDisclosure context={context}>
56
56
  <OpenAPISchemaProperties
57
57
  properties={properties}
58
58
  circularRefs={circularRefs}
59
59
  context={context}
60
60
  />
61
- ) : null}
62
- </OpenAPIDisclosure>
61
+ </OpenAPIDisclosure>
62
+ ) : null}
63
63
  </InteractiveSection>
64
64
  );
65
65
  }
@@ -70,7 +70,7 @@ export function OpenAPISchemaProperty(
70
70
  <OpenAPISchemaPresentation {...props} />
71
71
  {alternatives[0].map((alternative, index) => (
72
72
  <OpenAPISchemaAlternative
73
- key={index}
73
+ key={`alternative-${index}`}
74
74
  schema={alternative}
75
75
  circularRefs={circularRefs}
76
76
  context={context}
@@ -278,9 +278,9 @@ export function OpenAPISchemaPresentation(props: OpenAPISchemaPropertyEntry) {
278
278
  function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISchemaPropertyEntry[] {
279
279
  if (schema.allOf) {
280
280
  return schema.allOf.reduce((acc, subSchema) => {
281
- const properties = getSchemaProperties(noReference(subSchema)) ?? [
281
+ const properties = getSchemaProperties(subSchema) ?? [
282
282
  {
283
- schema: noReference(subSchema),
283
+ schema: subSchema,
284
284
  },
285
285
  ];
286
286
  return [...acc, ...properties];
@@ -289,7 +289,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
289
289
 
290
290
  // check array AND schema.items as this is sometimes null despite what the type indicates
291
291
  if (schema.type === 'array' && !!schema.items) {
292
- const items = noReference(schema.items);
292
+ const items = schema.items;
293
293
  const itemProperties = getSchemaProperties(items);
294
294
  if (itemProperties) {
295
295
  return itemProperties;
@@ -307,12 +307,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
307
307
  const result: OpenAPISchemaPropertyEntry[] = [];
308
308
 
309
309
  if (schema.properties) {
310
- Object.entries(schema.properties).forEach(([propertyName, rawPropertySchema]) => {
311
- const isReference = checkIsReference(rawPropertySchema);
312
- const propertySchema: OpenAPIV3.SchemaObject = isReference
313
- ? { propertyName }
314
- : rawPropertySchema;
315
-
310
+ Object.entries(schema.properties).forEach(([propertyName, propertySchema]) => {
316
311
  result.push({
317
312
  propertyName,
318
313
  required: Array.isArray(schema.required)
@@ -324,7 +319,7 @@ function getSchemaProperties(schema: OpenAPIV3.SchemaObject): null | OpenAPISche
324
319
  }
325
320
 
326
321
  if (schema.additionalProperties) {
327
- const additionalProperties = noReference(schema.additionalProperties);
322
+ const additionalProperties = schema.additionalProperties;
328
323
 
329
324
  result.push({
330
325
  propertyName: 'Other properties',
@@ -348,17 +343,11 @@ export function getSchemaAlternatives(
348
343
  const downAncestors = new Set(ancestors).add(schema);
349
344
 
350
345
  if (schema.anyOf) {
351
- return [
352
- flattenAlternatives('anyOf', schema.anyOf.map(noReference), downAncestors),
353
- noReference(schema.discriminator),
354
- ];
346
+ return [flattenAlternatives('anyOf', schema.anyOf, downAncestors), schema.discriminator];
355
347
  }
356
348
 
357
349
  if (schema.oneOf) {
358
- return [
359
- flattenAlternatives('oneOf', schema.oneOf.map(noReference), downAncestors),
360
- noReference(schema.discriminator),
361
- ];
350
+ return [flattenAlternatives('oneOf', schema.oneOf, downAncestors), schema.discriminator];
362
351
  }
363
352
 
364
353
  if (schema.allOf) {
@@ -396,8 +385,8 @@ export function getSchemaTitle(
396
385
 
397
386
  // Try using the discriminator
398
387
  if (discriminator?.propertyName && schema.properties) {
399
- const discriminatorProperty = noReference(schema.properties[discriminator.propertyName]);
400
- if (discriminatorProperty) {
388
+ const discriminatorProperty = schema.properties[discriminator.propertyName];
389
+ if (discriminatorProperty && !checkIsReference(discriminatorProperty)) {
401
390
  if (discriminatorProperty.enum) {
402
391
  return discriminatorProperty.enum.map((value) => value.toString()).join(' | ');
403
392
  }
@@ -411,7 +400,7 @@ export function getSchemaTitle(
411
400
  type = 'enum';
412
401
  // check array AND schema.items as this is sometimes null despite what the type indicates
413
402
  } else if (schema.type === 'array' && !!schema.items) {
414
- type = `${getSchemaTitle(noReference(schema.items))}[]`;
403
+ type = `${getSchemaTitle(schema.items)}[]`;
415
404
  } else if (Array.isArray(schema.type)) {
416
405
  type = schema.type.join(' | ');
417
406
  } else if (schema.type || schema.properties) {
@@ -8,7 +8,7 @@ import { OpenAPIResponses } from './OpenAPIResponses';
8
8
  import { OpenAPISchemaProperties } from './OpenAPISchema';
9
9
  import { OpenAPISecurities } from './OpenAPISecurities';
10
10
  import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
11
- import { noReference, resolveDescription } from './utils';
11
+ import { resolveDescription } from './utils';
12
12
 
13
13
  /**
14
14
  * Client component to render the spec for the request and response.
@@ -49,7 +49,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
49
49
  example: parameter.example,
50
50
  // Deprecated can be defined at the parameter level
51
51
  deprecated: parameter.deprecated,
52
- ...(noReference(parameter.schema) ?? {}),
52
+ ...(parameter.schema ?? {}),
53
53
  },
54
54
  required: parameter.required,
55
55
  };
@@ -61,13 +61,10 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
61
61
  })}
62
62
 
63
63
  {operation.requestBody ? (
64
- <OpenAPIRequestBody
65
- requestBody={noReference(operation.requestBody)}
66
- context={context}
67
- />
64
+ <OpenAPIRequestBody requestBody={operation.requestBody} context={context} />
68
65
  ) : null}
69
66
  {operation.responses ? (
70
- <OpenAPIResponses responses={noReference(operation.responses)} context={context} />
67
+ <OpenAPIResponses responses={operation.responses} context={context} />
71
68
  ) : null}
72
69
  </>
73
70
  );
@@ -1,8 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useMemo, useState } from 'react';
3
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
4
4
  import { Key, Tab, TabList, TabPanel, Tabs, TabsProps } from 'react-aria-components';
5
5
  import { Markdown } from './Markdown';
6
+ import { useSyncedTabsGlobalState } from './useSyncedTabsGlobalState';
7
+ import { useIntersectionObserver } from 'usehooks-ts';
6
8
 
7
9
  export type Tab = {
8
10
  key: Key;
@@ -13,8 +15,7 @@ export type Tab = {
13
15
 
14
16
  type OpenAPITabsContextData = {
15
17
  items: Tab[];
16
- selectedKey: Key;
17
- setSelectedKey: (key: Key) => void;
18
+ selectedTab: Tab;
18
19
  };
19
20
 
20
21
  const OpenAPITabsContext = createContext<OpenAPITabsContextData | null>(null);
@@ -30,25 +31,66 @@ function useOpenAPITabsContext() {
30
31
  /**
31
32
  * The OpenAPI Tabs wrapper component.
32
33
  */
33
- export function OpenAPITabs(props: React.PropsWithChildren<TabsProps & { items: Tab[] }>) {
34
- const { children, items } = props;
35
-
36
- const [selectedKey, setSelectedKey] = useState(() => {
37
- const firstItem = items[0];
38
- if (!firstItem) {
39
- throw new Error('OpenAPITabs: at least one tab is required');
34
+ export function OpenAPITabs(
35
+ props: React.PropsWithChildren<TabsProps & { items: Tab[]; stateKey?: string }>,
36
+ ) {
37
+ const { children, items, stateKey } = props;
38
+ const isVisible = stateKey
39
+ ? useIntersectionObserver({
40
+ threshold: 0.1,
41
+ rootMargin: '200px',
42
+ })
43
+ : true;
44
+ const defaultTab = items[0] as Tab;
45
+ const [syncedTabs, setSyncedTabs] = useSyncedTabsGlobalState<Tab>();
46
+ const [selectedTabKey, setSelectedTabKey] = useState(() => {
47
+ if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
48
+ const tabFromState = syncedTabs.get(stateKey);
49
+ return tabFromState?.key ?? items[0]?.key;
40
50
  }
41
- return firstItem.key;
51
+ return items[0]?.key;
42
52
  });
53
+ const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
54
+
55
+ const handleSelectionChange = (key: Key) => {
56
+ setSelectedTabKey(key);
57
+ if (stateKey) {
58
+ const tab = items.find((item) => item.key === key);
59
+
60
+ if (!tab) {
61
+ return;
62
+ }
63
+
64
+ setSyncedTabs((state) => {
65
+ const newState = new Map(state);
66
+ newState.set(stateKey, tab);
67
+ return newState;
68
+ });
69
+ }
70
+ };
43
71
 
44
- const contextValue = { items, selectedKey, setSelectedKey };
72
+ useEffect(() => {
73
+ if (isVisible && stateKey && syncedTabs && syncedTabs.has(stateKey)) {
74
+ const tabFromState = syncedTabs.get(stateKey);
75
+
76
+ if (!items.some((item) => item.key === tabFromState?.key)) {
77
+ return;
78
+ }
79
+
80
+ if (tabFromState && tabFromState?.key !== selectedTab?.key) {
81
+ setSelectedTab(tabFromState);
82
+ }
83
+ }
84
+ }, [isVisible, stateKey, syncedTabs, selectedTabKey]);
85
+
86
+ const contextValue = useMemo(() => ({ items, selectedTab }), [items, selectedTab]);
45
87
 
46
88
  return (
47
89
  <OpenAPITabsContext.Provider value={contextValue}>
48
90
  <Tabs
49
91
  className="openapi-tabs"
50
- onSelectionChange={setSelectedKey}
51
- selectedKey={selectedKey}
92
+ onSelectionChange={handleSelectionChange}
93
+ selectedKey={selectedTab?.key}
52
94
  >
53
95
  {children}
54
96
  </Tabs>
@@ -90,23 +132,21 @@ export function OpenAPITabsList() {
90
132
  * It renders the content of the selected tab.
91
133
  */
92
134
  export function OpenAPITabsPanels() {
93
- const { selectedKey, items } = useOpenAPITabsContext();
94
-
95
- const tab = useMemo(() => items.find((tab) => tab.key === selectedKey), [items, selectedKey]);
135
+ const { selectedTab } = useOpenAPITabsContext();
96
136
 
97
- if (!tab) {
137
+ if (!selectedTab) {
98
138
  return null;
99
139
  }
100
140
 
101
141
  return (
102
142
  <TabPanel
103
- key={`TabPanel-${tab.key}`}
104
- id={tab.key.toString()}
143
+ key={`TabPanel-${selectedTab.key}`}
144
+ id={selectedTab.key.toString()}
105
145
  className="openapi-tabs-panel"
106
146
  >
107
- {tab.body}
108
- {tab.description ? (
109
- <Markdown source={tab.description} className="openapi-tabs-footer" />
147
+ {selectedTab.body}
148
+ {selectedTab.description ? (
149
+ <Markdown source={selectedTab.description} className="openapi-tabs-footer" />
110
150
  ) : null}
111
151
  </TabPanel>
112
152
  );
@@ -1,5 +1,4 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
- import { noReference } from './utils';
3
2
  import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
4
3
 
5
4
  type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
@@ -50,13 +49,13 @@ export function generateMediaTypeExample(
50
49
  if (key) {
51
50
  const example = mediaType.examples[key];
52
51
  if (example) {
53
- return noReference(example).value;
52
+ return example.value;
54
53
  }
55
54
  }
56
55
  }
57
56
 
58
57
  if (mediaType.schema) {
59
- return generateSchemaExample(noReference(mediaType.schema), options);
58
+ return generateSchemaExample(mediaType.schema, options);
60
59
  }
61
60
 
62
61
  return undefined;