@gitbook/react-openapi 0.5.0 → 0.7.0

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @gitbook/react-openapi
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cf3045a: Add Python support in Code Samples
8
+ - 4247361: Add required query parameters to the code sample
9
+ - aa8c49e: Display pattern if available in parmas in OpenAPI block
10
+ - e914903: Synchronize response and response example tabs
11
+ - 4cbcc5b: Rollback of scalar modal while fixing perf issue
12
+
13
+ ### Patch Changes
14
+
15
+ - 51fa3ab: Adds content-visibility css property to OpenAPI Operation for better render performance
16
+ - f89b31c: Upgrade the scalar api client package
17
+ - 094e9cd: bump: scalar from 1.0.5 to 1.0.7
18
+ - 237b703: Fix crash when `example` is undefined for a response
19
+ - 51955da: Adds tabs to Response Example section e.g. for status code examples
20
+ - a679e72: Render mandatory headers in code sample
21
+ - c079c3c: Update Scalar client to latest version
22
+
23
+ ## 0.6.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 709f1a1: Update Scalar to the latest version, with faster performances and an improved experience
28
+
29
+ ### Patch Changes
30
+
31
+ - ede2335: Fix x-codeSamples: false not working at the single operation level
32
+ - 0426312: Fix tabs being empty for code samples when they are updated dynamically
33
+
3
34
  ## 0.5.0
4
35
 
5
36
  ### Minor Changes
@@ -1,4 +1,9 @@
1
1
  import React from 'react';
2
+ interface InteractiveSectionTab {
3
+ key: string;
4
+ label: string;
5
+ body: React.ReactNode;
6
+ }
2
7
  /**
3
8
  * To optimize rendering, most of the components are server-components,
4
9
  * and the interactiveness is mainly handled by a few key components like this one.
@@ -15,11 +20,7 @@ export declare function InteractiveSection(props: {
15
20
  toggleOpenIcon?: React.ReactNode;
16
21
  toggleCloseIcon?: React.ReactNode;
17
22
  /** Tabs of content to display */
18
- tabs?: Array<{
19
- key: string;
20
- label: string;
21
- body: React.ReactNode;
22
- }>;
23
+ tabs?: Array<InteractiveSectionTab>;
23
24
  /** Default tab to have opened */
24
25
  defaultTab?: string;
25
26
  /** Content of the header */
@@ -28,4 +29,7 @@ export declare function InteractiveSection(props: {
28
29
  children?: React.ReactNode;
29
30
  /** Children to display within the container */
30
31
  overlay?: React.ReactNode;
32
+ /** An optional key referencing a value in global state */
33
+ stateKey?: string;
31
34
  }): React.JSX.Element;
35
+ export {};
@@ -1,15 +1,24 @@
1
1
  'use client';
2
2
  import classNames from 'classnames';
3
3
  import React from 'react';
4
+ import { atom, useRecoilState } from 'recoil';
5
+ const syncedTabsAtom = atom({
6
+ key: 'syncedTabState',
7
+ default: {},
8
+ });
4
9
  /**
5
10
  * To optimize rendering, most of the components are server-components,
6
11
  * and the interactiveness is mainly handled by a few key components like this one.
7
12
  */
