mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Add OpenAPI servers selection (#3726)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add OpenAPI servers selection
|
||||||
@@ -39,6 +39,8 @@ export function getOpenAPIContext(args: {
|
|||||||
chevronDown: <Icon icon="chevron-down" />,
|
chevronDown: <Icon icon="chevron-down" />,
|
||||||
chevronRight: <Icon icon="chevron-right" />,
|
chevronRight: <Icon icon="chevron-right" />,
|
||||||
plus: <Icon icon="plus" />,
|
plus: <Icon icon="plus" />,
|
||||||
|
copy: <Icon icon="copy" />,
|
||||||
|
check: <Icon icon="check" />,
|
||||||
},
|
},
|
||||||
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
|
||||||
renderDocument: (documentProps) => (
|
renderDocument: (documentProps) => (
|
||||||
|
|||||||
@@ -478,17 +478,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path-server {
|
.openapi-path-server {
|
||||||
@apply text-tint hidden md:inline;
|
@apply text-tint inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path .openapi-method {
|
.openapi-summary .openapi-path .openapi-method {
|
||||||
@apply m-0 mt-0.5 items-center flex px-1;
|
@apply m-0 items-center flex px-2 py-1 h-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path-title {
|
.openapi-path-title {
|
||||||
@apply flex-1 relative font-normal text-left font-mono text-tint-strong/10;
|
@apply flex-1 relative font-normal text-left overflow-x-auto font-mono text-tint-strong/10;
|
||||||
@apply py-0.5 px-1 rounded hover:bg-tint transition-colors;
|
|
||||||
@apply whitespace-nowrap md:whitespace-normal;
|
@apply whitespace-nowrap md:whitespace-normal;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-title-row{
|
||||||
|
@apply flex flex-row items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path-title[data-deprecated="true"] {
|
.openapi-path-title[data-deprecated="true"] {
|
||||||
@@ -592,18 +597,30 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button {
|
.openapi-select > button {
|
||||||
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 p-1.5 border border-tint-subtle text-tint-strong rounded leading-none;
|
||||||
@apply hover:bg-tint-hover transition-all;
|
@apply hover:bg-tint-hover transition-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-select:not(.openapi-select-unstyled) > button {
|
||||||
|
@apply border border-tint-subtle bg-tint text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-select-unstyled > button {
|
||||||
|
@apply p-1;
|
||||||
|
}
|
||||||
|
|
||||||
.openapi-select > button[data-focused="true"] {
|
.openapi-select > button[data-focused="true"] {
|
||||||
@apply outline-primary -outline-offset-1 outline outline-1;
|
@apply outline-primary -outline-offset-1 outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button > span.react-aria-SelectValue {
|
.openapi-select > button > span.react-aria-SelectValue {
|
||||||
@apply shrink truncate flex items-center;
|
@apply shrink truncate flex items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-select > button > .react-aria-SelectValue [slot="description"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.openapi-select > button .openapi-markdown {
|
.openapi-select > button .openapi-markdown {
|
||||||
@apply *:leading-none;
|
@apply *:leading-none;
|
||||||
}
|
}
|
||||||
@@ -630,6 +647,14 @@ body:has(.openapi-select-popover) {
|
|||||||
@apply hover:bg-tint-hover hover:theme-gradient:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
@apply hover:bg-tint-hover hover:theme-gradient:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-select-item.openapi-select-item-column {
|
||||||
|
@apply flex flex-col gap-1 justify-start items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-select-item [slot="description"] {
|
||||||
|
@apply text-xs text-tint-subtle;
|
||||||
|
}
|
||||||
|
|
||||||
.openapi-select button .openapi-markdown,
|
.openapi-select button .openapi-markdown,
|
||||||
.openapi-select-item .openapi-markdown {
|
.openapi-select-item .openapi-markdown {
|
||||||
@apply text-[0.813rem] *:truncate *:!p-0 *:!m-0 [&>*:not(:first-child)]:hidden;
|
@apply text-[0.813rem] *:truncate *:!p-0 *:!m-0 [&>*:not(:first-child)]:hidden;
|
||||||
@@ -966,3 +991,20 @@ body:has(.openapi-select-popover) {
|
|||||||
.openapi-copy-button[data-disabled="true"] {
|
.openapi-copy-button[data-disabled="true"] {
|
||||||
@apply cursor-default;
|
@apply cursor-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-path-copy-button {
|
||||||
|
@apply p-1 flex rounded-md;
|
||||||
|
@apply hover:bg-tint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-copy-button-icon {
|
||||||
|
@apply size-6 flex opacity-0 transition-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path:hover .openapi-path-copy-button-icon {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-path-copy-button-icon svg {
|
||||||
|
@apply text-tint size-4;
|
||||||
|
}
|
||||||
@@ -116,7 +116,7 @@ export function InteractiveSection(props: {
|
|||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
stateKey={stateKey}
|
stateKey={stateKey}
|
||||||
items={tabs}
|
items={tabs}
|
||||||
onSelectionChange={() => {
|
onChange={() => {
|
||||||
state.expand();
|
state.expand();
|
||||||
}}
|
}}
|
||||||
icon={selectIcon}
|
icon={selectIcon}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import clsx from 'classnames';
|
|
||||||
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
||||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||||
import { createStateKey } from './utils';
|
import { createStateKey } from './utils';
|
||||||
@@ -49,16 +48,7 @@ function MediaTypeSelector(props: {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPISelect
|
<OpenAPISelect items={items} icon={selectIcon} stateKey={stateKey} placement="bottom start">
|
||||||
className={clsx('openapi-select')}
|
|
||||||
items={renderers.map((renderer) => ({
|
|
||||||
key: renderer.mediaType,
|
|
||||||
label: renderer.mediaType,
|
|
||||||
}))}
|
|
||||||
icon={selectIcon}
|
|
||||||
stateKey={stateKey}
|
|
||||||
placement="bottom start"
|
|
||||||
>
|
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components';
|
import { Button, type ButtonProps } from 'react-aria-components';
|
||||||
|
import { OpenAPITooltip } from './OpenAPITooltip';
|
||||||
import type { OpenAPIClientContext } from './context';
|
import type { OpenAPIClientContext } from './context';
|
||||||
import { t } from './translate';
|
import { t } from './translate';
|
||||||
|
|
||||||
@@ -36,13 +37,7 @@ export function OpenAPICopyButton(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<OpenAPITooltip isDisabled={!withTooltip} isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
isOpen={isOpen}
|
|
||||||
onOpenChange={setIsOpen}
|
|
||||||
isDisabled={!withTooltip}
|
|
||||||
closeDelay={200}
|
|
||||||
delay={200}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
preventFocusOnPress
|
preventFocusOnPress
|
||||||
@@ -56,17 +51,16 @@ export function OpenAPICopyButton(
|
|||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Tooltip
|
<OpenAPITooltip.Content isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||||
isOpen={isOpen}
|
{copied ? (
|
||||||
onOpenChange={setIsOpen}
|
<>
|
||||||
placement="top"
|
{context.icons.check}
|
||||||
offset={4}
|
{t(context.translation, 'copied')}
|
||||||
className="openapi-tooltip"
|
</>
|
||||||
>
|
) : (
|
||||||
{copied
|
label || t(context.translation, 'copy_to_clipboard')
|
||||||
? t(context.translation, 'copied')
|
)}
|
||||||
: label || t(context.translation, 'copy_to_clipboard')}
|
</OpenAPITooltip.Content>
|
||||||
</Tooltip>
|
</OpenAPITooltip>
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ function DisclosureItem(props: {
|
|||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
icon={selectIcon}
|
icon={selectIcon}
|
||||||
stateKey={selectStateKey}
|
stateKey={selectStateKey}
|
||||||
onSelectionChange={() => {
|
onChange={() => {
|
||||||
state.expand();
|
state.expand();
|
||||||
}}
|
}}
|
||||||
items={group.tabs}
|
items={group.tabs}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
import { OpenAPIPathItem } from './OpenAPIPathItem';
|
||||||
|
import { OpenAPIPathMultipleServers } from './OpenAPIPathMultipleServers';
|
||||||
import { type OpenAPIUniversalContext, getOpenAPIClientContext } from './context';
|
import { type OpenAPIUniversalContext, getOpenAPIClientContext } from './context';
|
||||||
|
import { formatPath } from './formatPath';
|
||||||
import type { OpenAPIOperationData } from './types';
|
import type { OpenAPIOperationData } from './types';
|
||||||
import { getDefaultServerURL } from './util/server';
|
import { getDefaultServerURL } from './util/server';
|
||||||
|
|
||||||
/**
|
export type OpenAPIPathProps = {
|
||||||
* Display the path of an operation.
|
|
||||||
*/
|
|
||||||
export function OpenAPIPath(props: {
|
|
||||||
data: OpenAPIOperationData;
|
data: OpenAPIOperationData;
|
||||||
context: OpenAPIUniversalContext;
|
|
||||||
/** Whether to show the server URL.
|
/** Whether to show the server URL.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
@@ -18,73 +16,27 @@ export function OpenAPIPath(props: {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
canCopy?: boolean;
|
canCopy?: boolean;
|
||||||
}) {
|
};
|
||||||
const { data, context, withServer = true, canCopy = true } = props;
|
|
||||||
const { method, path, operation } = data;
|
|
||||||
|
|
||||||
const server = getDefaultServerURL(data.servers);
|
|
||||||
const formattedPath = formatPath(path);
|
|
||||||
|
|
||||||
const element = (() => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{withServer ? <span className="openapi-path-server">{server}</span> : null}
|
|
||||||
{formattedPath}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="openapi-path">
|
|
||||||
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
|
||||||
|
|
||||||
<OpenAPICopyButton
|
|
||||||
value={`${withServer ? server : ''}${path}`}
|
|
||||||
className="openapi-path-title"
|
|
||||||
data-deprecated={operation.deprecated}
|
|
||||||
isDisabled={!canCopy}
|
|
||||||
context={getOpenAPIClientContext(context)}
|
|
||||||
>
|
|
||||||
{element}
|
|
||||||
</OpenAPICopyButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the path by wrapping placeholders in <span> tags.
|
* Display the path of an operation.
|
||||||
*/
|
*/
|
||||||
function formatPath(path: string) {
|
export function OpenAPIPath(props: OpenAPIPathProps & { context: OpenAPIUniversalContext }) {
|
||||||
// Matches placeholders like {id}, {userId}, etc.
|
const { data, withServer = true, context } = props;
|
||||||
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
const { path } = data;
|
||||||
|
const clientContext = getOpenAPIClientContext(context);
|
||||||
|
|
||||||
const parts: (string | React.JSX.Element)[] = [];
|
if (withServer && data.servers.length > 1) {
|
||||||
let lastIndex = 0;
|
return <OpenAPIPathMultipleServers {...props} context={clientContext} />;
|
||||||
|
|
||||||
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
|
||||||
path.replace(regex, (match, _, offset) => {
|
|
||||||
if (offset > lastIndex) {
|
|
||||||
parts.push(path.slice(lastIndex, offset));
|
|
||||||
}
|
|
||||||
parts.push(
|
|
||||||
<span key={`offset-${offset}`} className="openapi-path-variable">
|
|
||||||
{match}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
lastIndex = offset + match.length;
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastIndex < path.length) {
|
|
||||||
parts.push(path.slice(lastIndex));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedPath = parts.map((part, index) => {
|
const formattedPath = formatPath(path);
|
||||||
if (typeof part === 'string') {
|
const defaultServer = getDefaultServerURL(data.servers);
|
||||||
return <span key={`part-${index}`}>{part}</span>;
|
|
||||||
}
|
|
||||||
return part;
|
|
||||||
});
|
|
||||||
|
|
||||||
return formattedPath;
|
return (
|
||||||
|
<OpenAPIPathItem {...props} value={`${defaultServer}${path}`} context={clientContext}>
|
||||||
|
{withServer ? <span className="openapi-path-server">{defaultServer}</span> : null}
|
||||||
|
{formattedPath}
|
||||||
|
</OpenAPIPathItem>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||||
|
import type { OpenAPIPathProps } from './OpenAPIPath';
|
||||||
|
import type { OpenAPIClientContext } from './context';
|
||||||
|
|
||||||
|
export function OpenAPIPathItem(
|
||||||
|
props: OpenAPIPathProps & {
|
||||||
|
value?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
copyType?: 'button' | 'children';
|
||||||
|
context: OpenAPIClientContext;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { value, canCopy = true, context, children, data, copyType = 'children' } = props;
|
||||||
|
const { operation, method } = data;
|
||||||
|
|
||||||
|
const title = <span className="openapi-path-title">{children}</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="openapi-path">
|
||||||
|
<div className={`openapi-method openapi-method-${method}`}>{method}</div>
|
||||||
|
{canCopy && value ? (
|
||||||
|
copyType === 'children' ? (
|
||||||
|
<OpenAPICopyButton
|
||||||
|
value={value}
|
||||||
|
data-deprecated={operation.deprecated}
|
||||||
|
isDisabled={!canCopy}
|
||||||
|
context={context}
|
||||||
|
className="openapi-path-copy-button"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</OpenAPICopyButton>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{title}
|
||||||
|
<OpenAPICopyButton
|
||||||
|
value={value}
|
||||||
|
data-deprecated={operation.deprecated}
|
||||||
|
isDisabled={!canCopy}
|
||||||
|
context={context}
|
||||||
|
className="openapi-path-copy-button openapi-path-copy-button-icon"
|
||||||
|
>
|
||||||
|
{context.icons.copy}
|
||||||
|
</OpenAPICopyButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
import { Text } from 'react-aria-components';
|
||||||
|
import type { OpenAPIPathProps } from './OpenAPIPath';
|
||||||
|
import { OpenAPIPathItem } from './OpenAPIPathItem';
|
||||||
|
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||||
|
import { OpenAPITooltip } from './OpenAPITooltip';
|
||||||
|
import type { OpenAPIClientContext } from './context';
|
||||||
|
import { formatPath } from './formatPath';
|
||||||
|
import type { OpenAPIServerWithCustomProperties } from './types';
|
||||||
|
import { getDefaultServerURL } from './util/server';
|
||||||
|
import { createStateKey } from './utils';
|
||||||
|
|
||||||
|
export const serversStateKey = createStateKey('servers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the path of an operation.
|
||||||
|
*/
|
||||||
|
export function OpenAPIPathMultipleServers(
|
||||||
|
props: OpenAPIPathProps & { context: OpenAPIClientContext }
|
||||||
|
) {
|
||||||
|
const { data, withServer = true, context } = props;
|
||||||
|
const { path, servers } = data;
|
||||||
|
|
||||||
|
const defaultServer = getDefaultServerURL(servers);
|
||||||
|
const { key, setKey } = useSelectState(serversStateKey, defaultServer);
|
||||||
|
const formattedPath = formatPath(path);
|
||||||
|
|
||||||
|
const items = servers
|
||||||
|
.filter(
|
||||||
|
(server): server is OpenAPIServerWithCustomProperties & { url: string } => !!server.url
|
||||||
|
)
|
||||||
|
.map((server) => ({
|
||||||
|
key: server.url,
|
||||||
|
label: server.url,
|
||||||
|
description: server.description,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OpenAPIPathItem
|
||||||
|
copyType="button"
|
||||||
|
{...props}
|
||||||
|
value={`${withServer ? key : ''}${path}`}
|
||||||
|
context={context}
|
||||||
|
>
|
||||||
|
{withServer ? (
|
||||||
|
<OpenAPITooltip>
|
||||||
|
<OpenAPISelect
|
||||||
|
className="openapi-select openapi-select-unstyled"
|
||||||
|
items={items}
|
||||||
|
stateKey={serversStateKey}
|
||||||
|
placement="bottom start"
|
||||||
|
icon={context.icons.chevronDown}
|
||||||
|
defaultValue={defaultServer}
|
||||||
|
onChange={setKey}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<OpenAPISelectItem
|
||||||
|
textValue={item.label}
|
||||||
|
key={item.key}
|
||||||
|
id={item.key}
|
||||||
|
value={item}
|
||||||
|
className="openapi-select-item-column"
|
||||||
|
>
|
||||||
|
<Text slot="label">{item.label}</Text>
|
||||||
|
{item.description ? (
|
||||||
|
<Text slot="description">{item.description}</Text>
|
||||||
|
) : null}
|
||||||
|
</OpenAPISelectItem>
|
||||||
|
))}
|
||||||
|
</OpenAPISelect>
|
||||||
|
<OpenAPITooltip.Content>Click to select a server</OpenAPITooltip.Content>
|
||||||
|
</OpenAPITooltip>
|
||||||
|
) : null}
|
||||||
|
{formattedPath}
|
||||||
|
</OpenAPIPathItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectPro
|
|||||||
/**
|
/**
|
||||||
* Icon to display in the select button.
|
* Icon to display in the select button.
|
||||||
*/
|
*/
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectState(stateKey = 'select-state', initialKey: Key = 'default') {
|
export function useSelectState(stateKey = 'select-state', initialKey: Key = 'default') {
|
||||||
@@ -42,18 +42,10 @@ export function useSelectState(stateKey = 'select-state', initialKey: Key = 'def
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectProps<T>) {
|
export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectProps<T>) {
|
||||||
const {
|
const { icon, items, children, className, placement, stateKey, value, onChange, defaultValue } =
|
||||||
icon = '▼',
|
props;
|
||||||
items,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
placement,
|
|
||||||
stateKey,
|
|
||||||
selectedKey,
|
|
||||||
onSelectionChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const state = useSelectState(stateKey, items[0]?.key);
|
const state = useSelectState(stateKey, defaultValue ?? items[0]?.key);
|
||||||
|
|
||||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
const selected = items.find((item) => item.key === state.key) || items[0];
|
||||||
|
|
||||||
@@ -61,16 +53,16 @@ export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectP
|
|||||||
<Select
|
<Select
|
||||||
aria-label="OpenAPI Select"
|
aria-label="OpenAPI Select"
|
||||||
{...props}
|
{...props}
|
||||||
value={selectedKey || selected?.key}
|
value={value ?? selected?.key}
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
onSelectionChange?.(key);
|
onChange?.(key);
|
||||||
state.setKey(key);
|
state.setKey(key);
|
||||||
}}
|
}}
|
||||||
className={clsx('openapi-select', className)}
|
className={clsx('openapi-select', className)}
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
{icon}
|
{icon !== null ? icon || '▼' : null}
|
||||||
</Button>
|
</Button>
|
||||||
<Popover placement={placement} className="openapi-select-popover">
|
<Popover placement={placement} className="openapi-select-popover">
|
||||||
<ListBox className="openapi-select-listbox" items={items}>
|
<ListBox className="openapi-select-listbox" items={items}>
|
||||||
@@ -86,10 +78,14 @@ export function OpenAPISelectItem(props: ListBoxItemProps) {
|
|||||||
<ListBoxItem
|
<ListBoxItem
|
||||||
{...props}
|
{...props}
|
||||||
className={({ isFocused, isSelected }) =>
|
className={({ isFocused, isSelected }) =>
|
||||||
clsx('openapi-select-item', {
|
clsx(
|
||||||
'openapi-select-item-focused': isFocused,
|
'openapi-select-item',
|
||||||
'openapi-select-item-selected': isSelected,
|
{
|
||||||
})
|
'openapi-select-item-focused': isFocused,
|
||||||
|
'openapi-select-item-selected': isSelected,
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { TooltipTriggerProps } from 'react-aria';
|
||||||
|
import { Tooltip, type TooltipProps, TooltipTrigger } from 'react-aria-components';
|
||||||
|
|
||||||
|
export function OpenAPITooltip(
|
||||||
|
props: TooltipTriggerProps & {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { children, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<TooltipTrigger {...rest} closeDelay={200} delay={200}>
|
||||||
|
{children}
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenAPITooltipContent(props: TooltipProps) {
|
||||||
|
const { children, placement = 'top', offset = 4, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
{...rest}
|
||||||
|
placement={placement}
|
||||||
|
offset={offset}
|
||||||
|
className={classNames('openapi-tooltip', className)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAPITooltip.Content = OpenAPITooltipContent;
|
||||||
@@ -13,6 +13,8 @@ export interface OpenAPIClientContext {
|
|||||||
chevronDown: React.ReactNode;
|
chevronDown: React.ReactNode;
|
||||||
chevronRight: React.ReactNode;
|
chevronRight: React.ReactNode;
|
||||||
plus: React.ReactNode;
|
plus: React.ReactNode;
|
||||||
|
copy: React.ReactNode;
|
||||||
|
check: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Format the path by wrapping placeholders in <span> tags.
|
||||||
|
*/
|
||||||
|
export function formatPath(path: string) {
|
||||||
|
// Matches placeholders like {id}, {userId}, etc.
|
||||||
|
const regex = /\{\s*(\w+)\s*\}|:\w+/g;
|
||||||
|
|
||||||
|
const parts: (string | React.JSX.Element)[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
//Wrap the variables in <span> tags and maintain either {variable} or :variable
|
||||||
|
path.replace(regex, (match, _, offset) => {
|
||||||
|
if (offset > lastIndex) {
|
||||||
|
parts.push(path.slice(lastIndex, offset));
|
||||||
|
}
|
||||||
|
parts.push(
|
||||||
|
<span key={`offset-${offset}`} className="openapi-path-variable">
|
||||||
|
{match}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
lastIndex = offset + match.length;
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lastIndex < path.length) {
|
||||||
|
parts.push(path.slice(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedPath = parts.map((part, index) => {
|
||||||
|
if (typeof part === 'string') {
|
||||||
|
return <span key={`part-${index}`}>{part}</span>;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedPath;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export type OpenAPIServerVariableWithCustomProperties = OpenAPIV3.ServerVariable
|
|||||||
* OpenAPI ServerObject type extended to provide x-gitbook prefill custom properties at the variable level.
|
* OpenAPI ServerObject type extended to provide x-gitbook prefill custom properties at the variable level.
|
||||||
*/
|
*/
|
||||||
export type OpenAPIServerWithCustomProperties = Omit<OpenAPIV3.ServerObject, 'variables'> & {
|
export type OpenAPIServerWithCustomProperties = Omit<OpenAPIV3.ServerObject, 'variables'> & {
|
||||||
|
name?: string;
|
||||||
variables?: {
|
variables?: {
|
||||||
[variable: string]: OpenAPIServerVariableWithCustomProperties;
|
[variable: string]: OpenAPIServerVariableWithCustomProperties;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user