@gitbook/react-openapi 1.1.4 → 1.1.5

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/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "default": "./dist/index.js"
9
9
  }
10
10
  },
11
- "version": "1.1.4",
11
+ "version": "1.1.5",
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
14
  "@gitbook/openapi-parser": "workspace:*",
@@ -1,4 +1,6 @@
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
1
2
  import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
3
+ import { ScalarApiButton } from './ScalarApiButton';
2
4
  import { StaticSection } from './StaticSection';
3
5
  import { type CodeSampleInput, codeSampleGenerators } from './code-samples';
4
6
  import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
@@ -79,6 +81,7 @@ export function OpenAPICodeSample(props: {
79
81
  code: generator.generate(input),
80
82
  syntax: generator.syntax,
81
83
  }),
84
+ footer: <OpenAPICodeSampleFooter data={data} context={context} />,
82
85
  }));
83
86
 
84
87
  // Use custom samples if defined
@@ -105,6 +108,7 @@ export function OpenAPICodeSample(props: {
105
108
  code: sample.source,
106
109
  syntax: sample.lang,
107
110
  }),
111
+ footer: <OpenAPICodeSampleFooter data={data} context={context} />,
108
112
  }));
109
113
  }
110
114
  });
@@ -128,6 +132,30 @@ export function OpenAPICodeSample(props: {
128
132
  );
129
133
  }
130
134
 
135
+ function OpenAPICodeSampleFooter(props: {
136
+ data: OpenAPIOperationData;
137
+ context: OpenAPIContextProps;
138
+ }) {
139
+ const { data, context } = props;
140
+ const { method, path } = data;
141
+ const { specUrl } = context;
142
+ const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
143
+
144
+ if (hideTryItPanel) {
145
+ return null;
146
+ }
147
+
148
+ if (!validateHttpMethod(method)) {
149
+ return null;
150
+ }
151
+
152
+ return (
153
+ <div className="openapi-codesample-footer">
154
+ <ScalarApiButton method={method} path={path} specUrl={specUrl} />
155
+ </div>
156
+ );
157
+ }
158
+
131
159
  function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
132
160
  [key: string]: string;
133
161
  } {
@@ -169,3 +197,7 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
169
197
  }
170
198
  }
171
199
  }
200
+
201
+ function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {
202
+ return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
203
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
5
+
6
+ export function OpenAPICopyButton(
7
+ props: ButtonProps & {
8
+ value: string;
9
+ }
10
+ ) {
11
+ const { value } = props;
12
+ const { children, onPress, className } = props;
13
+ const [copied, setCopied] = useState(false);
14
+ const [isOpen, setIsOpen] = useState(false);
15
+
16
+ const handleCopy = () => {
17
+ if (!value) return;
18
+ navigator.clipboard.writeText(value).then(() => {
19
+ setIsOpen(true);
20
+ setCopied(true);
21
+
22
+ setTimeout(() => {
23
+ setCopied(false);
24
+ }, 2000);
25
+ });
26
+ };
27
+
28
+ return (
29
+ <TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen} closeDelay={200} delay={200}>
30
+ <Button
31
+ type="button"
32
+ preventFocusOnPress
33
+ onPress={(e) => {
34
+ handleCopy();
35
+ onPress?.(e);
36
+ }}
37
+ className={`openapi-copy-button ${className}`}
38
+ {...props}
39
+ >
40
+ {children}
41
+ </Button>
42
+
43
+ <Tooltip
44
+ isOpen={isOpen}
45
+ onOpenChange={setIsOpen}
46
+ placement="top"
47
+ offset={4}
48
+ className="openapi-tooltip"
49
+ >
50
+ {copied ? 'Copied' : 'Copy to clipboard'}{' '}
51
+ </Tooltip>
52
+ </TooltipTrigger>
53
+ );
54
+ }
@@ -35,6 +35,7 @@ export function OpenAPIOperation(props: {
35
35
  title: operation.summary,
36
36
  })
37
37
  : null}
38
+ <OpenAPIPath data={data} context={context} />
38
39
  {operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
39
40
  </div>
40
41
  <div className="openapi-columns">
@@ -49,7 +50,6 @@ export function OpenAPIOperation(props: {
49
50
  </div>
50
51
  ) : null}
51
52
  <OpenAPIOperationDescription operation={operation} context={context} />
52
- <OpenAPIPath data={data} context={context} />
53
53
  <OpenAPISpec data={data} context={clientContext} />
54
54
  </div>
55
55
  <div className="openapi-column-preview">
@@ -1,7 +1,6 @@
1
- import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
2
- import type React from 'react';
3
- import { ScalarApiButton } from './ScalarApiButton';
1
+ import { OpenAPICopyButton } from './OpenAPICopyButton';
4
2
  import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
3
+ import { getDefaultServerURL } from './util/server';
5
4
 
6
5
  /**
7
6
  * Display the path of an operation.
@@ -10,63 +9,62 @@ export function OpenAPIPath(props: {
10
9
  data: OpenAPIOperationData;
11
10
  context: OpenAPIContextProps;
12
11
  }) {
13
- const { data, context } = props;
14
- const { method, path } = data;
15
- const { specUrl } = context;
16
- const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
12
+ const { data } = props;
13
+ const { method, path, operation } = data;
14
+
15
+ const server = getDefaultServerURL(data.servers);
16
+ const formattedPath = formatPath(path);
17
17
 
18
18
  return (
19
19
  <div className="openapi-path">
20
20
  <div className={`openapi-method openapi-method-${method}`}>{method}</div>
21
- <div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
22
- <p>{formatPath(path)}</p>
23
- </div>
24
- {!hideTryItPanel && validateHttpMethod(method) && (
25
- <ScalarApiButton method={method} path={path} specUrl={specUrl} />
26
- )}
21
+
22
+ <OpenAPICopyButton
23
+ value={server + path}
24
+ className="openapi-path-title"
25
+ data-deprecated={operation.deprecated}
26
+ >
27
+ <span className="openapi-path-server">{server}</span>
28
+ {formattedPath}
29
+ </OpenAPICopyButton>
27
30
  </div>
28
31
  );
29
32
  }
30
33
 
31
- function validateHttpMethod(method: string): method is OpenAPIV3_1.HttpMethods {
32
- return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
33
- }
34
-
35
- // Format the path to highlight placeholders
34
+ /**
35
+ * Format the path by wrapping placeholders in <span> tags.
36
+ */
36
37
  function formatPath(path: string) {
37
38
  // Matches placeholders like {id}, {userId}, etc.
38
- const regex = /\{(\w+)\}/g;
39
+ const regex = /\{\s*(\w+)\s*\}|:\w+/g;
39
40
 
40
41
  const parts: (string | React.JSX.Element)[] = [];
41
42
  let lastIndex = 0;
42
43
 
43
- // Replace placeholders with <em> tags
44
- path.replace(regex, (match, key, offset) => {
45
- parts.push(path.slice(lastIndex, offset));
46
- parts.push(<em key={key}>{`{${key}}`}</em>);
44
+ //Wrap the variables in <span> tags and maintain either {variable} or :variable
45
+ path.replace(regex, (match, _, offset) => {
46
+ if (offset > lastIndex) {
47
+ parts.push(path.slice(lastIndex, offset));
48
+ }
49
+ parts.push(
50
+ <span key={offset} className="openapi-path-variable">
51
+ {match}
52
+ </span>
53
+ );
47
54
  lastIndex = offset + match.length;
48
55
  return match;
49
56
  });
50
57
 
51
- // Push remaining text after the last placeholder
52
- parts.push(path.slice(lastIndex));
53
-
54
- // Join parts with separators wrapped in <span>
55
- const formattedPath = parts.reduce(
56
- (acc, part, index) => {
57
- if (typeof part === 'string' && index > 0 && part === '/') {
58
- acc.push(
59
- <span className="openapi-path-separator" key={`sep-${index}`}>
60
- /
61
- </span>
62
- );
63
- }
58
+ if (lastIndex < path.length) {
59
+ parts.push(path.slice(lastIndex));
60
+ }
64
61
 
65
- acc.push(part);
66
- return acc;
67
- },
68
- [] as (string | React.JSX.Element)[]
69
- );
62
+ const formattedPath = parts.map((part, index) => {
63
+ if (typeof part === 'string') {
64
+ return <span key={index}>{part}</span>;
65
+ }
66
+ return part;
67
+ });
70
68
 
71
- return <span>{formattedPath}</span>;
69
+ return formattedPath;
72
70
  }
@@ -1,4 +1,5 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
+ import { Markdown } from './Markdown';
2
3
  import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
3
4
  import { StaticSection } from './StaticSection';
4
5
  import { generateSchemaExample } from './generateSchemaExample';
@@ -39,44 +40,40 @@ export function OpenAPIResponseExample(props: {
39
40
  return Number(a) - Number(b);
40
41
  });
41
42
 
42
- const tabs = responses
43
- .map(([key, responseObject]) => {
44
- const description = resolveDescription(responseObject);
45
-
46
- if (checkIsReference(responseObject)) {
47
- return {
48
- key: key,
49
- label: key,
50
- description,
51
- body: (
52
- <OpenAPIExample
53
- example={getExampleFromReference(responseObject)}
54
- context={context}
55
- syntax="json"
56
- />
57
- ),
58
- };
59
- }
60
-
61
- if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
62
- return {
63
- key: key,
64
- label: key,
65
- description,
66
- body: <OpenAPIEmptyResponseExample />,
67
- };
68
- }
43
+ const tabs = responses.map(([key, responseObject]) => {
44
+ const description = resolveDescription(responseObject);
69
45
 
46
+ if (checkIsReference(responseObject)) {
70
47
  return {
71
48
  key: key,
72
49
  label: key,
73
- description: resolveDescription(responseObject),
74
- body: <OpenAPIResponse context={context} content={responseObject.content} />,
50
+ body: (
51
+ <OpenAPIExample
52
+ example={getExampleFromReference(responseObject)}
53
+ context={context}
54
+ syntax="json"
55
+ />
56
+ ),
57
+ footer: description ? <Markdown source={description} /> : undefined,
75
58
  };
76
- })
77
- .filter((val): val is { key: string; label: string; body: any; description: string } =>
78
- Boolean(val)
79
- );
59
+ }
60
+
61
+ if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
62
+ return {
63
+ key: key,
64
+ label: key,
65
+ body: <OpenAPIEmptyResponseExample />,
66
+ footer: description ? <Markdown source={description} /> : undefined,
67
+ };
68
+ }
69
+
70
+ return {
71
+ key: key,
72
+ label: key,
73
+ body: <OpenAPIResponse context={context} content={responseObject.content} />,
74
+ footer: description ? <Markdown source={description} /> : undefined,
75
+ };
76
+ });
80
77
 
81
78
  if (tabs.length === 0) {
82
79
  return null;
@@ -3,14 +3,13 @@
3
3
  import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components';
5
5
  import { useEventCallback } from 'usehooks-ts';
6
- import { Markdown } from './Markdown';
7
6
  import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState';
8
7
 
9
8
  export type TabItem = {
10
9
  key: Key;
11
10
  label: string;
12
11
  body: React.ReactNode;
13
- description?: string;
12
+ footer?: React.ReactNode;
14
13
  };
15
14
 
16
15
  type OpenAPITabsContextData = {
@@ -140,9 +139,19 @@ export function OpenAPITabsPanels() {
140
139
  return (
141
140
  <TabPanel key={key} id={key} className="openapi-tabs-panel">
142
141
  {selectedTab.body}
143
- {selectedTab.description ? (
144
- <Markdown source={selectedTab.description} className="openapi-tabs-footer" />
142
+ {selectedTab.footer ? (
143
+ <OpenAPITabsPanelFooter>{selectedTab.footer}</OpenAPITabsPanelFooter>
145
144
  ) : null}
146
145
  </TabPanel>
147
146
  );
148
147
  }
148
+
149
+ /**
150
+ * The OpenAPI Tabs panel footer component.
151
+ * This component should be used as a child of the OpenAPITabs component.
152
+ */
153
+ function OpenAPITabsPanelFooter(props: { children: React.ReactNode }) {
154
+ const { children } = props;
155
+
156
+ return <div className="openapi-tabs-footer">{children}</div>;
157
+ }
@@ -27,14 +27,14 @@ export function ScalarApiButton(props: {
27
27
  setIsOpen(true);
28
28
  }}
29
29
  >
30
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
30
+ Test it
31
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 12" fill="currentColor">
31
32
  <path
32
33
  stroke="currentColor"
33
34
  strokeWidth="1.5"
34
35
  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"
35
36
  />
36
37
  </svg>
37
- Test it
38
38
  </button>
39
39
 
40
40
  {isOpen &&