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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function getOpenAPIContext(args: {
plus: <Icon icon="plus" />,
copy: <Icon icon="copy" />,
check: <Icon icon="check" />,
lock: <Icon icon="lock" />,
},
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
renderDocument: (documentProps) => (
Expand Down
47 changes: 34 additions & 13 deletions packages/gitbook/src/components/DocumentView/OpenAPI/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@
}

.openapi-securities-scopes ul {
@apply !my-0;
@apply !my-0 ml-4 pl-0;
}

.openapi-securities-url {
Expand Down Expand Up @@ -398,15 +398,13 @@
@apply text-left prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
}

.openapi-disclosure-group-trigger[aria-expanded="false"] {
.openapi-response-description.openapi-markdown {
@apply truncate;
@apply [&>*:not(:first-child)]:hidden *:truncate *:!p-0 *:!m-0;
}
.openapi-disclosure-group-trigger[aria-expanded="false"] .openapi-response-description.openapi-markdown {
@apply truncate;
@apply [&>*:not(:first-child)]:hidden *:truncate *:!p-0 *:!m-0;
}

.openapi-response-tab-content {
@apply basis-[60%]
}
.openapi-disclosure-group-trigger[aria-expanded="false"] .openapi-response-tab-content {
@apply basis-[60%];
}

.openapi-response-body {
Expand Down Expand Up @@ -528,7 +526,7 @@
.openapi-panel,
.openapi-codesample,
.openapi-response-examples {
@apply border shrink min-h-40 overflow-hidden rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
@apply border shrink min-h-40 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
}

.openapi-response-examples-panel {
Expand Down Expand Up @@ -851,7 +849,7 @@ body:has(.openapi-select-popover) {
.openapi-schema-alternatives .openapi-disclosure,
.openapi-schemas-disclosure .openapi-schema.openapi-disclosure
) {
@apply rounded-xl straight-corners:rounded-none;
@apply rounded-md circular-corners:rounded-xl straight-corners:rounded-none;
}

.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure {
Expand All @@ -866,10 +864,10 @@ body:has(.openapi-select-popover) {
@apply ring-1 shadow-sm;
}

.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) {
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure,.openapi-required-scopes):not(:first-child) {
@apply mt-2;
}
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) {
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure,.openapi-required-scopes):not(:last-child) {
@apply mb-2;
}

Expand Down Expand Up @@ -1015,4 +1013,27 @@ body:has(.openapi-select-popover) {

.openapi-path-copy-button-icon svg {
@apply text-tint size-4;
}

.openapi-required-scopes {
@apply border text-base rounded-md straight-corners:rounded-none circular-corners:rounded-md font-medium mx-0;
}

.openapi-required-scopes .openapi-required-scopes-header {
@apply flex items-center gap-3;
}

.openapi-required-scopes .openapi-required-scopes-header svg {
@apply size-3.5 text-tint-subtle rotate-none;
}

.openapi-required-scopes .openapi-disclosure-group-panel {
@apply px-3 pb-3;
}
.openapi-required-scopes .openapi-securities-scopes {
@apply ml-6 font-normal *:!text-[0.8125rem];
}

.openapi-required-scopes .openapi-required-scopes-description {
@apply text-xs !text-tint font-normal mb-2;
}
3 changes: 2 additions & 1 deletion packages/react-openapi/src/OpenAPICopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import clsx from 'classnames';
import { useState } from 'react';
import { Button, type ButtonProps } from 'react-aria-components';
import { OpenAPITooltip } from './OpenAPITooltip';
Expand Down Expand Up @@ -45,7 +46,7 @@ export function OpenAPICopyButton(
handleCopy();
onPress?.(e);
}}
className={`openapi-copy-button ${className}`}
className={clsx('openapi-copy-button', className)}
{...props}
>
{children}
Expand Down
15 changes: 9 additions & 6 deletions packages/react-openapi/src/OpenAPIDisclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export function OpenAPIDisclosure(props: {
children: React.ReactNode;
label: string | ((isExpanded: boolean) => string);
className?: string;
defaultExpanded?: boolean;
}): React.JSX.Element {
const { icon, header, label, children, className } = props;
const [isExpanded, setIsExpanded] = useState(false);
const { icon, header, label, children, className, defaultExpanded = false } = props;
const [isExpanded, setIsExpanded] = useState(defaultExpanded);

return (
<Disclosure
Expand All @@ -34,13 +35,15 @@ export function OpenAPIDisclosure(props: {
>
{header}
<div className="openapi-disclosure-trigger-label">
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
{label ? (
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
) : null}
{icon}
</div>
</Button>
<DisclosurePanel className="openapi-disclosure-panel">
{isExpanded ? children : null}
</DisclosurePanel>
{isExpanded ? (
<DisclosurePanel className="openapi-disclosure-panel">{children}</DisclosurePanel>
) : null}
</Disclosure>
);
}
51 changes: 47 additions & 4 deletions packages/react-openapi/src/OpenAPIDisclosureGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
'use client';

import clsx from 'classnames';
import { createContext, useContext, useRef } from 'react';
import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
import type { Key } from 'react-aria';
import {
type DisclosureGroupProps,
type DisclosureGroupState,
useDisclosureGroupState,
useDisclosureState,
} from 'react-stately';
import { useStore } from 'zustand';
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
import { getOrCreateDisclosureStoreByKey } from './getOrCreateDisclosureStoreByKey';

interface Props {
groups: TDisclosureGroup[];
icon?: React.ReactNode;
/** State key to use with a store */
selectStateKey?: string;
/** State key to synchronize disclosure groups across the page */
stateKey?: string;
/** Icon to display for the select */
selectIcon?: React.ReactNode;
className?: string;
}

type TDisclosureGroup = {
Expand All @@ -31,18 +38,50 @@ type TDisclosureGroup = {

const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);

function useDisclosureGroupStore(stateKey = 'disclosure-group', initialKeys?: Iterable<Key>) {
const store = useStore(getOrCreateDisclosureStoreByKey(stateKey, initialKeys));
return store;
}

/**
* Display an interactive OpenAPI disclosure group.
*/
export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
const { icon, groups, selectStateKey, selectIcon } = props;
const {
icon,
groups,
selectStateKey,
stateKey,
selectIcon,
className,
expandedKeys,
defaultExpandedKeys,
onExpandedChange,
} = props;

const initialKeys =
expandedKeys || defaultExpandedKeys
? new Set(expandedKeys || defaultExpandedKeys)
: undefined;
const { expandedKeys: storeExpandedKeys, setExpandedKeys } = useDisclosureGroupStore(
stateKey,
initialKeys
);

const state = useDisclosureGroupState(props);
const state = useDisclosureGroupState({
...props,
expandedKeys: storeExpandedKeys,
onExpandedChange: (keys) => {
setExpandedKeys(keys);
onExpandedChange?.(keys);
},
});

return (
<DisclosureGroupStateContext.Provider value={state}>
{groups.map((group) => (
<DisclosureItem
className={className}
selectStateKey={selectStateKey}
selectIcon={selectIcon}
icon={icon}
Expand All @@ -59,8 +98,9 @@ function DisclosureItem(props: {
icon?: React.ReactNode;
selectStateKey?: string;
selectIcon?: React.ReactNode;
className?: string;
}) {
const { icon, group, selectStateKey, selectIcon } = props;
const { icon, group, selectStateKey, selectIcon, className } = props;

const defaultId = useId();
const id = group.key || defaultId;
Expand Down Expand Up @@ -95,7 +135,10 @@ function DisclosureItem(props: {
const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];

return (
<div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
<div
className={clsx('openapi-disclosure-group', className)}
aria-expanded={state.isExpanded}
>
<div
slot="trigger"
ref={triggerRef}
Expand Down
120 changes: 120 additions & 0 deletions packages/react-openapi/src/OpenAPIRequiredScopes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use client';

import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
import { useSelectState } from './OpenAPISelect';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
import type { OpenAPISecurityScope } from './types';
import type { OperationSecurityInfo } from './utils';

/**
* Present securities authorization that can be used for this operation.
*/
export function OpenAPIRequiredScopes(props: {
securities: OperationSecurityInfo[];
context: OpenAPIClientContext;
stateKey: string;
}) {
const { securities, stateKey, context } = props;
const { key: selectedKey } = useSelectState(stateKey, securities[0]?.key);
const selectedSecurity = securities.find((security) => security.key === selectedKey);

if (!selectedSecurity) {
return null;
}

const scopes = selectedSecurity.schemes.flatMap((scheme) => {
if (scheme.type === 'oauth2') {
return Object.entries(scheme.flows ?? {}).flatMap(([_, flow]) =>
Object.entries(flow.scopes ?? {})
);
}

return scheme.scopes ?? [];
});

if (!scopes.length) {
return null;
}

return (
<OpenAPIDisclosureGroup
className="openapi-required-scopes"
icon={context.icons.chevronRight}
stateKey="required-scopes"
defaultExpandedKeys={['required-scopes']}
groups={[
{
key: 'required-scopes',
label: (
<div className="openapi-required-scopes-header">
{context.icons.lock}
<span>{t(context.translation, 'required_scopes')}</span>
</div>
),
tabs: [
{
key: 'scopes',
label: '',
body: <OpenAPISchemaScopes scopes={scopes} context={context} />,
},
],
},
]}
/>
);
}

function OpenAPISchemaScopes(props: {
scopes: OpenAPISecurityScope[];
context: OpenAPIClientContext;
}) {
const { scopes, context } = props;

return (
<div className="openapi-securities-scopes openapi-markdown">
<div className="openapi-required-scopes-description">
{t(context.translation, 'required_scopes_description')}
</div>
<ul>
{scopes.map((scope) => (
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
))}
</ul>
</div>
);
}

/**
* Display a scope item. Either a key-value pair or a single string.
*/
function OpenAPIScopeItem(props: {
scope: OpenAPISecurityScope;
context: OpenAPIClientContext;
}) {
const { scope, context } = props;

return (
<li>
<OpenAPIScopeItemKey name={scope[0]} context={context} />
{scope[1] ? <span>: {scope[1]}</span> : null}
</li>
);
}

/**
* Displays the scope name within a copyable button.
*/
function OpenAPIScopeItemKey(props: {
name: string;
context: OpenAPIClientContext;
}) {
const { name, context } = props;

return (
<OpenAPICopyButton value={name} context={context} withTooltip>
<code>{name}</code>
</OpenAPICopyButton>
);
}
Loading