Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/strong-needles-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---

Add OpenAPI servers selection
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function getOpenAPIContext(args: {
chevronDown: <Icon icon="chevron-down" />,
chevronRight: <Icon icon="chevron-right" />,
plus: <Icon icon="plus" />,
copy: <Icon icon="copy" />,
check: <Icon icon="check" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
Expand Down
56 changes: 49 additions & 7 deletions packages/gitbook/src/components/DocumentView/OpenAPI/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -478,17 +478,22 @@
}

.openapi-path-server {
@apply text-tint hidden md:inline;
@apply text-tint inline;
}

.openapi-path .openapi-method {
@apply m-0 mt-0.5 items-center flex px-1;
.openapi-summary .openapi-path .openapi-method {
@apply m-0 items-center flex px-2 py-1 h-6;
}

.openapi-path-title {
@apply flex-1 relative font-normal text-left font-mono text-tint-strong/10;
@apply py-0.5 px-1 rounded hover:bg-tint transition-colors;
@apply flex-1 relative font-normal text-left overflow-x-auto font-mono text-tint-strong/10;
@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"] {
Expand Down Expand Up @@ -592,18 +597,30 @@ body:has(.openapi-select-popover) {
}

.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;
}

.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"] {
@apply outline-primary -outline-offset-1 outline outline-1;
@apply outline-primary -outline-offset-1 outline;
}

.openapi-select > button > span.react-aria-SelectValue {
@apply shrink truncate flex items-center;
}

.openapi-select > button > .react-aria-SelectValue [slot="description"] {
display: none;
}

.openapi-select > button .openapi-markdown {
@apply *:leading-none;
}
Expand All @@ -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;
}

.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-item .openapi-markdown {
@apply text-[0.813rem] *:truncate *:!p-0 *:!m-0 [&>*:not(:first-child)]:hidden;
Expand Down Expand Up @@ -966,3 +991,20 @@ body:has(.openapi-select-popover) {
.openapi-copy-button[data-disabled="true"] {
@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;
}
2 changes: 1 addition & 1 deletion packages/react-openapi/src/InteractiveSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function InteractiveSection(props: {
<OpenAPISelect
stateKey={stateKey}
items={tabs}
onSelectionChange={() => {
onChange={() => {
state.expand();
}}
icon={selectIcon}
Expand Down
12 changes: 1 addition & 11 deletions packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use client';
import clsx from 'classnames';
import type { MediaTypeRenderer } from './OpenAPICodeSample';
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
import { createStateKey } from './utils';
Expand Down Expand Up @@ -49,16 +48,7 @@ function MediaTypeSelector(props: {
}));

return (
<OpenAPISelect
className={clsx('openapi-select')}
items={renderers.map((renderer) => ({
key: renderer.mediaType,
label: renderer.mediaType,
}))}
icon={selectIcon}
stateKey={stateKey}
placement="bottom start"
>
<OpenAPISelect items={items} icon={selectIcon} stateKey={stateKey} placement="bottom start">
{items.map((item) => (
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
{item.label}
Expand Down
34 changes: 14 additions & 20 deletions packages/react-openapi/src/OpenAPICopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client';

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 { t } from './translate';

Expand Down Expand Up @@ -36,13 +37,7 @@ export function OpenAPICopyButton(
};

return (
<TooltipTrigger
isOpen={isOpen}
onOpenChange={setIsOpen}
isDisabled={!withTooltip}
closeDelay={200}
delay={200}
>
<OpenAPITooltip isDisabled={!withTooltip} isOpen={isOpen} onOpenChange={setIsOpen}>
<Button
type="button"
preventFocusOnPress
Expand All @@ -56,17 +51,16 @@ export function OpenAPICopyButton(
{children}
</Button>

<Tooltip
isOpen={isOpen}
onOpenChange={setIsOpen}
placement="top"
offset={4}
className="openapi-tooltip"
>
{copied
? t(context.translation, 'copied')
: label || t(context.translation, 'copy_to_clipboard')}
</Tooltip>
</TooltipTrigger>
<OpenAPITooltip.Content isOpen={isOpen} onOpenChange={setIsOpen}>
{copied ? (
<>
{context.icons.check}
{t(context.translation, 'copied')}
</>
) : (
label || t(context.translation, 'copy_to_clipboard')
)}
</OpenAPITooltip.Content>
</OpenAPITooltip>
);
}
2 changes: 1 addition & 1 deletion packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function DisclosureItem(props: {
<OpenAPISelect
icon={selectIcon}
stateKey={selectStateKey}
onSelectionChange={() => {
onChange={() => {
state.expand();
}}
items={group.tabs}
Expand Down
88 changes: 20 additions & 68 deletions packages/react-openapi/src/OpenAPIPath.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPIPathItem } from './OpenAPIPathItem';
import { OpenAPIPathMultipleServers } from './OpenAPIPathMultipleServers';
import { type OpenAPIUniversalContext, getOpenAPIClientContext } from './context';
import { formatPath } from './formatPath';
import type { OpenAPIOperationData } from './types';
import { getDefaultServerURL } from './util/server';

/**
* Display the path of an operation.
*/
export function OpenAPIPath(props: {
export type OpenAPIPathProps = {
data: OpenAPIOperationData;
context: OpenAPIUniversalContext;
/** Whether to show the server URL.
* @default true
*/
Expand All @@ -18,73 +16,27 @@ export function OpenAPIPath(props: {
* @default true
*/
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) {
// Matches placeholders like {id}, {userId}, etc.
const regex = /\{\s*(\w+)\s*\}|:\w+/g;

const parts: (string | React.JSX.Element)[] = [];
let lastIndex = 0;
export function OpenAPIPath(props: OpenAPIPathProps & { context: OpenAPIUniversalContext }) {
const { data, withServer = true, context } = props;
const { path } = data;
const clientContext = getOpenAPIClientContext(context);

//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));
if (withServer && data.servers.length > 1) {
return <OpenAPIPathMultipleServers {...props} context={clientContext} />;
}

const formattedPath = parts.map((part, index) => {
if (typeof part === 'string') {
return <span key={`part-${index}`}>{part}</span>;
}
return part;
});
const formattedPath = formatPath(path);
const defaultServer = getDefaultServerURL(data.servers);

return formattedPath;
return (
<OpenAPIPathItem {...props} value={`${defaultServer}${path}`} context={clientContext}>
{withServer ? <span className="openapi-path-server">{defaultServer}</span> : null}
{formattedPath}
</OpenAPIPathItem>
);
}
51 changes: 51 additions & 0 deletions packages/react-openapi/src/OpenAPIPathItem.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading