mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 10:03:31 +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;
|
@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 {
|
.openapi-columns {
|
||||||
@apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch;
|
@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;
|
@apply font-semibold font-mono truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-description.openapi-markdown {
|
.openapi-description.openapi-markdown,
|
||||||
@apply prose-sm text-[0.938rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-description.openapi-markdown code {
|
.openapi-description.openapi-markdown code {
|
||||||
@apply prose-sm;
|
@apply prose-sm;
|
||||||
}
|
}
|
||||||
@@ -89,21 +90,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Method Tags */
|
/* Method Tags */
|
||||||
.openapi-method {
|
.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-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 {
|
.openapi-method-get,
|
||||||
/* @apply bg-[hsl(215,54%,86%)] dark:bg-[hsla(215,54%,45%,0.24)] dark:text-[hsl(215,54%,86%)]; */
|
.openapi-statuscode-success {
|
||||||
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-method-post {
|
.openapi-method-post,
|
||||||
/* @apply bg-[hsl(120,25%,80%)] dark:bg-[hsla(120,54%,32%,0.24)] dark:text-[hsl(120,25%,80%)]; */
|
.openapi-statuscode-redirection {
|
||||||
@apply bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100;
|
@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;
|
@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;
|
@apply bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-method-delete {
|
.openapi-method-delete,
|
||||||
@apply bg-pomegranate-100 text-pomegranate-800 dark:bg-pomegranate-900 dark:text-pomegranate-100;
|
.openapi-statuscode-error {
|
||||||
|
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-method-head,
|
.openapi-method-head,
|
||||||
@@ -140,7 +144,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-column-preview-body {
|
.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 {
|
.openapi-column-preview pre {
|
||||||
@@ -157,7 +161,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root-description {
|
.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 {
|
.openapi-schema-properties {
|
||||||
@@ -360,7 +364,7 @@
|
|||||||
|
|
||||||
/* Responses */
|
/* Responses */
|
||||||
.openapi-responses-header {
|
.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 {
|
.openapi-responses-header-content {
|
||||||
@@ -368,7 +372,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-response-tab-content {
|
.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 {
|
.openapi-response-description.openapi-markdown {
|
||||||
@@ -383,10 +388,6 @@
|
|||||||
@apply truncate max-w-full inline pr-1;
|
@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 {
|
.openapi-response-content-type {
|
||||||
@apply text-xs text-tint-8 ml-auto shrink-0;
|
@apply text-xs text-tint-8 ml-auto shrink-0;
|
||||||
}
|
}
|
||||||
@@ -416,15 +417,23 @@
|
|||||||
|
|
||||||
/* Code Sample */
|
/* Code Sample */
|
||||||
.openapi-codesample {
|
.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 {
|
.openapi-codesample-header {
|
||||||
@apply flex flex-row items-center;
|
@apply flex flex-row items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-codesample-header .openapi-select > button {
|
.openapi-response-media-types-examples-footer-content {
|
||||||
@apply border-none;
|
@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 {
|
.openapi-codesample-header-content {
|
||||||
@@ -432,7 +441,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-codesample-header-content .openapi-path {
|
.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 {
|
.openapi-codesample-header-content .openapi-path .openapi-path-variable {
|
||||||
@@ -463,7 +472,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-path .openapi-method {
|
.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 {
|
.openapi-path-title {
|
||||||
@@ -494,7 +503,7 @@
|
|||||||
|
|
||||||
/* Panel */
|
/* Panel */
|
||||||
.openapi-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 {
|
.openapi-panel-heading {
|
||||||
@@ -516,13 +525,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Example */
|
/* 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 {
|
.openapi-example-empty {
|
||||||
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
|
@apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Common Elements */
|
/* Common Elements */
|
||||||
.openapi-select {
|
.openapi-select {
|
||||||
/* unstyled */
|
@apply w-auto max-w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent react-aria popover from setting overflow:auto on body */
|
/* Prevent react-aria popover from setting overflow:auto on body */
|
||||||
@@ -531,24 +568,32 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button {
|
.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;
|
@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 {
|
.openapi-select > button > span.react-aria-SelectValue {
|
||||||
@apply shrink truncate;
|
@apply shrink truncate flex items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button > .gb-icon {
|
.openapi-select > button > span.react-aria-SelectValue span:not(.openapi-statuscode) {
|
||||||
@apply shrink-0;
|
@apply truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select > button svg {
|
.openapi-select > button .openapi-markdown {
|
||||||
@apply size-2.5;
|
@apply *:leading-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-select > button .gb-icon {
|
||||||
|
@apply size-2.5 shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select-popover {
|
.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;
|
@apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,11 +606,17 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-select-item {
|
.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;
|
@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 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 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;
|
@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;
|
@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 {
|
.openapi-section-toggle {
|
||||||
@apply text-tint-subtle contrast-more:text-tint-strong;
|
@apply text-tint-subtle contrast-more:text-tint-strong;
|
||||||
}
|
}
|
||||||
@@ -621,6 +680,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs */
|
/* Tabs */
|
||||||
|
.openapi-panel-header,
|
||||||
.openapi-tabs-list {
|
.openapi-tabs-list {
|
||||||
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
|
@apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -632,18 +692,17 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-tabs-tab[aria-selected="true"] {
|
.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-tabs-panel {
|
||||||
.openapi-codesample-panel {
|
|
||||||
@apply flex-1 text-sm relative focus-visible:outline-none;
|
@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;
|
@apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disclosure group */
|
/* Disclosure group */
|
||||||
.openapi-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 {
|
.openapi-disclosure-group-header {
|
||||||
@@ -651,16 +710,11 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group-trigger {
|
.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 {
|
.openapi-disclosure-group-trigger:disabled {
|
||||||
@apply cursor-default;
|
@apply cursor-default hover:bg-inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.openapi-disclosure-group:only-child,
|
|
||||||
.openapi-disclosure-group:last-child {
|
|
||||||
@apply border-b-0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon {
|
.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon {
|
||||||
@@ -680,28 +734,42 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group-panel {
|
.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 {
|
.openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg {
|
||||||
@apply rotate-90;
|
@apply rotate-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype {
|
.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype,
|
||||||
@apply opacity-11 flex;
|
.openapi-disclosure-group-mediatype:has(> .openapi-select[data-open="true"]) {
|
||||||
|
@apply opacity-11 visible flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-group-mediatype {
|
.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 {
|
.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 */
|
/* 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 {
|
.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 {
|
.openapi-disclosure-trigger span {
|
||||||
@@ -717,7 +785,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-trigger[aria-expanded="true"] {
|
.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"] {
|
.openapi-disclosure-trigger[aria-expanded="false"] {
|
||||||
@@ -725,7 +793,7 @@ body:has(.openapi-select-popover) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openapi-disclosure-panel[aria-hidden="false"] {
|
.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 {
|
.openapi-disclosure-panel .openapi-schema {
|
||||||
@@ -745,17 +813,13 @@ body:has(.openapi-select-popover) {
|
|||||||
@apply space-y-2.5;
|
@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-properties > .openapi-schema,
|
||||||
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
|
.openapi-section-schemas > .openapi-section-body > .openapi-schema-root {
|
||||||
@apply p-2.5;
|
@apply p-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-tooltip {
|
.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 {
|
.openapi-tooltip svg {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
|||||||
>
|
>
|
||||||
{section.tag ? (
|
{section.tag ? (
|
||||||
<span
|
<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}
|
{section.tag}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
|
||||||
import { useDisclosureState } from 'react-stately';
|
import { useDisclosureState } from 'react-stately';
|
||||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||||
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
|
import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection';
|
||||||
|
|
||||||
interface InteractiveSectionTab {
|
interface InteractiveSectionTab {
|
||||||
@@ -35,6 +35,10 @@ export function InteractiveSection(props: {
|
|||||||
header?: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
/** Children to display within the container */
|
/** Children to display within the container */
|
||||||
overlay?: React.ReactNode;
|
overlay?: React.ReactNode;
|
||||||
|
/** State key to use with a store */
|
||||||
|
stateKey?: string;
|
||||||
|
/** Icon for the tabs select */
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -46,12 +50,9 @@ export function InteractiveSection(props: {
|
|||||||
header,
|
header,
|
||||||
overlay,
|
overlay,
|
||||||
toggleIcon = '▶',
|
toggleIcon = '▶',
|
||||||
|
selectIcon,
|
||||||
|
stateKey = 'interactive-section',
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [selectedTabKey, setSelectedTab] = useState(defaultTab);
|
|
||||||
const selectedTab: InteractiveSectionTab | undefined =
|
|
||||||
tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0];
|
|
||||||
|
|
||||||
const state = useDisclosureState({
|
const state = useDisclosureState({
|
||||||
defaultExpanded: defaultOpened,
|
defaultExpanded: defaultOpened,
|
||||||
});
|
});
|
||||||
@@ -60,6 +61,10 @@ export function InteractiveSection(props: {
|
|||||||
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
|
||||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
|
const store = useSelectState(stateKey, defaultTab);
|
||||||
|
|
||||||
|
const selectedTab: InteractiveSectionTab | undefined =
|
||||||
|
tabs.find((tab) => tab.key === store.key) ?? tabs[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
@@ -108,16 +113,12 @@ export function InteractiveSection(props: {
|
|||||||
>
|
>
|
||||||
{tabs.length > 1 ? (
|
{tabs.length > 1 ? (
|
||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
className={clsx(
|
stateKey={stateKey}
|
||||||
'openapi-section-select',
|
|
||||||
`${className}-tabs-select`
|
|
||||||
)}
|
|
||||||
items={tabs}
|
items={tabs}
|
||||||
selectedKey={selectedTab?.key ?? ''}
|
onSelectionChange={() => {
|
||||||
onSelectionChange={(key) => {
|
|
||||||
setSelectedTab(String(key));
|
|
||||||
state.expand();
|
state.expand();
|
||||||
}}
|
}}
|
||||||
|
icon={selectIcon}
|
||||||
placement="bottom end"
|
placement="bottom end"
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ function OpenAPICodeSampleFooter(props: {
|
|||||||
method={data.method}
|
method={data.method}
|
||||||
path={data.path}
|
path={data.path}
|
||||||
renderers={renderers}
|
renderers={renderers}
|
||||||
|
selectIcon={context.icons.chevronDown}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
|
|||||||
@@ -1,70 +1,41 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
import type { MediaTypeRenderer } from './OpenAPICodeSample';
|
||||||
import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect';
|
import { OpenAPISelect, OpenAPISelectItem, useSelectState } 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]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OpenAPIMediaTypeExamplesSelector(props: {
|
export function OpenAPIMediaTypeExamplesSelector(props: {
|
||||||
method: string;
|
method: string;
|
||||||
path: string;
|
path: string;
|
||||||
renderers: MediaTypeRenderer[];
|
renderers: MediaTypeRenderer[];
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { method, path, renderers } = props;
|
const { method, path, renderers, selectIcon } = props;
|
||||||
if (!renderers[0]) {
|
if (!renderers[0]) {
|
||||||
throw new Error('No renderers provided');
|
throw new Error('No renderers provided');
|
||||||
}
|
}
|
||||||
const state = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
const stateKey = `media-type-${method}-${path}`;
|
||||||
const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0];
|
const state = useSelectState(stateKey, renderers[0].mediaType);
|
||||||
|
const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-codesample-selectors">
|
<div className="openapi-codesample-selectors">
|
||||||
<MediaTypeSelector state={state} renderers={renderers} />
|
<MediaTypeSelector selectIcon={selectIcon} stateKey={stateKey} renderers={renderers} />
|
||||||
<ExamplesSelector method={method} path={path} renderer={selected} />
|
<ExamplesSelector
|
||||||
|
selectIcon={selectIcon}
|
||||||
|
method={method}
|
||||||
|
path={path}
|
||||||
|
renderer={selected}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MediaTypeSelector(props: {
|
function MediaTypeSelector(props: {
|
||||||
state: MediaTypeState;
|
stateKey: string;
|
||||||
renderers: MediaTypeRenderer[];
|
renderers: MediaTypeRenderer[];
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { renderers, state } = props;
|
const { renderers, stateKey, selectIcon } = props;
|
||||||
|
|
||||||
if (renderers.length < 2) {
|
if (renderers.length < 2) {
|
||||||
return null;
|
return null;
|
||||||
@@ -78,12 +49,12 @@ function MediaTypeSelector(props: {
|
|||||||
return (
|
return (
|
||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
className={clsx('openapi-select')}
|
className={clsx('openapi-select')}
|
||||||
selectedKey={state.mediaType}
|
|
||||||
items={renderers.map((renderer) => ({
|
items={renderers.map((renderer) => ({
|
||||||
key: renderer.mediaType,
|
key: renderer.mediaType,
|
||||||
label: renderer.mediaType,
|
label: renderer.mediaType,
|
||||||
}))}
|
}))}
|
||||||
onSelectionChange={(e) => state.setMediaType(String(e))}
|
icon={selectIcon}
|
||||||
|
stateKey={stateKey}
|
||||||
placement="bottom start"
|
placement="bottom start"
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
@@ -99,9 +70,9 @@ function ExamplesSelector(props: {
|
|||||||
method: string;
|
method: string;
|
||||||
path: string;
|
path: string;
|
||||||
renderer: MediaTypeRenderer;
|
renderer: MediaTypeRenderer;
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { method, path, renderer } = props;
|
const { method, path, renderer, selectIcon } = props;
|
||||||
const state = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
|
||||||
if (renderer.examples.length < 2) {
|
if (renderer.examples.length < 2) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -114,8 +85,8 @@ function ExamplesSelector(props: {
|
|||||||
return (
|
return (
|
||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
items={items}
|
items={items}
|
||||||
selectedKey={state.index}
|
icon={selectIcon}
|
||||||
onSelectionChange={(e) => state.setIndex(Number(e))}
|
stateKey={`media-type-sample-${renderer.mediaType}-${method}-${path}`}
|
||||||
placement="bottom start"
|
placement="bottom start"
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
@@ -136,9 +107,9 @@ export function OpenAPIMediaTypeExamplesBody(props: {
|
|||||||
if (!renderers[0]) {
|
if (!renderers[0]) {
|
||||||
throw new Error('No renderers provided');
|
throw new Error('No renderers provided');
|
||||||
}
|
}
|
||||||
const mediaTypeState = useMediaTypeState({ method, path }, renderers[0].mediaType);
|
|
||||||
const selected =
|
const mediaTypeState = useSelectState(`media-type-${method}-${path}`, renderers[0].mediaType);
|
||||||
renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0];
|
const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0];
|
||||||
if (selected.examples.length === 0) {
|
if (selected.examples.length === 0) {
|
||||||
return selected.element;
|
return selected.element;
|
||||||
}
|
}
|
||||||
@@ -147,10 +118,13 @@ export function OpenAPIMediaTypeExamplesBody(props: {
|
|||||||
|
|
||||||
function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) {
|
function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) {
|
||||||
const { method, path, renderer } = props;
|
const { method, path, renderer } = props;
|
||||||
const exampleState = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType);
|
const exampleState = useSelectState(
|
||||||
const example = renderer.examples[exampleState.index] ?? renderer.examples[0];
|
`media-type-sample-${renderer.mediaType}-${method}-${path}`,
|
||||||
|
renderer.mediaType
|
||||||
|
);
|
||||||
|
const example = renderer.examples[Number(exampleState.key)] ?? renderer.examples[0];
|
||||||
if (!example) {
|
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;
|
return example.element;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,25 +22,21 @@ type CodeSampleItem = OpenAPISelectItem & {
|
|||||||
footer?: React.ReactNode;
|
footer?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OpenAPICodeSampleHeader(props: {
|
function OpenAPICodeSampleHeader(props: {
|
||||||
items: CodeSampleItem[];
|
items: CodeSampleItem[];
|
||||||
data: OpenAPIOperationData;
|
data: OpenAPIOperationData;
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { data, items } = props;
|
const { data, items, selectIcon } = props;
|
||||||
|
|
||||||
const state = useCodeSampleState(items[0]?.key ?? '');
|
|
||||||
const selected = items.find((item) => item.key === state.key) || items[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenAPIPath canCopy={false} withServer={false} data={data} />
|
<OpenAPIPath canCopy={false} withServer={false} data={data} />
|
||||||
{items.length > 1 ? (
|
{items.length > 1 ? (
|
||||||
<OpenAPISelect
|
<OpenAPISelect
|
||||||
selectedKey={selected?.key}
|
icon={selectIcon}
|
||||||
onSelectionChange={(key) => {
|
|
||||||
state.setKey(key);
|
|
||||||
}}
|
|
||||||
items={items}
|
items={items}
|
||||||
|
stateKey="codesample"
|
||||||
placement="bottom end"
|
placement="bottom end"
|
||||||
>
|
>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
@@ -59,8 +55,9 @@ export function OpenAPICodeSampleHeader(props: {
|
|||||||
export function OpenAPICodeSampleBody(props: {
|
export function OpenAPICodeSampleBody(props: {
|
||||||
items: CodeSampleItem[];
|
items: CodeSampleItem[];
|
||||||
data: OpenAPIOperationData;
|
data: OpenAPIOperationData;
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { items, data } = props;
|
const { items, data, selectIcon } = props;
|
||||||
if (!items[0]) {
|
if (!items[0]) {
|
||||||
throw new Error('No items provided');
|
throw new Error('No items provided');
|
||||||
}
|
}
|
||||||
@@ -75,7 +72,7 @@ export function OpenAPICodeSampleBody(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StaticSection
|
<StaticSection
|
||||||
header={<OpenAPICodeSampleHeader data={data} items={items} />}
|
header={<OpenAPICodeSampleHeader selectIcon={selectIcon} data={data} items={items} />}
|
||||||
className="openapi-codesample"
|
className="openapi-codesample"
|
||||||
>
|
>
|
||||||
<div id={selected.key as string} className="openapi-codesample-panel">
|
<div id={selected.key as string} className="openapi-codesample-panel">
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components';
|
import { Button, Disclosure, DisclosurePanel } from 'react-aria-components';
|
||||||
import type { OpenAPIClientContext } from './types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an interactive OpenAPI disclosure.
|
* Display an interactive OpenAPI disclosure.
|
||||||
*/
|
*/
|
||||||
export function OpenAPIDisclosure(props: {
|
export function OpenAPIDisclosure(props: {
|
||||||
context: OpenAPIClientContext;
|
icon: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
label: string;
|
label: string | ((isExpanded: boolean) => string);
|
||||||
|
className?: string;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { context, children, label } = props;
|
const { icon, children, label, className } = props;
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
className="openapi-disclosure"
|
className={clsx('openapi-disclosure', className)}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onExpandedChange={setIsExpanded}
|
onExpandedChange={setIsExpanded}
|
||||||
>
|
>
|
||||||
<Heading>
|
|
||||||
<Button
|
<Button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
className="openapi-disclosure-trigger"
|
className="openapi-disclosure-trigger"
|
||||||
@@ -30,12 +31,9 @@ export function OpenAPIDisclosure(props: {
|
|||||||
: 'none',
|
: 'none',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{context.icons.plus}
|
{icon}
|
||||||
<span>
|
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
|
||||||
{isExpanded ? 'Hide' : 'Show'} {label}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</Heading>
|
|
||||||
<DisclosurePanel className="openapi-disclosure-panel">
|
<DisclosurePanel className="openapi-disclosure-panel">
|
||||||
{isExpanded ? children : null}
|
{isExpanded ? children : null}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'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 { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
|
||||||
import {
|
import {
|
||||||
type DisclosureGroupProps,
|
type DisclosureGroupProps,
|
||||||
@@ -8,18 +8,23 @@ import {
|
|||||||
useDisclosureGroupState,
|
useDisclosureGroupState,
|
||||||
useDisclosureState,
|
useDisclosureState,
|
||||||
} from 'react-stately';
|
} from 'react-stately';
|
||||||
|
import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groups: TDisclosureGroup[];
|
groups: TDisclosureGroup[];
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
/** State key to use with a store */
|
||||||
|
selectStateKey?: string;
|
||||||
|
/** Icon to display for the select */
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TDisclosureGroup = {
|
type TDisclosureGroup = {
|
||||||
id: string;
|
key: string;
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
tabs?: {
|
tabs?: {
|
||||||
id: string;
|
key: string;
|
||||||
label?: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
body?: React.ReactNode;
|
body?: React.ReactNode;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -30,24 +35,35 @@ const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(n
|
|||||||
* Display an interactive OpenAPI disclosure group.
|
* Display an interactive OpenAPI disclosure group.
|
||||||
*/
|
*/
|
||||||
export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
|
export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
|
||||||
const { icon, groups } = props;
|
const { icon, groups, selectStateKey, selectIcon } = props;
|
||||||
|
|
||||||
const state = useDisclosureGroupState(props);
|
const state = useDisclosureGroupState(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisclosureGroupStateContext.Provider value={state}>
|
<DisclosureGroupStateContext.Provider value={state}>
|
||||||
{groups.map((group) => (
|
{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>
|
</DisclosureGroupStateContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) {
|
function DisclosureItem(props: {
|
||||||
const { icon, group } = props;
|
group: TDisclosureGroup;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
selectStateKey?: string;
|
||||||
|
selectIcon?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { icon, group, selectStateKey, selectIcon } = props;
|
||||||
|
|
||||||
const defaultId = useId();
|
const defaultId = useId();
|
||||||
const id = group.id || defaultId;
|
const id = group.key || defaultId;
|
||||||
const groupState = useContext(DisclosureGroupStateContext);
|
const groupState = useContext(DisclosureGroupStateContext);
|
||||||
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
const isExpanded = groupState?.expandedKeys.has(id) || false;
|
||||||
const state = useDisclosureState({
|
const state = useDisclosureState({
|
||||||
@@ -74,9 +90,9 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
const { buttonProps } = useButton(triggerProps, triggerRef);
|
const { buttonProps } = useButton(triggerProps, triggerRef);
|
||||||
const { isFocusVisible, focusProps } = useFocusRing();
|
const { isFocusVisible, focusProps } = useFocusRing();
|
||||||
|
|
||||||
const defaultTab = group.tabs?.[0]?.id || '';
|
const defaultTab = group.tabs?.[0]?.key || '';
|
||||||
const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
|
const store = useSelectState(selectStateKey, defaultTab);
|
||||||
const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
|
const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
|
<div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
|
||||||
@@ -104,23 +120,25 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
|
|||||||
{group.label}
|
{group.label}
|
||||||
</button>
|
</button>
|
||||||
{group.tabs ? (
|
{group.tabs ? (
|
||||||
<div className="openapi-disclosure-group-mediatype">
|
<div
|
||||||
|
className="openapi-disclosure-group-mediatype"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
{group.tabs?.length > 1 ? (
|
{group.tabs?.length > 1 ? (
|
||||||
<select
|
<OpenAPISelect
|
||||||
className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
|
icon={selectIcon}
|
||||||
onClick={(event) => event.stopPropagation()}
|
stateKey={selectStateKey}
|
||||||
value={selectedTab?.id}
|
onSelectionChange={() => {
|
||||||
onChange={(event) => {
|
|
||||||
setSelectedTabKey(event.target.value);
|
|
||||||
state.expand();
|
state.expand();
|
||||||
}}
|
}}
|
||||||
|
items={group.tabs}
|
||||||
>
|
>
|
||||||
{group.tabs.map((tab) => (
|
{group.tabs.map((tab) => (
|
||||||
<option key={tab.id} value={tab.id}>
|
<OpenAPISelectItem key={tab.key} id={tab.key} value={tab}>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</option>
|
</OpenAPISelectItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</OpenAPISelect>
|
||||||
) : group.tabs[0]?.label ? (
|
) : group.tabs[0]?.label ? (
|
||||||
<span>{group.tabs[0].label}</span>
|
<span>{group.tabs[0].label}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
|
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
|
||||||
import type { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
|
||||||
import { checkIsReference } from './utils';
|
import { checkIsReference } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,8 +10,10 @@ import { checkIsReference } from './utils';
|
|||||||
export function OpenAPIRequestBody(props: {
|
export function OpenAPIRequestBody(props: {
|
||||||
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
|
requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
|
||||||
context: OpenAPIClientContext;
|
context: OpenAPIClientContext;
|
||||||
|
data: OpenAPIOperationData;
|
||||||
}) {
|
}) {
|
||||||
const { requestBody, context } = props;
|
const { requestBody, context, data } = props;
|
||||||
|
const { method, path } = data;
|
||||||
|
|
||||||
if (checkIsReference(requestBody)) {
|
if (checkIsReference(requestBody)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -21,6 +23,8 @@ export function OpenAPIRequestBody(props: {
|
|||||||
<InteractiveSection
|
<InteractiveSection
|
||||||
header="Body"
|
header="Body"
|
||||||
className="openapi-requestbody"
|
className="openapi-requestbody"
|
||||||
|
stateKey={`media-type-${method}-${path}`}
|
||||||
|
selectIcon={context.icons.chevronDown}
|
||||||
tabs={Object.entries(requestBody.content ?? {}).map(
|
tabs={Object.entries(requestBody.content ?? {}).map(
|
||||||
([contentType, mediaTypeObject]) => {
|
([contentType, mediaTypeObject]) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function OpenAPIResponse(props: {
|
|||||||
return (
|
return (
|
||||||
<div className="openapi-response-body">
|
<div className="openapi-response-body">
|
||||||
{headers.length > 0 ? (
|
{headers.length > 0 ? (
|
||||||
<OpenAPIDisclosure context={context} label="Headers">
|
<OpenAPIDisclosure icon={context.icons.plus} label="Headers">
|
||||||
<OpenAPISchemaProperties
|
<OpenAPISchemaProperties
|
||||||
properties={headers.map(([name, header]) =>
|
properties={headers.map(([name, header]) =>
|
||||||
parameterToProperty({ name, ...header })
|
parameterToProperty({ name, ...header })
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import {
|
|||||||
getExampleFromReference,
|
getExampleFromReference,
|
||||||
getExamplesFromMediaTypeObject,
|
getExamplesFromMediaTypeObject,
|
||||||
} from './OpenAPIExample';
|
} from './OpenAPIExample';
|
||||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent';
|
||||||
import { StaticSection } from './StaticSection';
|
import { OpenAPIResponseMediaTypeContent } from './OpenAPIResponseMediaType';
|
||||||
import type { OpenAPIContext, OpenAPIOperationData } from './types';
|
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.
|
* Display an example of the response content.
|
||||||
@@ -45,11 +46,17 @@ export function OpenAPIResponseExample(props: {
|
|||||||
|
|
||||||
const tabs = responses.map(([key, responseObject]) => {
|
const tabs = responses.map(([key, responseObject]) => {
|
||||||
const description = resolveDescription(responseObject);
|
const description = resolveDescription(responseObject);
|
||||||
|
const label = description ? (
|
||||||
|
<Markdown source={description} />
|
||||||
|
) : (
|
||||||
|
getStatusCodeDefaultLabel(key)
|
||||||
|
);
|
||||||
|
|
||||||
if (checkIsReference(responseObject)) {
|
if (checkIsReference(responseObject)) {
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label,
|
||||||
|
statusCode: key,
|
||||||
body: (
|
body: (
|
||||||
<OpenAPIExample
|
<OpenAPIExample
|
||||||
example={getExampleFromReference(responseObject)}
|
example={getExampleFromReference(responseObject)}
|
||||||
@@ -57,24 +64,23 @@ export function OpenAPIResponseExample(props: {
|
|||||||
syntax="json"
|
syntax="json"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
footer: description ? <Markdown source={description} /> : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label,
|
||||||
|
statusCode: key,
|
||||||
body: <OpenAPIEmptyExample />,
|
body: <OpenAPIEmptyExample />,
|
||||||
footer: description ? <Markdown source={description} /> : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
label: key,
|
label,
|
||||||
|
statusCode: key,
|
||||||
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
body: <OpenAPIResponse context={context} content={responseObject.content} />,
|
||||||
footer: description ? <Markdown source={description} /> : undefined,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,11 +89,11 @@ export function OpenAPIResponseExample(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
|
<OpenAPIResponseExampleContent
|
||||||
<StaticSection header={<OpenAPITabsList />} className="openapi-panel">
|
selectIcon={context.icons.chevronDown}
|
||||||
<OpenAPITabsPanels />
|
blockKey={context.blockKey}
|
||||||
</StaticSection>
|
items={tabs}
|
||||||
</OpenAPITabs>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,42 +112,30 @@ function OpenAPIResponse(props: {
|
|||||||
throw new Error('One media type is required');
|
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 tabs = entries.map((entry) => {
|
||||||
const [mediaType, mediaTypeObject] = entry;
|
const [mediaType, mediaTypeObject] = entry;
|
||||||
return {
|
return {
|
||||||
key: mediaType,
|
key: mediaType,
|
||||||
label: mediaType,
|
label: mediaType,
|
||||||
body: (
|
body: <></>,
|
||||||
<OpenAPIResponseMediaType
|
examples: getExamples({
|
||||||
context={context}
|
mediaTypeObject,
|
||||||
mediaType={mediaType}
|
mediaType,
|
||||||
mediaTypeObject={mediaTypeObject}
|
context,
|
||||||
/>
|
}),
|
||||||
),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
|
<OpenAPIResponseMediaTypeContent
|
||||||
<StaticSection header={<OpenAPITabsList />} className="openapi-response-media-types">
|
selectIcon={context.icons.chevronDown}
|
||||||
<OpenAPITabsPanels />
|
blockKey={context.blockKey}
|
||||||
</StaticSection>
|
items={tabs}
|
||||||
</OpenAPITabs>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function OpenAPIResponseMediaType(props: {
|
function getExamples(props: {
|
||||||
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
mediaTypeObject: OpenAPIV3.MediaTypeObject;
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
context: OpenAPIContext;
|
context: OpenAPIContext;
|
||||||
@@ -149,23 +143,8 @@ function OpenAPIResponseMediaType(props: {
|
|||||||
const { mediaTypeObject, mediaType } = props;
|
const { mediaTypeObject, mediaType } = props;
|
||||||
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
|
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
|
||||||
const syntax = getSyntaxFromMediaType(mediaType);
|
const syntax = getSyntaxFromMediaType(mediaType);
|
||||||
const firstExample = examples[0];
|
|
||||||
|
|
||||||
if (!firstExample) {
|
return examples.map((example) => {
|
||||||
return <OpenAPIEmptyExample />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (examples.length === 1) {
|
|
||||||
return (
|
|
||||||
<OpenAPIExample
|
|
||||||
example={firstExample.example}
|
|
||||||
context={props.context}
|
|
||||||
syntax={syntax}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs = examples.map((example) => {
|
|
||||||
return {
|
return {
|
||||||
key: example.key,
|
key: example.key,
|
||||||
label: example.example.summary || 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 type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup';
|
||||||
import { OpenAPIResponse } from './OpenAPIResponse';
|
import { OpenAPIResponse } from './OpenAPIResponse';
|
||||||
|
import { useResponseExamplesState } from './OpenAPIResponseExampleContent';
|
||||||
import { StaticSection } from './StaticSection';
|
import { StaticSection } from './StaticSection';
|
||||||
import type { OpenAPIClientContext } from './types';
|
import type { OpenAPIClientContext } from './types';
|
||||||
|
import { createStateKey, getStatusCodeClassName, getStatusCodeDefaultLabel } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an interactive response body.
|
* Display an interactive response body.
|
||||||
@@ -14,12 +19,7 @@ export function OpenAPIResponses(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { responses, context } = props;
|
const { responses, context } = props;
|
||||||
|
|
||||||
return (
|
const groups = Object.entries(responses).map(
|
||||||
<StaticSection header="Responses" className="openapi-responses">
|
|
||||||
<OpenAPIDisclosureGroup
|
|
||||||
allowsMultipleExpanded
|
|
||||||
icon={context.icons.chevronRight}
|
|
||||||
groups={Object.entries(responses).map(
|
|
||||||
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => {
|
||||||
const tabs = (() => {
|
const tabs = (() => {
|
||||||
// If there is no content, but there are headers, we need to show the headers
|
// If there is no content, but there are headers, we need to show the headers
|
||||||
@@ -30,7 +30,8 @@ export function OpenAPIResponses(props: {
|
|||||||
) {
|
) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'default',
|
key: 'default',
|
||||||
|
label: '',
|
||||||
body: (
|
body: (
|
||||||
<OpenAPIResponse
|
<OpenAPIResponse
|
||||||
response={response}
|
response={response}
|
||||||
@@ -42,9 +43,8 @@ export function OpenAPIResponses(props: {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(response.content ?? {}).map(
|
return Object.entries(response.content ?? {}).map(([contentType, mediaType]) => ({
|
||||||
([contentType, mediaType]) => ({
|
key: contentType,
|
||||||
id: contentType,
|
|
||||||
label: contentType,
|
label: contentType,
|
||||||
body: (
|
body: (
|
||||||
<OpenAPIResponse
|
<OpenAPIResponse
|
||||||
@@ -53,17 +53,21 @@ export function OpenAPIResponses(props: {
|
|||||||
context={context}
|
context={context}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const description = response.description;
|
const description = response.description;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: statusCode,
|
key: statusCode,
|
||||||
label: (
|
label: (
|
||||||
<div className="openapi-response-tab-content">
|
<div className="openapi-response-tab-content">
|
||||||
<span className="openapi-response-statuscode">
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'openapi-statuscode',
|
||||||
|
`openapi-statuscode-${getStatusCodeClassName(statusCode)}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
{statusCode}
|
{statusCode}
|
||||||
</span>
|
</span>
|
||||||
{description ? (
|
{description ? (
|
||||||
@@ -71,13 +75,29 @@ export function OpenAPIResponses(props: {
|
|||||||
source={description}
|
source={description}
|
||||||
className="openapi-response-description"
|
className="openapi-response-description"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : (
|
||||||
|
getStatusCodeDefaultLabel(statusCode)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
tabs,
|
tabs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)}
|
);
|
||||||
|
|
||||||
|
const state = useResponseExamplesState(context.blockKey, groups[0]?.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StaticSection header="Responses" className="openapi-responses">
|
||||||
|
<OpenAPIDisclosureGroup
|
||||||
|
icon={context.icons.chevronRight}
|
||||||
|
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>
|
</StaticSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ function OpenAPISchemaProperty(props: {
|
|||||||
const properties = getSchemaProperties(schema);
|
const properties = getSchemaProperties(schema);
|
||||||
if (properties?.length) {
|
if (properties?.length) {
|
||||||
return (
|
return (
|
||||||
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
|
<OpenAPIDisclosure
|
||||||
|
icon={context.icons.plus}
|
||||||
|
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
|
||||||
|
>
|
||||||
<OpenAPISchemaProperties
|
<OpenAPISchemaProperties
|
||||||
properties={properties}
|
properties={properties}
|
||||||
circularRefs={circularRefs}
|
circularRefs={circularRefs}
|
||||||
@@ -208,7 +211,10 @@ function OpenAPISchemaAlternative(props: {
|
|||||||
{description ? (
|
{description ? (
|
||||||
<Markdown source={description} className="openapi-schema-description" />
|
<Markdown source={description} className="openapi-schema-description" />
|
||||||
) : null}
|
) : null}
|
||||||
<OpenAPIDisclosure context={context} label={getDisclosureLabel(schema)}>
|
<OpenAPIDisclosure
|
||||||
|
icon={context.icons.plus}
|
||||||
|
label={(isExpanded) => getDisclosureLabel(schema, isExpanded)}
|
||||||
|
>
|
||||||
{properties?.length ? (
|
{properties?.length ? (
|
||||||
<OpenAPISchemaProperties
|
<OpenAPISchemaProperties
|
||||||
properties={properties}
|
properties={properties}
|
||||||
@@ -502,19 +508,21 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
|||||||
return type;
|
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.type === 'array' && !!schema.items) {
|
||||||
if (schema.items.oneOf) {
|
if (schema.items.oneOf) {
|
||||||
return 'available items';
|
label = 'available items';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to "child attributes" for enums and objects
|
// Fallback to "child attributes" for enums and objects
|
||||||
if (schema.items.enum || schema.items.type === 'object') {
|
else if (schema.items.enum || schema.items.type === 'object') {
|
||||||
return 'child attributes';
|
label = 'child attributes';
|
||||||
|
} else {
|
||||||
|
label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
label = schema.title || 'child attributes';
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema.items.title ?? schema.title ?? getSchemaTitle(schema.items);
|
return `${isExpanded ? 'Hide' : 'Show'} ${label}`;
|
||||||
}
|
|
||||||
|
|
||||||
return schema.title || 'child attributes';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
OpenAPIOperationData,
|
OpenAPIOperationData,
|
||||||
OpenAPISecurityWithRequired,
|
OpenAPISecurityWithRequired,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { resolveDescription } from './utils';
|
import { createStateKey, resolveDescription } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Present securities authorization that can be used for this operation.
|
* Present securities authorization that can be used for this operation.
|
||||||
@@ -24,9 +24,11 @@ export function OpenAPISecurities(props: {
|
|||||||
return (
|
return (
|
||||||
<InteractiveSection
|
<InteractiveSection
|
||||||
header="Authorizations"
|
header="Authorizations"
|
||||||
|
stateKey={createStateKey('securities', context.blockKey)}
|
||||||
toggeable
|
toggeable
|
||||||
defaultOpened={false}
|
defaultOpened={false}
|
||||||
toggleIcon={context.icons.chevronRight}
|
toggleIcon={context.icons.chevronRight}
|
||||||
|
selectIcon={context.icons.chevronDown}
|
||||||
className="openapi-securities"
|
className="openapi-securities"
|
||||||
tabs={securities.map(([key, security]) => {
|
tabs={securities.map(([key, security]) => {
|
||||||
const description = resolveDescription(security);
|
const description = resolveDescription(security);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
type Key,
|
type Key,
|
||||||
@@ -13,38 +14,63 @@ import {
|
|||||||
type SelectProps,
|
type SelectProps,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from 'react-aria-components';
|
} from 'react-aria-components';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
import { getOrCreateStoreByKey } from './getOrCreateStoreByKey';
|
||||||
|
|
||||||
export type OpenAPISelectItem = {
|
export type OpenAPISelectItem = {
|
||||||
key: Key;
|
key: Key;
|
||||||
label: string;
|
label: string | React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectProps<T>, 'children'> {
|
interface OpenAPISelectProps<T extends OpenAPISelectItem> extends Omit<SelectProps<T>, 'children'> {
|
||||||
items: T[];
|
items: T[];
|
||||||
children: React.ReactNode | ((item: T) => React.ReactNode);
|
children: React.ReactNode | ((item: T) => React.ReactNode);
|
||||||
selectedKey?: Key;
|
|
||||||
onChange?: (key: string | number) => void;
|
|
||||||
placement?: PopoverProps['placement'];
|
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>) {
|
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 (
|
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>
|
<Button>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
<span aria-hidden="true">
|
{icon}
|
||||||
<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>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Popover placement={placement} className="openapi-select-popover">
|
<Popover placement={placement} className="openapi-select-popover">
|
||||||
<ListBox className="openapi-select-listbox" items={items}>
|
<ListBox className="openapi-select-listbox" items={items}>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
|
|||||||
key="body"
|
key="body"
|
||||||
requestBody={operation.requestBody}
|
requestBody={operation.requestBody}
|
||||||
context={context}
|
context={context}
|
||||||
|
data={data}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{operation.responses ? (
|
{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: {
|
export function StaticSection(props: {
|
||||||
className: string;
|
className: string;
|
||||||
header: React.ReactNode;
|
header?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { className, header, children } = props;
|
const { className, header, children, footer } = props;
|
||||||
return (
|
return (
|
||||||
<Section className={className}>
|
<Section className={className}>
|
||||||
|
{header ? (
|
||||||
<SectionHeader className={className}>
|
<SectionHeader className={className}>
|
||||||
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
<SectionHeaderContent className={className}>{header}</SectionHeaderContent>
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
) : null}
|
||||||
<SectionBody className={className}>{children}</SectionBody>
|
<SectionBody className={className}>{children}</SectionBody>
|
||||||
|
{footer ? (
|
||||||
|
<SectionFooter className={className}>
|
||||||
|
<SectionFooterContent className={className}>{footer}</SectionFooterContent>
|
||||||
|
</SectionFooter>
|
||||||
|
) : null}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,52 @@ export interface CodeSampleGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const codeSampleGenerators: 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',
|
id: 'curl',
|
||||||
label: 'cURL',
|
label: 'cURL',
|
||||||
@@ -140,52 +186,6 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
|||||||
return code;
|
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) {
|
function indent(code: string, spaces: number) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { createStore } from 'zustand';
|
import { createStore } from 'zustand';
|
||||||
|
|
||||||
type Key = string | number;
|
type Key = string | number;
|
||||||
@@ -10,7 +8,7 @@ type State = {
|
|||||||
|
|
||||||
type Actions = { setKey: (key: Key | null) => void };
|
type Actions = { setKey: (key: Key | null) => void };
|
||||||
|
|
||||||
type Store = State & Actions;
|
export type Store = State & Actions;
|
||||||
|
|
||||||
const createStateStore = (initial?: Key) => {
|
const createStateStore = (initial?: Key) => {
|
||||||
return createStore<Store>()((set) => ({
|
return createStore<Store>()((set) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { OpenAPISchema } from '@gitbook/openapi-parser';
|
import type { OpenAPISchema } from '@gitbook/openapi-parser';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
|
import { OpenAPIDisclosure } from '../OpenAPIDisclosure';
|
||||||
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
|
import { OpenAPIExample, getExampleFromSchema } from '../OpenAPIExample';
|
||||||
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
|
import { OpenAPIRootSchema } from '../OpenAPISchemaServer';
|
||||||
import { Section, SectionBody, StaticSection } from '../StaticSection';
|
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.
|
// If there are multiple schemas, we use a disclosure group to show them all.
|
||||||
return (
|
return (
|
||||||
<div className={clsx('openapi-schemas', className)}>
|
<div className={clsx('openapi-schemas', className)}>
|
||||||
<OpenAPIDisclosureGroup
|
{schemas.map(({ name, schema }) => {
|
||||||
allowsMultipleExpanded
|
return (
|
||||||
|
<OpenAPIDisclosure
|
||||||
|
className="openapi-schemas-disclosure"
|
||||||
|
key={name}
|
||||||
icon={context.icons.chevronRight}
|
icon={context.icons.chevronRight}
|
||||||
groups={schemas.map(({ name, schema }) => ({
|
label={name}
|
||||||
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">
|
<Section className="openapi-section-schemas">
|
||||||
<SectionBody>
|
<SectionBody>
|
||||||
<OpenAPIRootSchema
|
<OpenAPIRootSchema schema={schema} context={clientContext} />
|
||||||
schema={schema}
|
|
||||||
context={clientContext}
|
|
||||||
/>
|
|
||||||
</SectionBody>
|
</SectionBody>
|
||||||
</Section>
|
</Section>
|
||||||
),
|
</OpenAPIDisclosure>
|
||||||
},
|
);
|
||||||
],
|
})}
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,3 +149,65 @@ function shouldDisplayExample(schema: OpenAPIV3.SchemaObject): boolean {
|
|||||||
Object.keys(schema.example).length > 0)
|
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