mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Improve OpenAPIResponses examples and schemas (#3123)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve OpenAPIResponses examples and schemas
|
||||
@@ -8,6 +8,10 @@
|
||||
@apply flex flex-col mb-14 flex-1;
|
||||
}
|
||||
|
||||
.openapi-schemas-title {
|
||||
@apply tabular-nums text-[0.813rem] leading-4 font-mono shrink-0 font-medium text-tint-strong;
|
||||
}
|
||||
|
||||
.openapi-columns {
|
||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch;
|
||||
}
|
||||
@@ -46,10 +50,7 @@
|
||||
@apply font-semibold font-mono truncate;
|
||||
}
|
||||
|
||||
.openapi-description.openapi-markdown {
|
||||
@apply prose-sm text-[0.938rem];
|
||||
}
|
||||
|
||||
.openapi-description.openapi-markdown,
|
||||
.openapi-description.openapi-markdown code {
|
||||
@apply prose-sm;
|
||||
}
|
||||
@@ -89,21 +90,23 @@
|
||||
}
|
||||
|
||||
/* Method Tags */
|
||||
.openapi-method {
|
||||
@apply rounded uppercase font-mono shrink-0 font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
|
||||
.openapi-method,
|
||||
.openapi-statuscode {
|
||||
@apply rounded uppercase font-mono items-center shrink-0 font-semibold text-[0.813rem] px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap;
|
||||
}
|
||||
|
||||
.openapi-method-get {
|
||||
/* @apply bg-[hsl(215,54%,86%)] dark:bg-[hsla(215,54%,45%,0.24)] dark:text-[hsl(215,54%,86%)]; */
|
||||
.openapi-method-get,
|
||||
.openapi-statuscode-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
||||
}
|
||||
|
||||
.openapi-method-post {
|
||||
/* @apply bg-[hsl(120,25%,80%)] dark:bg-[hsla(120,54%,32%,0.24)] dark:text-[hsl(120,25%,80%)]; */
|
||||
.openapi-method-post,
|
||||
.openapi-statuscode-redirection {
|
||||
@apply bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100;
|
||||
}
|
||||
|
||||
.openapi-method-put {
|
||||
.openapi-method-put,
|
||||
.openapi-statuscode-informational {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100;
|
||||
}
|
||||
|
||||
@@ -111,8 +114,9 @@
|
||||
@apply bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100;
|
||||
}
|
||||
|
||||
.openapi-method-delete {
|
||||
@apply bg-pomegranate-100 text-pomegranate-800 dark:bg-pomegranate-900 dark:text-pomegranate-100;
|
||||
.openapi-method-delete,
|
||||
.openapi-statuscode-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
|
||||
}
|
||||
|
||||
.openapi-method-head,
|
||||
@@ -140,7 +144,7 @@
|
||||
}
|
||||
|
||||
.openapi-column-preview-body {
|
||||
@apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:page-has-toc:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static;
|
||||
@apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static;
|
||||
}
|
||||
|
||||
.openapi-column-preview pre {
|
||||
@@ -157,7 +161,7 @@
|
||||
}
|
||||
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root-description {
|
||||
@apply px-2.5 pt-1 !text-sm;
|
||||
@apply px-2.5 pt-2.5 mt-0 !text-sm;
|
||||
}
|
||||
|
||||
.openapi-schema-properties {
|
||||
@@ -360,7 +364,7 @@
|
||||
|
||||
/* Responses */
|
||||
.openapi-responses-header {
|
||||
@apply py-2 border-b border-tint-subtle max-w-full flex-1;
|
||||
@apply py-2 max-w-full flex-1;
|
||||
}
|
||||
|
||||
.openapi-responses-header-content {
|
||||
@@ -368,7 +372,8 @@
|
||||
}
|
||||
|
||||
.openapi-response-tab-content {
|
||||
@apply overflow-hidden max-w-full flex items-baseline gap-2;
|
||||
@apply overflow-hidden max-w-full flex items-baseline;
|
||||
@apply text-left text-pretty relative leading-[1.125rem] text-tint !font-normal truncate select-text;
|
||||
}
|
||||
|
||||
.openapi-response-description.openapi-markdown {
|
||||
@@ -383,10 +388,6 @@
|
||||
@apply truncate max-w-full inline pr-1;
|
||||
}
|
||||
|
||||
.openapi-response-statuscode {
|
||||
@apply tabular-nums text-sm font-normal font-mono shrink-0;
|
||||
}
|
||||
|
||||
.openapi-response-content-type {
|
||||
@apply text-xs text-tint-8 ml-auto shrink-0;
|
||||
}
|
||||
@@ -416,15 +417,23 @@
|
||||
|
||||
/* Code Sample */
|
||||
.openapi-codesample {
|
||||
@apply border rounded bg-tint border-tint-subtle;
|
||||
@apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-codesample-header {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
.openapi-codesample-header .openapi-select > button {
|
||||
@apply border-none;
|
||||
.openapi-response-media-types-examples-footer-content {
|
||||
@apply flex flex-row items-center gap-2.5;
|
||||
}
|
||||
|
||||
.openapi-response-examples-header .openapi-select > button {
|
||||
@apply max-w-full overflow-hidden shrink pl-0.5 py-0.5;
|
||||
}
|
||||
|
||||
.openapi-response-examples-header .openapi-select > button .openapi-statuscode {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content {
|
||||
@@ -432,7 +441,7 @@
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content .openapi-path {
|
||||
@apply flex items-center font-mono *:text-[0.813rem] gap-2 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
|
||||
@apply flex items-center font-mono text-[0.813rem] gap-1 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong;
|
||||
}
|
||||
|
||||
.openapi-codesample-header-content .openapi-path .openapi-path-variable {
|
||||
@@ -463,7 +472,7 @@
|
||||
}
|
||||
|
||||
.openapi-path .openapi-method {
|
||||
@apply text-[0.813rem] m-0 mt-0.5 items-center flex px-1;
|
||||
@apply m-0 mt-0.5 items-center flex px-1;
|
||||
}
|
||||
|
||||
.openapi-path-title {
|
||||
@@ -494,7 +503,7 @@
|
||||
|
||||
/* Panel */
|
||||
.openapi-panel {
|
||||
@apply border rounded bg-tint border-tint-subtle;
|
||||
@apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-panel-heading {
|
||||
@@ -516,13 +525,41 @@
|
||||
}
|
||||
|
||||
/* Example */
|
||||
.openapi-response-examples {
|
||||
@apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-response-examples-header {
|
||||
@apply flex flex-row items-center p-2.5;
|
||||
}
|
||||
|
||||
.openapi-response-examples-header-content {
|
||||
@apply max-w-full overflow-hidden truncate;
|
||||
}
|
||||
|
||||
.openapi-response-examples-statuscode-title {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.openapi-response-examples-header .openapi-select > button,
|
||||
.openapi-response-examples-header .openapi-markdown,
|
||||
.openapi-response-examples-statuscode-title {
|
||||
@apply text-[0.813rem] truncate text-tint font-normal;
|
||||
}
|
||||
|
||||
.openapi-response-examples-panel,
|
||||
.openapi-codesample-panel {
|
||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
.openapi-example-empty {
|
||||
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
|
||||
}
|
||||
|
||||
/* Common Elements */
|
||||
.openapi-select {
|
||||
/* unstyled */
|
||||
@apply w-auto max-w-full;
|
||||
}
|
||||
|
||||
/* Prevent react-aria popover from setting overflow:auto on body */
|
||||
@@ -531,24 +568,32 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-select > button {
|
||||
@apply flex items-center cursor-pointer gap-1.5 text-tint-strong max-w-60 rounded text-xs leading-6 px-1.5 truncate border border-tint-subtle bg-tint;
|
||||
@apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong max-w-32 rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint;
|
||||
@apply hover:bg-tint-hover transition-all;
|
||||
}
|
||||
|
||||
.openapi-select > button[data-focused="true"] {
|
||||
@apply outline-primary -outline-offset-1 outline outline-1;
|
||||
}
|
||||
|
||||
.openapi-select > button > span.react-aria-SelectValue {
|
||||
@apply shrink truncate;
|
||||
@apply shrink truncate flex items-center;
|
||||
}
|
||||
|
||||
.openapi-select > button > .gb-icon {
|
||||
@apply shrink-0;
|
||||
.openapi-select > button > span.react-aria-SelectValue span:not(.openapi-statuscode) {
|
||||
@apply truncate;
|
||||
}
|
||||
|
||||
.openapi-select > button svg {
|
||||
@apply size-2.5;
|
||||
.openapi-select > button .openapi-markdown {
|
||||
@apply *:leading-none;
|
||||
}
|
||||
|
||||
.openapi-select > button .gb-icon {
|
||||
@apply size-2.5 shrink-0;
|
||||
}
|
||||
|
||||
.openapi-select-popover {
|
||||
@apply min-w-32 max-w-fit w-auto max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md;
|
||||
@apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none;
|
||||
@apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
|
||||
}
|
||||
|
||||
@@ -561,11 +606,17 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-select-item {
|
||||
@apply text-sm cursor-pointer px-1.5 py-0.5 truncate text-tint ring-0 border-none rounded !outline-none;
|
||||
@apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 *:truncate text-tint ring-0 border-none rounded !outline-none;
|
||||
@apply hover:bg-tint-hover theme-gradient:hover: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-selected {
|
||||
.openapi-select button .openapi-markdown,
|
||||
.openapi-select-item .openapi-markdown {
|
||||
@apply text-[0.813rem] *:truncate;
|
||||
}
|
||||
|
||||
.openapi-select-item-selected,
|
||||
.openapi-select-item-selected .openapi-markdown {
|
||||
@apply text-primary-subtle hover:text-primary hover:bg-primary-hover;
|
||||
@apply theme-muted:hover:bg-primary-active theme-gradient:hover:bg-primary-active tint:font-semibold;
|
||||
@apply contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:font-semibold;
|
||||
@@ -600,6 +651,14 @@ body:has(.openapi-select-popover) {
|
||||
@apply text-tint;
|
||||
}
|
||||
|
||||
.openapi-section-footer {
|
||||
@apply flex flex-row items-center p-2.5 gap-2.5 text-sm text-tint-strong border-t border-tint-subtle;
|
||||
}
|
||||
|
||||
.openapi-section-footer-content {
|
||||
@apply text-sm text-tint-strong;
|
||||
}
|
||||
|
||||
.openapi-section-toggle {
|
||||
@apply text-tint-subtle contrast-more:text-tint-strong;
|
||||
}
|
||||
@@ -621,6 +680,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.openapi-panel-header,
|
||||
.openapi-tabs-list {
|
||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
|
||||
scrollbar-width: none;
|
||||
@@ -632,18 +692,17 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-tabs-tab[aria-selected="true"] {
|
||||
@apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
|
||||
@apply !text-primary-subtle after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all;
|
||||
}
|
||||
|
||||
.openapi-tabs-panel,
|
||||
.openapi-codesample-panel {
|
||||
.openapi-tabs-panel {
|
||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
||||
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||
}
|
||||
|
||||
/* Disclosure group */
|
||||
.openapi-disclosure-group {
|
||||
@apply border-b border-tint-subtle relative;
|
||||
@apply border-tint-subtle border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-header {
|
||||
@@ -651,16 +710,11 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-trigger {
|
||||
@apply flex items-baseline relative flex-1 gap-2.5 py-2 truncate -outline-offset-1;
|
||||
@apply flex items-baseline transition-all hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-trigger:disabled {
|
||||
@apply cursor-default;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group:only-child,
|
||||
.openapi-disclosure-group:last-child {
|
||||
@apply border-b-0;
|
||||
@apply cursor-default hover:bg-inherit;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon {
|
||||
@@ -680,28 +734,42 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-panel {
|
||||
@apply pb-2.5;
|
||||
@apply p-3 pt-1 transition-all;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype {
|
||||
@apply opacity-11 flex;
|
||||
.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype,
|
||||
.openapi-disclosure-group-mediatype:has(> .openapi-select[data-open="true"]) {
|
||||
@apply opacity-11 visible flex;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-mediatype {
|
||||
@apply opacity-0 hidden text-xs transition-all duration-200 shrink-0 absolute right-0 top-2.5;
|
||||
@apply opacity-0 invisible text-xs transition-opacity duration-300 shrink-0 absolute right-2.5 top-2.5;
|
||||
}
|
||||
|
||||
.openapi-disclosure-group-mediatype > span {
|
||||
@apply px-1 bg-tint-6 text-tint-12 rounded-full;
|
||||
@apply px-1 bg-tint-6 text-tint-12 rounded-full straight-corners:rounded-md;
|
||||
}
|
||||
|
||||
/* Disclosure */
|
||||
.openapi-schemas-disclosure > .openapi-disclosure-trigger {
|
||||
@apply flex items-center font-mono !w-full transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1;
|
||||
}
|
||||
|
||||
.openapi-schemas-disclosure > .openapi-disclosure-trigger,
|
||||
.openapi-schemas-disclosure .openapi-disclosure-panel {
|
||||
@apply straight-corners:!rounded-none;
|
||||
}
|
||||
|
||||
.openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg {
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger {
|
||||
@apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
|
||||
@apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl straight-corners:rounded border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger span {
|
||||
@@ -717,7 +785,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger[aria-expanded="true"] {
|
||||
@apply w-full rounded-lg border-b rounded-b-none;
|
||||
@apply w-full rounded-lg border-b rounded-b-none straight-corners:rounded-b-none;
|
||||
}
|
||||
|
||||
.openapi-disclosure-trigger[aria-expanded="false"] {
|
||||
@@ -725,7 +793,7 @@ body:has(.openapi-select-popover) {
|
||||
}
|
||||
|
||||
.openapi-disclosure-panel[aria-hidden="false"] {
|
||||
@apply border-b border-x border-tint-subtle rounded-b-lg;
|
||||
@apply border-b border-x border-tint-subtle rounded-b-lg straight-corners:rounded-b;
|
||||
}
|
||||
|
||||
.openapi-disclosure-panel .openapi-schema {
|
||||
@@ -745,17 +813,13 @@ body:has(.openapi-select-popover) {
|
||||
@apply space-y-2.5;
|
||||
}
|
||||
|
||||
.openapi-section-schemas {
|
||||
@apply border border-tint-subtle rounded-lg;
|
||||
}
|
||||
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema,
|
||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
|
||||
@apply p-2.5;
|
||||
}
|
||||
|
||||
.openapi-tooltip {
|
||||
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md font-medium px-1.5 py-0.5 shadow-sm text-[13px];
|
||||
@apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px];
|
||||
}
|
||||
|
||||
.openapi-tooltip svg {
|
||||
|
||||
@@ -123,7 +123,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
>
|
||||
{section.tag ? (
|
||||
<span
|
||||
className={`-mt-0.5 openapi-method openapi-method-${section.tag.toLowerCase()}`}
|
||||
className={`-mt-0.5 openapi-method !text-xs openapi-method-${section.tag.toLowerCase()}`}
|
||||
>
|
||||
{section.tag}
|
||||
</span>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||
import { useDisclosureState } from 'react-stately';
|
||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
|
||||
|
||||
interface InteractiveSectionTab {
|
||||
@@ -35,6 +35,10 @@ export function InteractiveSection(props: {
|
||||
header?: React.ReactNode;
|
||||
/** Children to display within the container */
|
||||
overlay?: React.ReactNode;
|
||||
/** State key to use with a store */
|
||||
stateKey?: string;
|
||||
/** Icon for the tabs select */
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const {
|
||||
id,
|
||||
@@ -46,12 +50,9 @@ export function InteractiveSection(props: {
|
||||
header,
|
||||
overlay,
|
||||
toggleIcon = '▶',
|
||||
selectIcon,
|
||||
stateKey = 'interactive-section',
|
||||
} = props;
|
||||
|
||||
const [selectedTabKey, setSelectedTab] = useState(defaultTab);
|
||||
const selectedTab: InteractiveSectionTab | undefined =
|
||||
tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
||||
|
||||
const state = useDisclosureState({
|
||||
defaultExpanded: defaultOpened,
|
||||
});
|
||||
@@ -60,6 +61,10 @@ export function InteractiveSection(props: {
|
||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
const store = useSelectState(stateKey, defaultTab);
|
||||
|
||||
const selectedTab: InteractiveSectionTab | undefined =
|
||||
tabs.find((tab) => tab.key === store.key) ?? tabs[0];
|
||||
|
||||
return (
|
||||
<Section
|
||||
@@ -108,16 +113,12 @@ export function InteractiveSection(props: {
|
||||
>
|
||||
{tabs.length > 1 ? (
|
||||
<OpenAPISelect
|
||||
className={clsx(
|
||||
'openapi-section-select',
|
||||
`${className}-tabs-select`
|
||||
)}
|
||||
stateKey={stateKey}
|
||||
items={tabs}
|
||||
selectedKey={selectedTab?.key ?? ''}
|
||||
onSelectionChange={(key) => {
|
||||
setSelectedTab(String(key));
|
||||
onSelectionChange={() => {
|
||||
state.expand();
|
||||
}}
|
||||
icon={selectIcon}
|
||||
placement="bottom end"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -206,6 +206,7 @@ function OpenAPICodeSampleFooter(props: {
|
||||
method={data.method}
|
||||
path={data.path}
|
||||
renderers={renderers}
|
||||
selectIcon={context.icons.chevronDown}
|
||||
/>
|
||||
) : (
|
||||
<span />
|
||||
|
||||
@@ -1,70 +1,41 @@
|
||||
'use client';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
||||
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||
|
||||
type MediaTypeState = {
|
||||
mediaType: string;
|
||||
setMediaType: (mediaType: string) => void;
|
||||
};
|
||||
|
||||
function useMediaTypeState(
|
||||
data: { method: string; path: string },
|
||||
defaultKey: string
|
||||
): MediaTypeState {
|
||||
const { method, path } = data;
|
||||
const store = useStore(getOrCreateStoreByKey(`media-type-${method}-${path}`, defaultKey));
|
||||
if (typeof store.key !== 'string') {
|
||||
throw new Error('Media type key is not a string');
|
||||
}
|
||||
return {
|
||||
mediaType: store.key,
|
||||
setMediaType: useCallback((index: string) => store.setKey(index), [store.setKey]),
|
||||
};
|
||||
}
|
||||
|
||||
function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) {
|
||||
const { method, path } = data;
|
||||
const store = useStore(
|
||||
getOrCreateStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0)
|
||||
);
|
||||
if (typeof store.key !== 'number') {
|
||||
throw new Error('Example key is not a number');
|
||||
}
|
||||
return {
|
||||
index: store.key,
|
||||
setIndex: useCallback((index: number) => store.setKey(index), [store.setKey]),
|
||||
};
|
||||
}
|
||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||
|
||||
export function OpenAPIMediaTypeExamplesSelector(props: {
|
||||
method: string;
|
||||
path: string;
|
||||
renderers: MediaTypeRenderer[];
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { method, path, renderers } = props;
|
||||
const { method, path, renderers, selectIcon } = props;
|
||||
if (!renderers[0]) {
|
||||
throw new Error('No renderers provided');
|
||||
}
|
||||
const state = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
||||
const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0];
|
||||
const stateKey = `media-type-${method}-${path}`;
|
||||
const state = useSelectState(stateKey, renderers[0].mediaType);
|
||||
const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0];
|
||||
|
||||
return (
|
||||
<div className="openapi-codesample-selectors">
|
||||
<MediaTypeSelector state={state} renderers={renderers} />
|
||||
<ExamplesSelector method={method} path={path} renderer={selected} />
|
||||
<MediaTypeSelector selectIcon={selectIcon} stateKey={stateKey} renderers={renderers} />
|
||||
<ExamplesSelector
|
||||
selectIcon={selectIcon}
|
||||
method={method}
|
||||
path={path}
|
||||
renderer={selected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaTypeSelector(props: {
|
||||
state: MediaTypeState;
|
||||
stateKey: string;
|
||||
renderers: MediaTypeRenderer[];
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { renderers, state } = props;
|
||||
const { renderers, stateKey, selectIcon } = props;
|
||||
|
||||
if (renderers.length < 2) {
|
||||
return null;
|
||||
@@ -78,12 +49,12 @@ function MediaTypeSelector(props: {
|
||||
return (
|
||||
<OpenAPISelect
|
||||
className={clsx('openapi-select')}
|
||||
selectedKey={state.mediaType}
|
||||
items={renderers.map((renderer) => ({
|
||||
key: renderer.mediaType,
|
||||
label: renderer.mediaType,
|
||||
}))}
|
||||
onSelectionChange={(e) => state.setMediaType(String(e))}
|
||||
icon={selectIcon}
|
||||
stateKey={stateKey}
|
||||
placement="bottom start"
|
||||
>
|
||||
{items.map((item) => (
|
||||
@@ -99,9 +70,9 @@ function ExamplesSelector(props: {
|
||||
method: string;
|
||||
path: string;
|
||||
renderer: MediaTypeRenderer;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { method, path, renderer } = props;
|
||||
const state = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
||||
const { method, path, renderer, selectIcon } = props;
|
||||
if (renderer.examples.length < 2) {
|
||||
return null;
|
||||
}
|
||||
@@ -114,8 +85,8 @@ function ExamplesSelector(props: {
|
||||
return (
|
||||
<OpenAPISelect
|
||||
items={items}
|
||||
selectedKey={state.index}
|
||||
onSelectionChange={(e) => state.setIndex(Number(e))}
|
||||
icon={selectIcon}
|
||||
stateKey={`media-type-sample-${renderer.mediaType}-${method}-${path}`}
|
||||
placement="bottom start"
|
||||
>
|
||||
{items.map((item) => (
|
||||
@@ -136,9 +107,9 @@ export function OpenAPIMediaTypeExamplesBody(props: {
|
||||
if (!renderers[0]) {
|
||||
throw new Error('No renderers provided');
|
||||
}
|
||||
const mediaTypeState = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
||||
const selected =
|
||||
renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0];
|
||||
|
||||
const mediaTypeState = useSelectState(`media-type-${method}-${path}`, renderers[0].mediaType);
|
||||
const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0];
|
||||
if (selected.examples.length === 0) {
|
||||
return selected.element;
|
||||
}
|
||||
@@ -147,10 +118,13 @@ export function OpenAPIMediaTypeExamplesBody(props: {
|
||||
|
||||
function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) {
|
||||
const { method, path, renderer } = props;
|
||||
const exampleState = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
||||
const example = renderer.examples[exampleState.index] ?? renderer.examples[0];
|
||||
const exampleState = useSelectState(
|
||||
`media-type-sample-${renderer.mediaType}-${method}-${path}`,
|
||||
renderer.mediaType
|
||||
);
|
||||
const example = renderer.examples[Number(exampleState.key)] ?? renderer.examples[0];
|
||||
if (!example) {
|
||||
throw new Error(`No example found for index ${exampleState.index}`);
|
||||
throw new Error(`No example found for key ${exampleState.key}`);
|
||||
}
|
||||
return example.element;
|
||||
}
|
||||
|
||||
@@ -22,25 +22,21 @@ type CodeSampleItem = OpenAPISelectItem & {
|
||||
footer?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function OpenAPICodeSampleHeader(props: {
|
||||
function OpenAPICodeSampleHeader(props: {
|
||||
items: CodeSampleItem[];
|
||||
data: OpenAPIOperationData;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { data, items } = props;
|
||||
|
||||
const state = useCodeSampleState(items[0]?.key ?? '');
|
||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
||||
const { data, items, selectIcon } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<OpenAPIPath canCopy={false} withServer={false} data={data} />
|
||||
{items.length > 1 ? (
|
||||
<OpenAPISelect
|
||||
selectedKey={selected?.key}
|
||||
onSelectionChange={(key) => {
|
||||
state.setKey(key);
|
||||
}}
|
||||
icon={selectIcon}
|
||||
items={items}
|
||||
stateKey="codesample"
|
||||
placement="bottom end"
|
||||
>
|
||||
{items.map((item) => (
|
||||
@@ -59,8 +55,9 @@ export function OpenAPICodeSampleHeader(props: {
|
||||
export function OpenAPICodeSampleBody(props: {
|
||||
items: CodeSampleItem[];
|
||||
data: OpenAPIOperationData;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { items, data } = props;
|
||||
const { items, data, selectIcon } = props;
|
||||
if (!items[0]) {
|
||||
throw new Error('No items provided');
|
||||
}
|
||||
@@ -75,7 +72,7 @@ export function OpenAPICodeSampleBody(props: {
|
||||
|
||||
return (
|
||||
<StaticSection
|
||||
header={<OpenAPICodeSampleHeader data={data} items={items} />}
|
||||
header={<OpenAPICodeSampleHeader selectIcon={selectIcon} data={data} items={items} />}
|
||||
className="openapi-codesample"
|
||||
>
|
||||
<div id={selected.key as string} className="openapi-codesample-panel">
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
'use client';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { Button, Disclosure, DisclosurePanel } from 'react-aria-components';
|
||||
|
||||
/**
|
||||
* Display an interactive OpenAPI disclosure.
|
||||
*/
|
||||
export function OpenAPIDisclosure(props: {
|
||||
context: OpenAPIClientContext;
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
label: string;
|
||||
label: string | ((isExpanded: boolean) => string);
|
||||
className?: string;
|
||||
}): React.JSX.Element {
|
||||
const { context, children, label } = props;
|
||||
const { icon, children, label, className } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
className="openapi-disclosure"
|
||||
className={clsx('openapi-disclosure', className)}
|
||||
isExpanded={isExpanded}
|
||||
onExpandedChange={setIsExpanded}
|
||||
>
|
||||
<Heading>
|
||||
<Button
|
||||
slot="trigger"
|
||||
className="openapi-disclosure-trigger"
|
||||
style={({ isFocusVisible }) => ({
|
||||
outline: isFocusVisible
|
||||
? '2px solid rgb(var(--primary-color-500) / 0.4)'
|
||||
: 'none',
|
||||
})}
|
||||
>
|
||||
{context.icons.plus}
|
||||
<span>
|
||||
{isExpanded ? 'Hide' : 'Show'} {label}
|
||||
</span>
|
||||
</Button>
|
||||
</Heading>
|
||||
<Button
|
||||
slot="trigger"
|
||||
className="openapi-disclosure-trigger"
|
||||
style={({ isFocusVisible }) => ({
|
||||
outline: isFocusVisible
|
||||
? '2px solid rgb(var(--primary-color-500) / 0.4)'
|
||||
: 'none',
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
|
||||
</Button>
|
||||
<DisclosurePanel className="openapi-disclosure-panel">
|
||||
{isExpanded ? children : null}
|
||||
</DisclosurePanel>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useRef, useState } from 'react';
|
||||
import { createContext, useContext, useRef } from 'react';
|
||||
import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
|
||||
import {
|
||||
type DisclosureGroupProps,
|
||||
@@ -8,18 +8,23 @@ import {
|
||||
useDisclosureGroupState,
|
||||
useDisclosureState,
|
||||
} from 'react-stately';
|
||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||
|
||||
interface Props {
|
||||
groups: TDisclosureGroup[];
|
||||
icon?: React.ReactNode;
|
||||
/** State key to use with a store */
|
||||
selectStateKey?: string;
|
||||
/** Icon to display for the select */
|
||||
selectIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
type TDisclosureGroup = {
|
||||
id: string;
|
||||
key: string;
|
||||
label: string | React.ReactNode;
|
||||
tabs?: {
|
||||
id: string;
|
||||
label?: string | React.ReactNode;
|
||||
key: string;
|
||||
label: string | React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
}[];
|
||||
};
|
||||
@@ -30,24 +35,35 @@ const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(n
|
||||
* Display an interactive OpenAPI disclosure group.
|
||||
*/
|
||||
export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
|
||||
const { icon, groups } = props;
|
||||
const { icon, groups, selectStateKey, selectIcon } = props;
|
||||
|
||||
const state = useDisclosureGroupState(props);
|
||||
|
||||
return (
|
||||
<DisclosureGroupStateContext.Provider value={state}>
|
||||
{groups.map((group) => (
|
||||
<DisclosureItem icon={icon} key={group.id} group={group} />
|
||||
<DisclosureItem
|
||||
selectStateKey={selectStateKey}
|
||||
selectIcon={selectIcon}
|
||||
icon={icon}
|
||||
key={group.key}
|
||||
group={group}
|
||||
/>
|
||||
))}
|
||||
</DisclosureGroupStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) {
|
||||
const { icon, group } = props;
|
||||
function DisclosureItem(props: {
|
||||
group: TDisclosureGroup;
|
||||
icon?: React.ReactNode;
|
||||
selectStateKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { icon, group, selectStateKey, selectIcon } = props;
|
||||
|
||||
const defaultId = useId();
|
||||
const id = group.id || defaultId;
|
||||
const id = group.key || defaultId;
|
||||
const groupState = useContext(DisclosureGroupStateContext);
|
||||
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
||||
const state = useDisclosureState({
|
||||
@@ -74,9 +90,9 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||
const { isFocusVisible, focusProps } = useFocusRing();
|
||||
|
||||
const defaultTab = group.tabs?.[0]?.id || '';
|
||||
const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
|
||||
const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
|
||||
const defaultTab = group.tabs?.[0]?.key || '';
|
||||
const store = useSelectState(selectStateKey, defaultTab);
|
||||
const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];
|
||||
|
||||
return (
|
||||
<div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
|
||||
@@ -104,23 +120,25 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
||||
{group.label}
|
||||
</button>
|
||||
{group.tabs ? (
|
||||
<div className="openapi-disclosure-group-mediatype">
|
||||
<div
|
||||
className="openapi-disclosure-group-mediatype"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{group.tabs?.length > 1 ? (
|
||||
<select
|
||||
className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
value={selectedTab?.id}
|
||||
onChange={(event) => {
|
||||
setSelectedTabKey(event.target.value);
|
||||
<OpenAPISelect
|
||||
icon={selectIcon}
|
||||
stateKey={selectStateKey}
|
||||
onSelectionChange={() => {
|
||||
state.expand();
|
||||
}}
|
||||
items={group.tabs}
|
||||
>
|
||||
{group.tabs.map((tab) => (
|
||||
<option key={tab.id} value={tab.id}>
|
||||
<OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
|
||||
{tab.label}
|
||||
</option>
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</select>
|
||||
</OpenAPISelect>
|
||||
) : group.tabs[0]?.label ? (
|
||||
<span>{group.tabs[0].label}</span>
|
||||
) : null}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
|
||||
import { checkIsReference } from './utils';
|
||||
|
||||
/**
|
||||
@@ -10,8 +10,10 @@ import { checkIsReference } from './utils';
|
||||
export function OpenAPIRequestBody(props: {
|
||||
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
|
||||
context: OpenAPIClientContext;
|
||||
data: OpenAPIOperationData;
|
||||
}) {
|
||||
const { requestBody, context } = props;
|
||||
const { requestBody, context, data } = props;
|
||||
const { method, path } = data;
|
||||
|
||||
if (checkIsReference(requestBody)) {
|
||||
return null;
|
||||
@@ -21,6 +23,8 @@ export function OpenAPIRequestBody(props: {
|
||||
<InteractiveSection
|
||||
header="Body"
|
||||
className="openapi-requestbody"
|
||||
stateKey={`media-type-${method}-${path}`}
|
||||
selectIcon={context.icons.chevronDown}
|
||||
tabs={Object.entries(requestBody.content ?? {}).map(
|
||||
([contentType, mediaTypeObject]) => {
|
||||
return {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function OpenAPIResponse(props: {
|
||||
return (
|
||||
<div className="openapi-response-body">
|
||||
{headers.length > 0 ? (
|
||||
<OpenAPIDisclosure context={context} label="Headers">
|
||||
<OpenAPIDisclosure icon={context.icons.plus} label="Headers">
|
||||
<OpenAPISchemaProperties
|
||||
properties={headers.map(([name, header]) =>
|
||||
parameterToProperty({ name, ...header })
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
getExampleFromReference,
|
||||
getExamplesFromMediaTypeObject,
|
||||
} from './OpenAPIExample';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
|
||||
import { OpenAPIResponseMediaTypeContent } from './OpenAPIResponseMediaType';
|
||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
||||
import { checkIsReference, createStateKey, resolveDescription } from './utils';
|
||||
import { getStatusCodeDefaultLabel } from './utils';
|
||||
import { checkIsReference, resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
* Display an example of the response content.
|
||||
@@ -45,11 +46,17 @@ export function OpenAPIResponseExample(props: {
|
||||
|
||||
const tabs = responses.map(([key, responseObject]) => {
|
||||
const description = resolveDescription(responseObject);
|
||||
const label = description ? (
|
||||
<Markdown source={description} />
|
||||
) : (
|
||||
getStatusCodeDefaultLabel(key)
|
||||
);
|
||||
|
||||
if (checkIsReference(responseObject)) {
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
label,
|
||||
statusCode: key,
|
||||
body: (
|
||||
<OpenAPIExample
|
||||
example={getExampleFromReference(responseObject)}
|
||||
@@ -57,24 +64,23 @@ export function OpenAPIResponseExample(props: {
|
||||
syntax="json"
|
||||
/>
|
||||
),
|
||||
footer: description ? <Markdown source={description} /> : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
label,
|
||||
statusCode: key,
|
||||
body: <OpenAPIEmptyExample />,
|
||||
footer: description ? <Markdown source={description} /> : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key: key,
|
||||
label: key,
|
||||
label,
|
||||
statusCode: key,
|
||||
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
||||
footer: description ? <Markdown source={description} /> : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -83,11 +89,11 @@ export function OpenAPIResponseExample(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-panel">
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
<OpenAPIResponseExampleContent
|
||||
selectIcon={context.icons.chevronDown}
|
||||
blockKey={context.blockKey}
|
||||
items={tabs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,42 +112,30 @@ function OpenAPIResponse(props: {
|
||||
throw new Error('One media type is required');
|
||||
}
|
||||
|
||||
if (entries.length === 1) {
|
||||
const [mediaType, mediaTypeObject] = firstEntry;
|
||||
return (
|
||||
<OpenAPIResponseMediaType
|
||||
context={context}
|
||||
mediaType={mediaType}
|
||||
mediaTypeObject={mediaTypeObject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = entries.map((entry) => {
|
||||
const [mediaType, mediaTypeObject] = entry;
|
||||
return {
|
||||
key: mediaType,
|
||||
label: mediaType,
|
||||
body: (
|
||||
<OpenAPIResponseMediaType
|
||||
context={context}
|
||||
mediaType={mediaType}
|
||||
mediaTypeObject={mediaTypeObject}
|
||||
/>
|
||||
),
|
||||
body: <></>,
|
||||
examples: getExamples({
|
||||
mediaTypeObject,
|
||||
mediaType,
|
||||
context,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
|
||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-media-types">
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
<OpenAPIResponseMediaTypeContent
|
||||
selectIcon={context.icons.chevronDown}
|
||||
blockKey={context.blockKey}
|
||||
items={tabs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseMediaType(props: {
|
||||
function getExamples(props: {
|
||||
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
||||
mediaType: string;
|
||||
context: OpenAPIContext;
|
||||
@@ -149,23 +143,8 @@ function OpenAPIResponseMediaType(props: {
|
||||
const { mediaTypeObject, mediaType } = props;
|
||||
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
|
||||
const syntax = getSyntaxFromMediaType(mediaType);
|
||||
const firstExample = examples[0];
|
||||
|
||||
if (!firstExample) {
|
||||
return <OpenAPIEmptyExample />;
|
||||
}
|
||||
|
||||
if (examples.length === 1) {
|
||||
return (
|
||||
<OpenAPIExample
|
||||
example={firstExample.example}
|
||||
context={props.context}
|
||||
syntax={syntax}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = examples.map((example) => {
|
||||
return examples.map((example) => {
|
||||
return {
|
||||
key: example.key,
|
||||
label: example.example.summary || example.key,
|
||||
@@ -174,17 +153,6 @@ function OpenAPIResponseMediaType(props: {
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
|
||||
<StaticSection
|
||||
header={<OpenAPITabsList />}
|
||||
className="openapi-response-media-type-examples"
|
||||
>
|
||||
<OpenAPITabsPanels />
|
||||
</StaticSection>
|
||||
</OpenAPITabs>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type { Key } from 'react-aria';
|
||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { createStateKey, getStatusCodeClassName } from './utils';
|
||||
|
||||
type OpenAPIResponseExampleItem = OpenAPISelectItem & {
|
||||
statusCode: string;
|
||||
body: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the state of the response examples select.
|
||||
*/
|
||||
export function useResponseExamplesState(
|
||||
blockKey: string | undefined,
|
||||
initialKey: Key = 'default'
|
||||
) {
|
||||
return useSelectState(getResponseExampleStateKey(blockKey), initialKey);
|
||||
}
|
||||
|
||||
export function OpenAPIResponseExampleContent(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { blockKey, items, selectIcon } = props;
|
||||
|
||||
return (
|
||||
<StaticSection
|
||||
header={
|
||||
<OpenAPIResponseExampleHeader
|
||||
selectIcon={selectIcon}
|
||||
blockKey={blockKey}
|
||||
items={items}
|
||||
/>
|
||||
}
|
||||
className="openapi-response-examples"
|
||||
>
|
||||
<OpenAPIResponseExampleBody blockKey={blockKey} items={items} />
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseExampleHeader(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { items, blockKey, selectIcon } = props;
|
||||
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="openapi-response-examples-statuscode-title">
|
||||
<span
|
||||
className={clsx(
|
||||
'openapi-statuscode',
|
||||
`openapi-statuscode-${getStatusCodeClassName(item.statusCode)}`,
|
||||
'openapi-response-examples-statuscode'
|
||||
)}
|
||||
>
|
||||
{item.statusCode}
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OpenAPISelect
|
||||
items={items}
|
||||
icon={selectIcon}
|
||||
stateKey={getResponseExampleStateKey(blockKey)}
|
||||
placement="bottom start"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||
<span
|
||||
className={clsx(
|
||||
'openapi-statuscode',
|
||||
`openapi-statuscode-${getStatusCodeClassName(item.statusCode)}`,
|
||||
'openapi-response-examples-statuscode'
|
||||
)}
|
||||
>
|
||||
{item.statusCode}
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</OpenAPISelect>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseExampleBody(props: {
|
||||
items: OpenAPIResponseExampleItem[];
|
||||
blockKey?: string;
|
||||
}) {
|
||||
const { blockKey, items } = props;
|
||||
const state = useResponseExamplesState(blockKey, items[0]?.key);
|
||||
|
||||
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
|
||||
|
||||
if (!selectedItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="openapi-response-examples-panel">{selectedItem.body}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state key for the response examples.
|
||||
*/
|
||||
function getResponseExampleStateKey(blockKey: string | undefined) {
|
||||
return createStateKey('openapi-responses', blockKey);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
import type { Key } from 'react-aria';
|
||||
import { OpenAPIEmptyExample } from './OpenAPIExample';
|
||||
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import { createStateKey } from './utils';
|
||||
|
||||
type OpenAPIResponseMediaTypeItem = OpenAPISelectItem & {
|
||||
body: React.ReactNode;
|
||||
examples?: OpenAPIResponseMediaTypeItem[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the state of the response examples select.
|
||||
*/
|
||||
export function useResponseMediaTypesState(
|
||||
blockKey: string | undefined,
|
||||
initialKey: Key = 'default'
|
||||
) {
|
||||
return useSelectState(getResponseMediaTypeStateKey(blockKey), initialKey);
|
||||
}
|
||||
|
||||
function useResponseMediaTypeExamplesState(
|
||||
blockKey: string | undefined,
|
||||
initialKey: Key = 'default'
|
||||
) {
|
||||
return useSelectState(getResponseMediaTypeExamplesStateKey(blockKey), initialKey);
|
||||
}
|
||||
|
||||
export function OpenAPIResponseMediaTypeContent(props: {
|
||||
items: OpenAPIResponseMediaTypeItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { blockKey, items, selectIcon } = props;
|
||||
const state = useResponseMediaTypesState(blockKey, items[0]?.key);
|
||||
|
||||
const examples = items.find((item) => item.key === state.key)?.examples ?? [];
|
||||
|
||||
if (!items.length && !examples.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StaticSection
|
||||
footer={
|
||||
items.length > 1 || examples.length > 1 ? (
|
||||
<OpenAPIResponseMediaTypeFooter
|
||||
blockKey={blockKey}
|
||||
items={items}
|
||||
examples={examples}
|
||||
selectIcon={selectIcon}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
className="openapi-response-media-types-examples"
|
||||
>
|
||||
<OpenAPIResponseMediaTypeBody blockKey={blockKey} items={items} examples={examples} />
|
||||
</StaticSection>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseMediaTypeFooter(props: {
|
||||
items: OpenAPIResponseMediaTypeItem[];
|
||||
examples?: OpenAPIResponseMediaTypeItem[];
|
||||
blockKey?: string;
|
||||
selectIcon?: React.ReactNode;
|
||||
}) {
|
||||
const { items, examples, blockKey, selectIcon } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.length > 1 && (
|
||||
<OpenAPISelect
|
||||
icon={selectIcon}
|
||||
items={items}
|
||||
stateKey={getResponseMediaTypeStateKey(blockKey)}
|
||||
placement="bottom start"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<OpenAPISelectItem key={item.key} id={item.key} value={item}>
|
||||
<span>{item.label}</span>
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</OpenAPISelect>
|
||||
)}
|
||||
|
||||
{examples && examples.length > 1 ? (
|
||||
<OpenAPISelect
|
||||
icon={selectIcon}
|
||||
items={examples}
|
||||
stateKey={getResponseMediaTypeExamplesStateKey(blockKey)}
|
||||
placement="bottom start"
|
||||
>
|
||||
{examples.map((example) => (
|
||||
<OpenAPISelectItem key={example.key} id={example.key} value={example}>
|
||||
<span>{example.label}</span>
|
||||
</OpenAPISelectItem>
|
||||
))}
|
||||
</OpenAPISelect>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAPIResponseMediaTypeBody(props: {
|
||||
items: OpenAPIResponseMediaTypeItem[];
|
||||
examples?: OpenAPIResponseMediaTypeItem[];
|
||||
blockKey?: string;
|
||||
}) {
|
||||
const { blockKey, items, examples } = props;
|
||||
const state = useResponseMediaTypesState(blockKey, items[0]?.key);
|
||||
|
||||
const selectedItem = items.find((item) => item.key === state.key) ?? items[0];
|
||||
|
||||
const exampleState = useResponseMediaTypeExamplesState(
|
||||
blockKey,
|
||||
selectedItem?.examples?.[0]?.key
|
||||
);
|
||||
|
||||
if (!selectedItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (examples) {
|
||||
const selectedExample =
|
||||
examples.find((example) => example.key === exampleState.key) ?? examples[0];
|
||||
|
||||
if (!selectedExample) {
|
||||
return <OpenAPIEmptyExample />;
|
||||
}
|
||||
|
||||
return selectedExample.body;
|
||||
}
|
||||
|
||||
return selectedItem.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state key for the response media types.
|
||||
*/
|
||||
function getResponseMediaTypeStateKey(blockKey: string | undefined) {
|
||||
return createStateKey('response-media-types', blockKey);
|
||||
}
|
||||
|
||||
function getResponseMediaTypeExamplesStateKey(blockKey: string | undefined) {
|
||||
return createStateKey('response-media-types-examples', blockKey);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import clsx from 'clsx';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||
import { useResponseExamplesState } from './OpenAPIResponseExampleContent';
|
||||
import { StaticSection } from './StaticSection';
|
||||
import type { OpenAPIClientContext } from './types';
|
||||
import { createStateKey, getStatusCodeClassName, getStatusCodeDefaultLabel } from './utils';
|
||||
|
||||
/**
|
||||
* Display an interactive response body.
|
||||
@@ -14,70 +19,85 @@ export function OpenAPIResponses(props: {
|
||||
}) {
|
||||
const { responses, context } = props;
|
||||
|
||||
const groups = Object.entries(responses).map(
|
||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||
const tabs = (() => {
|
||||
// If there is no content, but there are headers, we need to show the headers
|
||||
if (
|
||||
(!response.content || !Object.keys(response.content).length) &&
|
||||
response.headers &&
|
||||
Object.keys(response.headers).length
|
||||
) {
|
||||
return [
|
||||
{
|
||||
key: 'default',
|
||||
label: '',
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={{}}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return Object.entries(response.content ?? {}).map(([contentType, mediaType]) => ({
|
||||
key: contentType,
|
||||
label: contentType,
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={mediaType}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
})();
|
||||
|
||||
const description = response.description;
|
||||
|
||||
return {
|
||||
key: statusCode,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content">
|
||||
<span
|
||||
className={clsx(
|
||||
'openapi-statuscode',
|
||||
`openapi-statuscode-${getStatusCodeClassName(statusCode)}`
|
||||
)}
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
{description ? (
|
||||
<Markdown
|
||||
source={description}
|
||||
className="openapi-response-description"
|
||||
/>
|
||||
) : (
|
||||
getStatusCodeDefaultLabel(statusCode)
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
tabs,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
|
||||
|
||||
return (
|
||||
<StaticSection header="Responses" className="openapi-responses">
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={Object.entries(responses).map(
|
||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||
const tabs = (() => {
|
||||
// If there is no content, but there are headers, we need to show the headers
|
||||
if (
|
||||
(!response.content || !Object.keys(response.content).length) &&
|
||||
response.headers &&
|
||||
Object.keys(response.headers).length
|
||||
) {
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={{}}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return Object.entries(response.content ?? {}).map(
|
||||
([contentType, mediaType]) => ({
|
||||
id: contentType,
|
||||
label: contentType,
|
||||
body: (
|
||||
<OpenAPIResponse
|
||||
response={response}
|
||||
mediaType={mediaType}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
||||
const description = response.description;
|
||||
|
||||
return {
|
||||
id: statusCode,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content">
|
||||
<span className="openapi-response-statuscode">
|
||||
{statusCode}
|
||||
</span>
|
||||
{description ? (
|
||||
<Markdown
|
||||
source={description}
|
||||
className="openapi-response-description"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
tabs,
|
||||
};
|
||||
}
|
||||
)}
|
||||
expandedKeys={state.key ? new Set([state.key]) : new Set()}
|
||||
onExpandedChange={(keys) => {
|
||||
const key = keys.values().next().value ?? null;
|
||||
state.setKey(key);
|
||||
}}
|
||||
groups={groups}
|
||||
selectStateKey={createStateKey('response-media-types', context.blockKey)}
|
||||
/>
|
||||
</StaticSection>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,10 @@ function OpenAPISchemaProperty(props: {
|
||||
const properties = getSchemaProperties(schema);
|
||||
if (properties?.length) {
|
||||
return (
|
||||
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
|
||||
<OpenAPIDisclosure
|
||||
icon={context.icons.plus}
|
||||
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
|
||||
>
|
||||
<OpenAPISchemaProperties
|
||||
properties={properties}
|
||||
circularRefs={circularRefs}
|
||||
@@ -208,7 +211,10 @@ function OpenAPISchemaAlternative(props: {
|
||||
{description ? (
|
||||
<Markdown source={description} className="openapi-schema-description" />
|
||||
) : null}
|
||||
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
|
||||
<OpenAPIDisclosure
|
||||
icon={context.icons.plus}
|
||||
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
|
||||
>
|
||||
{properties?.length ? (
|
||||
<OpenAPISchemaProperties
|
||||
properties={properties}
|
||||
@@ -502,19 +508,21 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
||||
return type;
|
||||
}
|
||||
|
||||
function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string {
|
||||
function getDisclosureLabel(schema: OpenAPIV3.SchemaObject, isExpanded: boolean) {
|
||||
let label: string;
|
||||
if (schema.type === 'array' && !!schema.items) {
|
||||
if (schema.items.oneOf) {
|
||||
return 'available items';
|
||||
label = 'available items';
|
||||
}
|
||||
|
||||
// Fallback to "child attributes" for enums and objects
|
||||
if (schema.items.enum || schema.items.type === 'object') {
|
||||
return 'child attributes';
|
||||
else if (schema.items.enum || schema.items.type === 'object') {
|
||||
label = 'child attributes';
|
||||
} else {
|
||||
label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
|
||||
}
|
||||
|
||||
return schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
|
||||
} else {
|
||||
label = schema.title || 'child attributes';
|
||||
}
|
||||
|
||||
return schema.title || 'child attributes';
|
||||
return `${isExpanded ? 'Hide' : 'Show'} ${label}`;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
OpenAPIOperationData,
|
||||
OpenAPISecurityWithRequired,
|
||||
} from './types';
|
||||
import { resolveDescription } from './utils';
|
||||
import { createStateKey, resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
* Present securities authorization that can be used for this operation.
|
||||
@@ -24,9 +24,11 @@ export function OpenAPISecurities(props: {
|
||||
return (
|
||||
<InteractiveSection
|
||||
header="Authorizations"
|
||||
stateKey={createStateKey('securities', context.blockKey)}
|
||||
toggeable
|
||||
defaultOpened={false}
|
||||
toggleIcon={context.icons.chevronRight}
|
||||
selectIcon={context.icons.chevronDown}
|
||||
className="openapi-securities"
|
||||
tabs={securities.map(([key, security]) => {
|
||||
const description = resolveDescription(security);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
type Key,
|
||||
@@ -13,38 +14,63 @@ import {
|
||||
type SelectProps,
|
||||
SelectValue,
|
||||
} from 'react-aria-components';
|
||||
import { useStore } from 'zustand';
|
||||
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||
|
||||
export type OpenAPISelectItem = {
|
||||
key: Key;
|
||||
label: string;
|
||||
label: string | React.ReactNode;
|
||||
};
|
||||
|
||||
interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectProps<T>, 'children'> {
|
||||
items: T[];
|
||||
children: React.ReactNode | ((item: T) => React.ReactNode);
|
||||
selectedKey?: Key;
|
||||
onChange?: (key: string | number) => void;
|
||||
placement?: PopoverProps['placement'];
|
||||
stateKey?: string;
|
||||
/**
|
||||
* Icon to display in the select button.
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function useSelectState(stateKey = 'select-state', initialKey?: Key) {
|
||||
const store = useStore(getOrCreateStoreByKey(stateKey, initialKey));
|
||||
return {
|
||||
key: store.key,
|
||||
setKey: useCallback((key: Key | null) => store.setKey(key), [store.setKey]),
|
||||
};
|
||||
}
|
||||
|
||||
export function OpenAPISelect<T extends OpenAPISelectItem>(props: OpenAPISelectProps<T>) {
|
||||
const { items, children, className, placement } = props;
|
||||
const {
|
||||
icon = '▼',
|
||||
items,
|
||||
children,
|
||||
className,
|
||||
placement,
|
||||
stateKey,
|
||||
selectedKey,
|
||||
onSelectionChange,
|
||||
} = props;
|
||||
|
||||
const state = useSelectState(stateKey, items[0]?.key);
|
||||
|
||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
||||
|
||||
return (
|
||||
<Select {...props} className={clsx('openapi-select', className)}>
|
||||
<Select
|
||||
aria-label="OpenAPI Select"
|
||||
{...props}
|
||||
selectedKey={selectedKey || selected?.key}
|
||||
onSelectionChange={(key) => {
|
||||
onSelectionChange?.(key);
|
||||
state.setKey(key);
|
||||
}}
|
||||
className={clsx('openapi-select', className)}
|
||||
>
|
||||
<Button>
|
||||
<SelectValue />
|
||||
<span aria-hidden="true">
|
||||
<svg
|
||||
className="gb-icon"
|
||||
style={{
|
||||
maskImage:
|
||||
"url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/chevron-down.svg?v=2&token=a463935e93')",
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'center center',
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{icon}
|
||||
</Button>
|
||||
<Popover placement={placement} className="openapi-select-popover">
|
||||
<ListBox className="openapi-select-listbox" items={items}>
|
||||
|
||||
@@ -42,6 +42,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
||||
key="body"
|
||||
requestBody={operation.requestBody}
|
||||
context={context}
|
||||
data={data}
|
||||
/>
|
||||
) : null}
|
||||
{operation.responses ? (
|
||||
|
||||
@@ -42,18 +42,50 @@ export const SectionBody = forwardRef(function SectionBody(
|
||||
);
|
||||
});
|
||||
|
||||
export function SectionFooter(props: ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'openapi-section-footer',
|
||||
props.className && `${props.className}-footer`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SectionFooterContent(props: ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={clsx(
|
||||
'openapi-section-footer-content',
|
||||
props.className && `${props.className}-footer-content`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StaticSection(props: {
|
||||
className: string;
|
||||
header: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const { className, header, children } = props;
|
||||
const { className, header, children, footer } = props;
|
||||
return (
|
||||
<Section className={className}>
|
||||
<SectionHeader className={className}>
|
||||
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
||||
</SectionHeader>
|
||||
{header ? (
|
||||
<SectionHeader className={className}>
|
||||
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
||||
</SectionHeader>
|
||||
) : null}
|
||||
<SectionBody className={className}>{children}</SectionBody>
|
||||
{footer ? (
|
||||
<SectionFooter className={className}>
|
||||
<SectionFooterContent className={className}>{footer}</SectionFooterContent>
|
||||
</SectionFooter>
|
||||
) : null}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,52 @@ export interface CodeSampleGenerator {
|
||||
}
|
||||
|
||||
export const codeSampleGenerators: CodeSampleGenerator[] = [
|
||||
{
|
||||
id: 'http',
|
||||
label: 'HTTP',
|
||||
syntax: 'bash',
|
||||
generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
|
||||
const { host, path } = parseHostAndPath(url);
|
||||
|
||||
if (body) {
|
||||
// if we had a body add a content length header
|
||||
const bodyContent = body ? stringifyOpenAPI(body) : '';
|
||||
// handle unicode chars with a text encoder
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const bodyString = BodyGenerators.getHTTPBody(body, headers);
|
||||
|
||||
if (bodyString) {
|
||||
body = bodyString;
|
||||
}
|
||||
|
||||
headers = {
|
||||
...headers,
|
||||
'Content-Length': encoder.encode(bodyContent).length.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!headers.hasOwnProperty('Accept')) {
|
||||
headers.Accept = '*/*';
|
||||
}
|
||||
|
||||
const headerString = headers
|
||||
? `${Object.entries(headers)
|
||||
.map(([key, value]) =>
|
||||
key.toLowerCase() !== 'host' ? `${key}: ${value}` : ''
|
||||
)
|
||||
.join('\n')}\n`
|
||||
: '';
|
||||
|
||||
const bodyString = body ? `\n${body}` : '';
|
||||
|
||||
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
|
||||
Host: ${host}
|
||||
${headerString}${bodyString}`;
|
||||
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'curl',
|
||||
label: 'cURL',
|
||||
@@ -140,52 +186,6 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
||||
return code;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'http',
|
||||
label: 'HTTP',
|
||||
syntax: 'bash',
|
||||
generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
|
||||
const { host, path } = parseHostAndPath(url);
|
||||
|
||||
if (body) {
|
||||
// if we had a body add a content length header
|
||||
const bodyContent = body ? stringifyOpenAPI(body) : '';
|
||||
// handle unicode chars with a text encoder
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const bodyString = BodyGenerators.getHTTPBody(body, headers);
|
||||
|
||||
if (bodyString) {
|
||||
body = bodyString;
|
||||
}
|
||||
|
||||
headers = {
|
||||
...headers,
|
||||
'Content-Length': encoder.encode(bodyContent).length.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!headers.hasOwnProperty('Accept')) {
|
||||
headers.Accept = '*/*';
|
||||
}
|
||||
|
||||
const headerString = headers
|
||||
? `${Object.entries(headers)
|
||||
.map(([key, value]) =>
|
||||
key.toLowerCase() !== 'host' ? `${key}: ${value}` : ''
|
||||
)
|
||||
.join('\n')}\n`
|
||||
: '';
|
||||
|
||||
const bodyString = body ? `\n${body}` : '';
|
||||
|
||||
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
|
||||
Host: ${host}
|
||||
${headerString}${bodyString}`;
|
||||
|
||||
return httpRequest;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function indent(code: string, spaces: number) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { createStore } from 'zustand';
|
||||
|
||||
type Key = string | number;
|
||||
@@ -10,7 +8,7 @@ type State = {
|
||||
|
||||
type Actions = { setKey: (key: Key | null) => void };
|
||||
|
||||
type Store = State & Actions;
|
||||
export type Store = State & Actions;
|
||||
|
||||
const createStateStore = (initial?: Key) => {
|
||||
return createStore<Store>()((set) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { OpenAPISchema } from '@gitbook/openapi-parser';
|
||||
import clsx from 'clsx';
|
||||
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
|
||||
import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
|
||||
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
|
||||
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
|
||||
import { Section, SectionBody, StaticSection } from '../StaticSection';
|
||||
@@ -72,33 +72,22 @@ export function OpenAPISchemas(props: {
|
||||
// If there are multiple schemas, we use a disclosure group to show them all.
|
||||
return (
|
||||
<div className={clsx('openapi-schemas', className)}>
|
||||
<OpenAPIDisclosureGroup
|
||||
allowsMultipleExpanded
|
||||
icon={context.icons.chevronRight}
|
||||
groups={schemas.map(({ name, schema }) => ({
|
||||
id: name,
|
||||
label: (
|
||||
<div className="openapi-response-tab-content" key={`model-${name}`}>
|
||||
<span className="openapi-response-statuscode">{name}</span>
|
||||
</div>
|
||||
),
|
||||
tabs: [
|
||||
{
|
||||
id: 'model',
|
||||
body: (
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema
|
||||
schema={schema}
|
||||
context={clientContext}
|
||||
/>
|
||||
</SectionBody>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
],
|
||||
}))}
|
||||
/>
|
||||
{schemas.map(({ name, schema }) => {
|
||||
return (
|
||||
<OpenAPIDisclosure
|
||||
className="openapi-schemas-disclosure"
|
||||
key={name}
|
||||
icon={context.icons.chevronRight}
|
||||
label={name}
|
||||
>
|
||||
<Section className="openapi-section-schemas">
|
||||
<SectionBody>
|
||||
<OpenAPIRootSchema schema={schema} context={clientContext} />
|
||||
</SectionBody>
|
||||
</Section>
|
||||
</OpenAPIDisclosure>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,3 +149,65 @@ function shouldDisplayExample(schema: OpenAPIV3.SchemaObject): boolean {
|
||||
Object.keys(schema.example).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name for a status code.
|
||||
* 1xx: informational
|
||||
* 2xx: success
|
||||
* 3xx: redirect
|
||||
* 4xx, 5xx: error
|
||||
*/
|
||||
export function getStatusCodeClassName(statusCode: number | string): string {
|
||||
const category = getStatusCodeCategory(statusCode);
|
||||
switch (category) {
|
||||
case 1:
|
||||
return 'informational';
|
||||
case 2:
|
||||
return 'success';
|
||||
case 3:
|
||||
return 'redirect';
|
||||
case 4:
|
||||
case 5:
|
||||
return 'error';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a default label for a status code.
|
||||
* This is used when there is no label provided in the OpenAPI spec.
|
||||
* 1xx: Information
|
||||
* 2xx: Success
|
||||
* 3xx: Redirect
|
||||
* 4xx, 5xx: Error
|
||||
*/
|
||||
export function getStatusCodeDefaultLabel(statusCode: number | string): string {
|
||||
const category = getStatusCodeCategory(statusCode);
|
||||
switch (category) {
|
||||
case 1:
|
||||
return 'Information';
|
||||
case 2:
|
||||
return 'Success';
|
||||
case 3:
|
||||
return 'Redirect';
|
||||
case 4:
|
||||
case 5:
|
||||
return 'Error';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusCodeCategory(statusCode: number | string): number | string {
|
||||
const code = typeof statusCode === 'string' ? Number.parseInt(statusCode, 10) : statusCode;
|
||||
|
||||
if (Number.isNaN(code) || code < 100 || code >= 600) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Determine the category of the status code based on the first digit
|
||||
const category = Math.floor(code / 100);
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user