8
13
  export function InteractiveSection(props) {
9
- const { id, className, toggeable = false, defaultOpened = true, tabs = [], defaultTab = tabs[0]?.key, header, children, overlay, toggleOpenIcon = '▶', toggleCloseIcon = '▼', } = props;
14
+ const { id, className, toggeable = false, defaultOpened = true, tabs = [], defaultTab = tabs[0]?.key, header, children, overlay, toggleOpenIcon = '▶', toggleCloseIcon = '▼', stateKey, } = props;
15
+ const [syncedTabs, setSyncedTabs] = useRecoilState(syncedTabsAtom);
16
+ const tabFromState = stateKey && stateKey in syncedTabs
17
+ ? tabs.find((tab) => tab.key === syncedTabs[stateKey])
18
+ : undefined;
10
19
  const [opened, setOpened] = React.useState(defaultOpened);
11
- const [selectedTab, setSelectedTab] = React.useState(defaultTab);
12
- const tabBody = tabs.find((tab) => tab.key === selectedTab)?.body;
20
+ const [selectedTabKey, setSelectedTab] = React.useState(tabFromState?.key ?? defaultTab);
21
+ const selectedTab = tabFromState ?? tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
13
22
  return (React.createElement("div", { id: id, className: classNames('openapi-section', toggeable ? 'openapi-section-toggeable' : null, className, toggeable ? `${className}-${opened ? 'opened' : 'closed'}` : null) },
14
23
  React.createElement("div", { onClick: () => {
15
24
  if (toggeable) {
@@ -20,13 +29,19 @@ export function InteractiveSection(props) {
20
29
  React.createElement("div", { className: classNames('openapi-section-header-controls', `${className}-header-controls`), onClick: (event) => {
21
30
  event.stopPropagation();
22
31
  } },
23
- tabs.length ? (React.createElement("select", { className: classNames('openapi-section-select', 'openapi-select', `${className}-tabs-select`), value: selectedTab, onChange: (event) => {
32
+ tabs.length ? (React.createElement("select", { className: classNames('openapi-section-select', 'openapi-select', `${className}-tabs-select`), value: selectedTab.key, onChange: (event) => {
24
33
  setSelectedTab(event.target.value);
34
+ if (stateKey) {
35
+ setSyncedTabs((state) => ({
36
+ ...state,
37
+ [stateKey]: event.target.value,
38
+ }));
39
+ }
25
40
  setOpened(true);
26
41
  } }, tabs.map((tab) => (React.createElement("option", { key: tab.key, value: tab.key }, tab.label))))) : null,
27
- (children || tabBody) && toggeable ? (React.createElement("button", { className: classNames('openapi-section-toggle', `${className}-toggle`), onClick: () => setOpened(!opened) }, opened ? toggleCloseIcon : toggleOpenIcon)) : null)),
28
- (!toggeable || opened) && (children || tabBody) ? (React.createElement("div", { className: classNames('openapi-section-body', `${className}-body`) },
42
+ (children || selectedTab?.body) && toggeable ? (React.createElement("button", { className: classNames('openapi-section-toggle', `${className}-toggle`), onClick: () => setOpened(!opened) }, opened ? toggleCloseIcon : toggleOpenIcon)) : null)),
43
+ (!toggeable || opened) && (children || selectedTab?.body) ? (React.createElement("div", { className: classNames('openapi-section-body', `${className}-body`) },
29
44
  children,
30
- tabBody)) : null,
45
+ selectedTab?.body)) : null,
31
46
  overlay));
32
47
  }
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { codeSampleGenerators } from './code-samples';
3
- import { toJSON } from './fetchOpenAPIOperation';
4
- import { generateMediaTypeExample } from './generateSchemaExample';
3
+ import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
5
4
  import { InteractiveSection } from './InteractiveSection';
6
5
  import { getServersURL } from './OpenAPIServerURL';
7
6
  import { ScalarApiButton } from './ScalarApiButton';
@@ -12,16 +11,44 @@ import { noReference } from './utils';
12
11
  */
13
12
  export function OpenAPICodeSample(props) {
14
13
  const { data, context } = props;
14
+ const searchParams = new URLSearchParams();
15
+ const headersObject = {};
16
+ data.operation.parameters?.forEach((rawParam) => {
17
+ const param = noReference(rawParam);
18
+ if (!param) {
19
+ return;
20
+ }
21
+ if (param.in === 'header' && param.required) {
22
+ const example = param.schema
23
+ ? generateSchemaExample(noReference(param.schema))
24
+ : undefined;
25
+ if (example !== undefined) {
26
+ headersObject[param.name] =
27
+ typeof example !== 'string' ? JSON.stringify(example) : example;
28
+ }
29
+ }
30
+ else if (param.in === 'query' && param.required) {
31
+ const example = param.schema
32
+ ? generateSchemaExample(noReference(param.schema))
33
+ : undefined;
34
+ if (example !== undefined) {
35
+ searchParams.append(param.name, String(Array.isArray(example) ? example[0] : example));
36
+ }
37
+ }
38
+ });
15
39
  const requestBody = noReference(data.operation.requestBody);
16
40
  const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
17
41
  const input = {
18
- url: getServersURL(data.servers) + data.path,
42
+ url: getServersURL(data.servers) +
43
+ data.path +
44
+ (searchParams.size ? `?${searchParams.toString()}` : ''),
19
45
  method: data.method,
20
46
  body: requestBodyContent
21
47
  ? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
22
48
  : undefined,
23
49
  headers: {
24
50
  ...getSecurityHeaders(data.securities),
51
+ ...headersObject,
25
52
  ...(requestBodyContent
26
53
  ? {
27
54
  'Content-Type': requestBodyContent[0],
@@ -38,23 +65,28 @@ export function OpenAPICodeSample(props) {
38
65
  let customCodeSamples = null;
39
66
  ['x-custom-examples', 'x-code-samples', 'x-codeSamples'].forEach((key) => {
40
67
  const customSamples = data.operation[key];
41
- if (customSamples) {
42
- customCodeSamples = customSamples.map((sample) => ({
68
+ if (customSamples && Array.isArray(customSamples)) {
69
+ customCodeSamples = customSamples
70
+ .filter((sample) => {
71
+ return (typeof sample.label === 'string' &&
72
+ typeof sample.source === 'string' &&
73
+ typeof sample.lang === 'string');
74
+ })
75
+ .map((sample) => ({
43
76
  key: `redocly-${sample.lang}`,
44
77
  label: sample.label,
45
78
  body: React.createElement(context.CodeBlock, { code: sample.source, syntax: sample.lang }),
46
79
  }));
47
80
  }
48
81
  });
49
- const samples = customCodeSamples ?? (data['x-codeSamples'] !== false ? autoCodeSamples : []);
82
+ // Code samples can be disabled at the top-level or at the operation level
83
+ // If code samples are defined at the operation level, it will override the top-level setting
84
+ const codeSamplesDisabled = data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
85
+ const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
50
86
  if (samples.length === 0) {
51
87
  return null;
52
88
  }
53
- async function fetchOperationData() {
54
- 'use server';
55
- return toJSON(data);
56
- }
57
- return (React.createElement(InteractiveSection, { header: "Request", className: "openapi-codesample", tabs: samples, overlay: data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (React.createElement(ScalarApiButton, { fetchOperationData: fetchOperationData })) }));
89
+ return (React.createElement(InteractiveSection, { header: "Request", className: "openapi-codesample", tabs: samples, overlay: data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (React.createElement(ScalarApiButton, null)) }));
58
90
  }
59
91
  function getSecurityHeaders(securities) {
60
92
  const security = securities[0];
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import classNames from 'classnames';
3
+ import { ApiClientModalProvider } from '@scalar/api-client-react';
3
4
  import { toJSON } from './fetchOpenAPIOperation';
4
5
  import { Markdown } from './Markdown';
5
6
  import { OpenAPICodeSample } from './OpenAPICodeSample';
6
7
  import { OpenAPIResponseExample } from './OpenAPIResponseExample';
7
8
  import { OpenAPIServerURL } from './OpenAPIServerURL';
8
9
  import { OpenAPISpec } from './OpenAPISpec';
9
- import { ScalarApiClient } from './ScalarApiButton';
10
10
  /**
11
11
  * Display an interactive OpenAPI operation.
12
12
  */
@@ -16,11 +16,12 @@ export function OpenAPIOperation(props) {
16
16
  const clientContext = {
17
17
  defaultInteractiveOpened: context.defaultInteractiveOpened,
18
18
  icons: context.icons,
19
+ blockKey: context.blockKey,
19
20
  };
20
- return (React.createElement(ScalarApiClient, null,
21
+ return (React.createElement(ApiClientModalProvider, { configuration: { spec: { url: context.specUrl } }, initialRequest: { path: data.path, method: data.method } },
21
22
  React.createElement("div", { className: classNames('openapi-operation', className) },
22
23
  React.createElement("div", { className: "openapi-intro" },
23
- React.createElement("h2", { className: "openapi-summary" }, operation.summary),
24
+ React.createElement("h2", { className: "openapi-summary", id: context.id }, operation.summary),
24
25
  operation.description ? (React.createElement(Markdown, { className: "openapi-description", source: operation.description })) : null,
25
26
  React.createElement("div", { className: "openapi-target" },
26
27
  React.createElement("span", { className: classNames('openapi-method', `openapi-method-${method.toLowerCase()}`) }, method.toUpperCase()),
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { InteractiveSection } from './InteractiveSection';
3
3
  import { generateSchemaExample } from './generateSchemaExample';
4
- import { noReference } from './utils';
4
+ import { createStateKey, noReference } from './utils';
5
5
  /**
6
6
  * Display an example of the response content.
7
7
  */
@@ -28,21 +28,27 @@ export function OpenAPIResponseExample(props) {
28
28
  }
29
29
  return Number(a) - Number(b);
30
30
  });
31
- // Take the first one
32
- const response = responses[0];
33
- if (!response) {
34
- return null;
35
- }
36
- const responseObject = noReference(response[1]);
37
- const schema = noReference((responseObject.content?.['application/json'] ??
38
- responseObject.content?.[Object.keys(responseObject.content)[0]])?.schema);
39
- if (!schema) {
40
- return null;
41
- }
42
- const example = generateSchemaExample(schema);
43
- if (example === undefined) {
31
+ const examples = responses
32
+ .map((response) => {
33
+ const responseObject = noReference(response[1]);
34
+ const schema = noReference((responseObject.content?.['application/json'] ??
35
+ responseObject.content?.[Object.keys(responseObject.content)[0]])?.schema);
36
+ if (!schema) {
37
+ return null;
38
+ }
39
+ const example = generateSchemaExample(schema);
40
+ if (example === undefined) {
41
+ return null;
42
+ }
43
+ return {
44
+ key: `${response[0]}`,
45
+ label: `${response[0]}`,
46
+ body: (React.createElement(context.CodeBlock, { code: typeof example === 'string' ? example : JSON.stringify(example, null, 2), syntax: "json" })),
47
+ };
48
+ })
49
+ .filter((val) => Boolean(val));
50
+ if (examples.length === 0) {
44
51
  return null;
45
52
  }
46
- return (React.createElement(InteractiveSection, { header: "Response", className: "openapi-response-example" },
47
- React.createElement(context.CodeBlock, { code: typeof example === 'string' ? example : JSON.stringify(example, null, 2), syntax: "json" })));
53
+ return (React.createElement(InteractiveSection, { stateKey: createStateKey('response', context.blockKey), header: "Response", className: "openapi-response-example", tabs: examples }));
48
54
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import classNames from 'classnames';
3
- import { noReference } from './utils';
3
+ import { createStateKey, noReference } from './utils';
4
4
  import { OpenAPIResponse } from './OpenAPIResponse';
5
5
  import { InteractiveSection } from './InteractiveSection';
6
6
  /**
@@ -8,7 +8,7 @@ import { InteractiveSection } from './InteractiveSection';
8
8
  */
9
9
  export function OpenAPIResponses(props) {
10
10
  const { responses, context } = props;
11
- return (React.createElement(InteractiveSection, { header: "Response", className: classNames('openapi-responses'), tabs: Object.entries(responses).map(([statusCode, response]) => {
11
+ return (React.createElement(InteractiveSection, { stateKey: createStateKey('response', context.blockKey), header: "Response", className: classNames('openapi-responses'), tabs: Object.entries(responses).map(([statusCode, response]) => {
12
12
  return {
13
13
  key: statusCode,
14
14
  label: statusCode,
@@ -34,7 +34,10 @@ export function OpenAPISchemaProperty(props) {
34
34
  schema.description ? (React.createElement(Markdown, { source: schema.description, className: "openapi-schema-description" })) : null,
35
35
  shouldDisplayExample(schema) ? (React.createElement("span", { className: "openapi-schema-example" },
36
36
  "Example: ",
37
- React.createElement("code", null, JSON.stringify(schema.example)))) : null) }, (properties && properties.length > 0) ||
37
+ React.createElement("code", null, JSON.stringify(schema.example)))) : null,
38
+ schema.pattern ? (React.createElement("div", { className: "openapi-schema-pattern" },
39
+ "Pattern: ",
40
+ React.createElement("code", null, schema.pattern))) : null) }, (properties && properties.length > 0) ||
38
41
  (schema.enum && schema.enum.length > 0) ||
39
42
  parentCircularRef ? (React.createElement(React.Fragment, null,
40
43
  properties?.length ? (React.createElement(OpenAPISchemaProperties, { properties: properties, circularRefs: circularRefs, context: context })) : null,
@@ -1,14 +1,5 @@
1
1
  import React from 'react';
2
- import { OpenAPIOperationData } from './fetchOpenAPIOperation';
3
2
  /**
4
3
  * Button which launches the Scalar API Client
5
4
  */
6
- export declare function ScalarApiButton(props: {
7
- fetchOperationData: () => Promise<OpenAPIOperationData>;
8
- }): React.JSX.Element;
9
- /**
10
- * Wrap the rendering with a context to open the scalar modal.
11
- */
12
- export declare function ScalarApiClient(props: {
13
- children: React.ReactNode;
14
- }): React.JSX.Element;
5
+ export declare function ScalarApiButton(): React.JSX.Element;
@@ -1,89 +1,14 @@
1
1
  'use client';
2
- import { getHarRequest, getParametersFromOperation, getRequestFromOperation, } from '@scalar/oas-utils';
2
+ import { useApiClientModal } from '@scalar/api-client-react';
3
3
  import React from 'react';
4
- import { fromJSON } from './fetchOpenAPIOperation';
5
- const ApiClientReact = React.lazy(async () => {
6
- const mod = await import('@scalar/api-client-react');
7
- return { default: mod.ApiClientReact };
8
- });
9
- const ScalarContext = React.createContext(() => { });
10
4
  /**
11
5
  * Button which launches the Scalar API Client
12
6
  */
13
- export function ScalarApiButton(props) {
14
- const { fetchOperationData } = props;
15
- const open = React.useContext(ScalarContext);
7
+ export function ScalarApiButton() {
8
+ const client = useApiClientModal();
16
9
  return (React.createElement("div", { className: "scalar scalar-activate" },
17
- React.createElement("button", { className: "scalar-activate-button", onClick: () => {
18
- open(fetchOperationData);
19
- } },
10
+ React.createElement("button", { className: "scalar-activate-button", onClick: () => client?.open() },
20
11
  React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "10", height: "12", fill: "none" },
21
12
  React.createElement("path", { stroke: "currentColor", strokeWidth: "1.5", d: "M1 10.05V1.43c0-.2.2-.31.37-.22l7.26 4.08c.17.1.17.33.01.43l-7.26 4.54a.25.25 0 0 1-.38-.21Z" })),
22
13
  "Test it")));
23
14
  }
24
- /**
25
- * Wrap the rendering with a context to open the scalar modal.
26
- */
27
- export function ScalarApiClient(props) {
28
- const { children } = props;
29
- const [active, setActive] = React.useState(null);
30
- const proxy = '/~scalar/proxy';
31
- const open = React.useCallback(async (fetchOperationData) => {
32
- setActive({ operationData: null });
33
- const operationData = fromJSON(await fetchOperationData());
34
- setActive({ operationData });
35
- }, []);
36
- const onClose = React.useCallback(() => {
37
- setActive(null);
38
- }, []);
39
- const request = React.useMemo(() => {
40
- const operationData = active?.operationData;
41
- if (!operationData) {
42
- return null;
43
- }
44
- const operationId = operationData.operation.operationId ?? operationData.method + operationData.path;
45
- const operation = {
46
- ...operationData,
47
- httpVerb: operationData.method,
48
- pathParameters: operationData.operation.parameters,
49
- };
50
- const variables = getParametersFromOperation(operation, 'path', false);
51
- const request = getHarRequest({
52
- url: operationData.path,
53
- }, getRequestFromOperation({
54
- ...operation,
55
- information: {
56
- requestBody: operationData.operation.requestBody,
57
- },
58
- }, { requiredOnly: false }));
59
- return {
60
- id: operationId,
61
- type: operationData.method,
62
- path: operationData.path,
63
- variables,
64
- cookies: request.cookies.map((cookie) => {
65
- return { ...cookie, enabled: true };
66
- }),
67
- query: request.queryString.map((queryString) => {
68
- const query = queryString;
69
- return { ...queryString, enabled: query.required ?? true };
70
- }),
71
- headers: request.headers.map((header) => {
72
- return { ...header, enabled: true };
73
- }),
74
- url: operationData.servers[0]?.url,
75
- body: request.postData?.text,
76
- };
77
- }, [active]);
78
- return (React.createElement(ScalarContext.Provider, { value: open },
79
- children,
80
- active ? (React.createElement("div", { className: "scalar" },
81
- React.createElement("div", { className: "scalar-container" },
82
- React.createElement("div", { className: "scalar-app" },
83
- React.createElement(React.Suspense, { fallback: React.createElement(ScalarLoading, null) },
84
- React.createElement(ApiClientReact, { close: onClose, proxy: proxy, isOpen: true, request: request }))),
85
- React.createElement("div", { onClick: () => onClose(), className: "scalar-app-exit" })))) : null));
86
- }
87
- function ScalarLoading() {
88
- return React.createElement("div", { className: "scalar-app-loading" }, "Loading...");
89
- }
@@ -40,6 +40,25 @@ export const codeSampleGenerators = [
40
40
  return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
41
41
  },
42
42
  },
43
+ {
44
+ id: 'python',
45
+ label: 'Python',
46
+ syntax: 'python',
47
+ generate: ({ method, url, headers, body }) => {
48
+ let code = 'import requests\n\n';
49
+ code += `response = requests.${method.toLowerCase()}(\n`;
50
+ code += indent(`"${url}",\n`, 4);
51
+ if (headers) {
52
+ code += indent(`headers=${JSON.stringify(headers)},\n`, 4);
53
+ }
54
+ if (body) {
55
+ code += indent(`json=${JSON.stringify(body)}\n`, 4);
56
+ }
57
+ code += ')\n';
58
+ code += `data = response.json()`;
59
+ return code;
60
+ },
61
+ },
43
62
  ];
44
63
  function indent(code, spaces) {
45
64
  const indent = ' '.repeat(spaces);
@@ -32,7 +32,7 @@ export interface OpenAPICustomSpecProperties {
32
32
  */
33
33
  export interface OpenAPICustomOperationProperties {
34
34
  'x-code-samples'?: OpenAPICustomCodeSample[];
35
- 'x-codeSamples'?: OpenAPICustomCodeSample[];
35
+ 'x-codeSamples'?: OpenAPICustomCodeSample[] | false;
36
36
  'x-custom-examples'?: OpenAPICustomCodeSample[];
37
37
  /**
38
38
  * If `true`, the "Try it" button will not be displayed.
@@ -70,7 +70,7 @@ export function generateSchemaExample(schema, options = {}, ancestors = new Set(
70
70
  }
71
71
  if (schema.properties) {
72
72
  const example = {};
73
- const props = onlyRequired ? schema.required ?? [] : Object.keys(schema.properties);
73
+ const props = onlyRequired ? (schema.required ?? []) : Object.keys(schema.properties);
74
74
  for (const key of props) {
75
75
  const property = noReference(schema.properties[key]);
76
76
  if (property && (onlyRequired || !property.deprecated)) {