From 4d476a8cbf5b5ec6b9a61b92be59335c7b6ae618 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 27 Aug 2024 19:03:20 +0100 Subject: [PATCH 01/55] Select variables in server url --- .../src/OpenAPIServerURLVariable.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 408c84117..7bf93c997 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -12,5 +12,25 @@ export function OpenAPIServerURLVariable(props: { variable: OpenAPIV3.ServerVariableObject; }) { const { variable } = props; + + if (variable.enum && variable.enum.length > 0) { + return (); + + } return {variable.default}; } From 7e79e14a6214683724590f276ce4417a12da0687 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 3 Sep 2024 10:11:58 +0100 Subject: [PATCH 02/55] Update server url based on variable selection --- .../(content)/[[...pathname]]/page.tsx | 1 + .../components/DocumentView/DocumentView.tsx | 2 + .../DocumentView/OpenAPI/OpenAPI.tsx | 31 ++++++++- .../src/components/PageBody/PageBody.tsx | 4 +- .../react-openapi/src/OpenAPICodeSample.tsx | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 22 ++++++- .../react-openapi/src/OpenAPIServerURL.tsx | 27 +++++--- .../src/OpenAPIServerURLForm.tsx | 43 ++++++++++++ .../src/OpenAPIServerURLVariable.tsx | 65 +++++++++++++------ packages/react-openapi/src/types.ts | 4 ++ 10 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 packages/react-openapi/src/OpenAPIServerURLForm.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..84a925860 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -90,6 +90,7 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } + searchParams={searchParams} /> {page.layout.outline ? ( boolean; + + searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 014a53193..ee96a211e 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,6 +1,6 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation } from '@gitbook/react-openapi'; +import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; @@ -45,6 +45,10 @@ async function OpenAPIBody(props: BlockProps) { return null; } + const enumSelectors = + context.searchParams && context.searchParams.block === block.key + ? parseModifiers(data, context.searchParams) + : undefined; return ( ) { CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, + enumSelectors, blockKey: block.key, }} className="openapi-block" @@ -91,3 +96,27 @@ function OpenAPIFallback() { ); } + +function parseModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return; + } + const { servers } = params; + const serverIndex = + servers && !isNaN(Number(servers)) + ? Math.min(0, Math.max(Number(servers), servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + if (server && server.variables) { + return Object.keys(server.variables).reduce>( + (result, key) => { + const selection = Number(params[key]); + if (!isNaN(selection)) { + result[key] = selection; + } + return result; + }, + { servers: serverIndex }, + ); + } +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 05440f9d9..98dd20f97 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,6 +34,7 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; + searchParams: Record; }) { const { space, @@ -44,6 +45,7 @@ export function PageBody(props: { page, document, withPageFeedback, + searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -54,7 +56,6 @@ export function PageBody(props: { 'siteId' in contentPointer ? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId } : undefined; - return ( <>
resolveContentRef(ref, context, options), shouldHighlightCode, + searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 265d57207..670d2712d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -54,7 +54,7 @@ export function OpenAPICodeSample(props: { const input: CodeSampleInput = { url: - getServersURL(data.servers) + + getServersURL(data.servers, context.enumSelectors) + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 365cede51..a6f495097 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPIServerURL } from './OpenAPIServerURL'; +import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton'; /** * Display an interactive OpenAPI operation. */ -export function OpenAPIOperation(props: { +export async function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; context: OpenAPIContextProps; @@ -25,8 +25,10 @@ export function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, + enumSelectors: context.enumSelectors, }; + const config = await getConfiguration(context); return (
@@ -47,7 +49,7 @@ export function OpenAPIOperation(props: { {method.toUpperCase()} - + {path}
@@ -67,3 +69,17 @@ export function OpenAPIOperation(props: {
); } + +async function getConfiguration(context: OpenAPIContextProps) { + const response = await fetch(context.specUrl); + const doc = await response.json(); + + return { + spec: { + content: { + ...doc, + servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], + }, + }, + }; +} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index e81fb47ba..79973d621 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,18 +1,23 @@ import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; +import { OpenAPIClientContext } from './types'; +import { ServerURLForm } from './OpenAPIServerURLForm'; /** * Show the url of the server with variables replaced by their default values. */ -export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { - const { servers } = props; - const server = servers[0]; - +export function OpenAPIServerURL(props: { + servers: OpenAPIV3.ServerObject[]; + context: OpenAPIClientContext; +}) { + const { servers, context } = props; + const serverIndex = context.enumSelectors?.servers ?? 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { if (part.kind === 'text') { return {part.text}; @@ -26,18 +31,22 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { key={i} name={part.name} variable={server.variables[part.name]} + enumIndex={context.enumSelectors?.[part.name]} /> ); } })} - + ); } /** * Get the default URL for the server. */ -export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { const server = servers[0]; const parts = parseServerURL(server?.url ?? ''); @@ -46,7 +55,9 @@ export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { if (part.kind === 'text') { return part.text; } else { - return server.variables?.[part.name]?.default ?? `{${part.name}}`; + return selectors && !isNaN(selectors[part.name]) + ? server.variables?.[part.name]?.enum?.[selectors[part.name]] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); } }) .join(''); diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx new file mode 100644 index 000000000..565108417 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { OpenAPIClientContext } from './types'; +import { OpenAPIV3 } from 'openapi-types'; +import { useApiClientModal } from '@scalar/api-client-react'; + +export function ServerURLForm(props: { + children: React.ReactNode; + context: OpenAPIClientContext; + server: OpenAPIV3.ServerObject; +}) { + const { children, context, server } = props; + const router = useRouter(); + const client = useApiClientModal(); + const [isPending, startTransition] = React.useTransition(); + + function updateServerUrl(formData: FormData) { + startTransition(() => { + if (!server.variables) { + return; + } + let params = new URLSearchParams(`block=${context.blockKey}`); + const variableKeys = Object.keys(server.variables); + for (const pair of formData.entries()) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + params.set(pair[0], `${pair[1]}`); + } + } + router.push(`?${params}`, { scroll: false }); + }); + } + + return ( +
+
+ + {children} +
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 7bf93c997..5bc9d62bc 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,8 +1,9 @@ 'use client'; - import * as React from 'react'; +import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; +import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -10,27 +11,53 @@ import { OpenAPIV3 } from 'openapi-types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; + enumIndex?: number; }) { - const { variable } = props; + const { enumIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { - return (); - + return ( + v === variable.default) + } + /> + ); } + return {variable.default}; } + +/** + * Render a select if there is an enum for a Server URL variable + */ +function EnumSelect(props: { + value?: number; + name: string; + variable: OpenAPIV3.ServerVariableObject; +}) { + const { value, name, variable } = props; + return ( + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 156a28ac0..b278f2efd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -21,6 +21,10 @@ export interface OpenAPIClientContext { blockKey?: string; /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; + + blockKey?: string; + + enumSelectors?: Record; } export interface OpenAPIFetcher { From e85220657321d5e8c9db15a2b315f6c51e2e47e9 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:46:18 +0100 Subject: [PATCH 03/55] Update server choice based on selection --- .../DocumentView/OpenAPI/OpenAPI.tsx | 13 ++++---- .../components/DocumentView/OpenAPI/style.css | 4 +++ .../react-openapi/src/OpenAPICodeSample.tsx | 1 - .../react-openapi/src/OpenAPIOperation.tsx | 20 ++---------- .../react-openapi/src/OpenAPIServerURL.tsx | 15 +++++---- .../src/OpenAPIServerURLForm.tsx | 32 ++++++++++++------- .../react-openapi/src/ScalarApiButton.tsx | 8 ++--- packages/react-openapi/src/ServerSelector.tsx | 26 +++++++++++++++ packages/react-openapi/src/types.ts | 11 +++++-- 9 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 packages/react-openapi/src/ServerSelector.tsx diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index ee96a211e..438c3ca27 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -49,6 +49,7 @@ async function OpenAPIBody(props: BlockProps) { context.searchParams && context.searchParams.block === block.key ? parseModifiers(data, context.searchParams) : undefined; + return ( >( + if (server) { + return Object.keys(server.variables ?? {}).reduce>( (result, key) => { const selection = Number(params[key]); if (!isNaN(selection)) { @@ -116,7 +117,7 @@ function parseModifiers(data: OpenAPIOperationData, params: Record *:last-child { @apply mb-0; } + +.openapi-server-button { + @apply disabled:opacity-5; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 670d2712d..eff0c9154 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -51,7 +51,6 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; - const input: CodeSampleInput = { url: getServersURL(data.servers, context.enumSelectors) + diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index a6f495097..fb1daa951 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -28,9 +28,8 @@ export async function OpenAPIOperation(props: { enumSelectors: context.enumSelectors, }; - const config = await getConfiguration(context); return ( - +

@@ -49,8 +48,7 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - - {path} +

@@ -69,17 +67,3 @@ export async function OpenAPIOperation(props: {
); } - -async function getConfiguration(context: OpenAPIContextProps) { - const response = await fetch(context.specUrl); - const doc = await response.json(); - - return { - spec: { - content: { - ...doc, - servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], - }, - }, - }; -} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 79973d621..c30517a9d 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -3,6 +3,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; +import { ServerSelector } from './ServerSelector'; /** * Show the url of the server with variables replaced by their default values. @@ -10,16 +11,17 @@ import { ServerURLForm } from './OpenAPIServerURLForm'; export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[]; context: OpenAPIClientContext; + path?: string; }) { - const { servers, context } = props; - const serverIndex = context.enumSelectors?.servers ?? 0; + const { path, servers, context } = props; + const serverIndex = context.enumSelectors?.server ?? 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -35,7 +37,7 @@ export function OpenAPIServerURL(props: { /> ); } - })} + })}{path} ); } @@ -47,7 +49,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const server = servers[0]; + const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return parts diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 565108417..2818bbbf6 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -4,25 +4,34 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; -import { useApiClientModal } from '@scalar/api-client-react'; +import { ServerSelector } from './ServerSelector'; export function ServerURLForm(props: { children: React.ReactNode; context: OpenAPIClientContext; - server: OpenAPIV3.ServerObject; + servers: OpenAPIV3.ServerObject[]; + serverIndex: number; }) { - const { children, context, server } = props; + const { children, context, servers, serverIndex } = props; const router = useRouter(); - const client = useApiClientModal(); const [isPending, startTransition] = React.useTransition(); + + const server = servers[serverIndex]; + const formRef = React.useRef(null); - function updateServerUrl(formData: FormData) { + function switchServer(index: number) { startTransition(() => { - if (!server.variables) { - return; + if (index !== serverIndex) { + let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + router.push(`?${params}`, { scroll: false }); } - let params = new URLSearchParams(`block=${context.blockKey}`); - const variableKeys = Object.keys(server.variables); + }); + } + + function updateServerVariables(formData: FormData) { + startTransition(() => { + let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { params.set(pair[0], `${pair[1]}`); @@ -33,10 +42,11 @@ export function ServerURLForm(props: { } return ( -
+
- {children} + {children} + { servers.length > 1 ? : null }
); diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 02d9ffaa1..60b7f1586 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -55,8 +55,8 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode }) { - const { children } = props; +export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { + const { children, serverUrl } = props; const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: operationData.servers[0]?.url, + url: serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active]); + }, [active, serverUrl]); return ( diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx new file mode 100644 index 000000000..a235b7b67 --- /dev/null +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; + +export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { + const { currentIndex, onChange, servers } = props; + const [index, setIndex] = React.useState(currentIndex); + + React.useEffect(() => { + onChange(index); + }, [index]); + + return + + + + ; +} \ No newline at end of file diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index b278f2efd..215d8ef56 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,15 +15,20 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; + /** * The key of the block */ blockKey?: string; - /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ - id?: string; - blockKey?: string; + /** + * Optional id attached to the OpenAPI Operation heading and used as an anchor + */ + id?: string; + /** + * Selectors to update openapi enums, e.g. for server url variables + */ enumSelectors?: Record; } From cb454a3c4ea97eeb4e0d7100922aa82a5e24728e Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:48:47 +0100 Subject: [PATCH 04/55] Format --- .../components/DocumentView/OpenAPI/style.css | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 6 ++- .../react-openapi/src/OpenAPIServerURL.tsx | 5 +- .../src/OpenAPIServerURLForm.tsx | 18 +++++-- packages/react-openapi/src/ServerSelector.tsx | 52 +++++++++++++------ packages/react-openapi/src/types.ts | 4 +- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 1882a423c..6bd0206ad 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -367,4 +367,4 @@ .openapi-server-button { @apply disabled:opacity-5; -} \ No newline at end of file +} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index fb1daa951..f722ce213 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -48,7 +48,11 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - + diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index c30517a9d..14d245aa4 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -21,7 +21,7 @@ export function OpenAPIServerURL(props: { return ( {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -37,7 +37,8 @@ export function OpenAPIServerURL(props: { /> ); } - })}{path} + })} + {path} ); } diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 2818bbbf6..5b16213a7 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -15,14 +15,16 @@ export function ServerURLForm(props: { const { children, context, servers, serverIndex } = props; const router = useRouter(); const [isPending, startTransition] = React.useTransition(); - + const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { startTransition(() => { if (index !== serverIndex) { - let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${index ?? '0'}`, + ); router.push(`?${params}`, { scroll: false }); } }); @@ -30,7 +32,9 @@ export function ServerURLForm(props: { function updateServerVariables(formData: FormData) { startTransition(() => { - let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, + ); const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { @@ -46,7 +50,13 @@ export function ServerURLForm(props: {
{children} - { servers.length > 1 ? : null } + {servers.length > 1 ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index a235b7b67..53051e3b8 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; -export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { +export function ServerSelector(props: { + currentIndex: number; + onChange: (value: number) => void; + servers: any[]; +}) { const { currentIndex, onChange, servers } = props; const [index, setIndex] = React.useState(currentIndex); @@ -10,17 +14,35 @@ export function ServerSelector(props: { currentIndex: number; onChange: (value:n onChange(index); }, [index]); - return - - - - ; -} \ No newline at end of file + return ( + + + + + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 215d8ef56..90d9889bd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,13 +15,13 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; - + /** * The key of the block */ blockKey?: string; - /** + /** * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; From ca4518d068823356ab24a45e1f880260945d4b1a Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:58:10 +0100 Subject: [PATCH 05/55] Changeset --- .changeset/long-shrimps-judge.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/long-shrimps-judge.md diff --git a/.changeset/long-shrimps-judge.md b/.changeset/long-shrimps-judge.md new file mode 100644 index 000000000..edae3be68 --- /dev/null +++ b/.changeset/long-shrimps-judge.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': minor +'gitbook': minor +--- + +Allow selection of server url From 2d15453e51a8aa77d2da2d664ba7b170853f2192 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 09:35:43 +0100 Subject: [PATCH 06/55] Revised client state with context --- .../(content)/[[...pathname]]/page.tsx | 1 - .../components/DocumentView/DocumentView.tsx | 2 - .../DocumentView/OpenAPI/OpenAPI.tsx | 63 +++++------------- .../DocumentView/OpenAPI/OpenAPIContext.tsx | 21 ++++++ .../src/components/PageBody/PageBody.tsx | 3 - packages/react-openapi/package.json | 3 +- .../react-openapi/src/OpenAPICodeSample.tsx | 3 +- .../src/OpenAPIContextProvider.tsx | 52 +++++++++++++++ .../react-openapi/src/OpenAPIOperation.tsx | 5 +- .../react-openapi/src/OpenAPIServerURL.tsx | 50 +++----------- .../src/OpenAPIServerURLForm.tsx | 65 +++++++++++-------- .../src/OpenAPIServerURLVariable.tsx | 19 +++--- .../react-openapi/src/ScalarApiButton.tsx | 12 ++-- packages/react-openapi/src/client.ts | 2 + packages/react-openapi/src/utils.ts | 38 +++++++++++ 15 files changed, 202 insertions(+), 137 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx create mode 100644 packages/react-openapi/src/OpenAPIContextProvider.tsx create mode 100644 packages/react-openapi/src/client.ts diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index 84a925860..b5ab1424e 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -90,7 +90,6 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } - searchParams={searchParams} /> {page.layout.outline ? ( boolean; - - searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 438c3ca27..05c094467 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,12 +1,13 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; +import { OpenAPIOperation } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; +import OpenAPIContext from './OpenAPIContext'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -45,27 +46,23 @@ async function OpenAPIBody(props: BlockProps) { return null; } - const enumSelectors = - context.searchParams && context.searchParams.block === block.key - ? parseModifiers(data, context.searchParams) - : undefined; - return ( - , - chevronRight: , - }, - CodeBlock: PlainCodeBlock, - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - enumSelectors, - blockKey: block.key, - }} - className="openapi-block" - /> + + , + chevronRight: , + }, + CodeBlock: PlainCodeBlock, + defaultInteractiveOpened: context.mode === 'print', + id: block.meta?.id, + blockKey: block.key + }} + className="openapi-block" + /> + ); } @@ -97,27 +94,3 @@ function OpenAPIFallback() { ); } - -function parseModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { - return; - } - const { server: serverQueryParam } = params; - const serverIndex = - serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) - : 0; - const server = data.servers[serverIndex]; - if (server) { - return Object.keys(server.variables ?? {}).reduce>( - (result, key) => { - const selection = Number(params[key]); - if (!isNaN(selection)) { - result[key] = selection; - } - return result; - }, - { server: serverIndex }, - ); - } -} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx new file mode 100644 index 000000000..16a48beca --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx @@ -0,0 +1,21 @@ +'use client' + +import { DocumentBlock } from "@gitbook/api"; +import { OpenAPIOperationData } from "@gitbook/react-openapi"; +import {OpenAPIContextProvider} from "@gitbook/react-openapi/client"; +import { useRouter, useSearchParams } from 'next/navigation'; +import * as React from 'react'; + +export default function OpenAPIContext(props: { children: React.ReactNode; block: DocumentBlock; data: OpenAPIOperationData }) { + const { block, children, data } = props; + const [isPending, startTransition] = React.useTransition(); + const router = useRouter(); + const searchParams = useSearchParams(); + + return { + startTransition(() => { + const queryParams = new URLSearchParams(params ?? ''); + router.replace(`?${queryParams}`, { scroll: false }); + }) + }}>{children} +} \ No newline at end of file diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 98dd20f97..052d9abb2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,7 +34,6 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; - searchParams: Record; }) { const { space, @@ -45,7 +44,6 @@ export function PageBody(props: { page, document, withPageFeedback, - searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -98,7 +96,6 @@ export function PageBody(props: { resolveContentRef: (ref, options) => resolveContentRef(ref, context, options), shouldHighlightCode, - searchParams, }} /> ) : ( diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index fa08009bc..ba2de7a13 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -6,7 +6,8 @@ "types": "./dist/index.d.ts", "development": "./src/index.ts", "default": "./dist/index.js" - } + }, + "./client": "./src/client.ts" }, "version": "0.6.0", "dependencies": { diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index eff0c9154..b3021263a 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -4,10 +4,9 @@ import { CodeSampleInput, codeSampleGenerators } from './code-samples'; import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample'; import { InteractiveSection } from './InteractiveSection'; -import { getServersURL } from './OpenAPIServerURL'; import { ScalarApiButton } from './ScalarApiButton'; import { OpenAPIContextProps } from './types'; -import { noReference } from './utils'; +import { getServersURL, noReference } from './utils'; /** * Display code samples to execute the operation. diff --git a/packages/react-openapi/src/OpenAPIContextProvider.tsx b/packages/react-openapi/src/OpenAPIContextProvider.tsx new file mode 100644 index 000000000..3ee32c4f4 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIContextProvider.tsx @@ -0,0 +1,52 @@ +'use client' + +import * as React from 'react'; +import { OpenAPIOperationData } from './fetchOpenAPIOperation'; +import { getServersURL } from './utils'; + +type OpenAPIContextProps = { isPending?: boolean; serverUrl?: string; state?: Record | null, onUpdate: (params: Record | null) => void; } +const OpenAPIContext = React.createContext(null); + +export function useOpenAPIContext() { + return React.useContext(OpenAPIContext); +} + +export function OpenAPIContextProvider(props: { children: React.ReactNode; data: OpenAPIOperationData; isPending?: boolean; params?: Record; onUpdate: OpenAPIContextProps['onUpdate']; }) { + const { children, data, isPending, params, onUpdate } = props; + + const clientState = React.useMemo(() => { + if (!params) { return null; } + return parseClientStateModifiers(data, params); + }, [data, params]); + const serverUrl = getServersURL(data.servers, clientState ?? undefined) + + return {children} +} + + +function parseClientStateModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return null; + } + const serverQueryParam = params['server']; + const serverIndex = + serverQueryParam && !isNaN(Number(serverQueryParam)) + ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + return server ? Object.keys(server.variables ?? {}).reduce>( + (result, key) => { + const selection = params[key]; + if (!isNaN(Number(selection))) { + result[key] = selection; + } + return result; + }, + { server: `${serverIndex}`, edit: params['edit'] }, + ) : null; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index f722ce213..b0a2b0325 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; +import { OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -25,11 +25,10 @@ export async function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, - enumSelectors: context.enumSelectors, }; return ( - +

diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 14d245aa4..6b90a5c86 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,9 +1,12 @@ +'use client'; + import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; -import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { parseServerURL } from './utils'; /** * Show the url of the server with variables replaced by their default values. @@ -14,9 +17,12 @@ export function OpenAPIServerURL(props: { path?: string; }) { const { path, servers, context } = props; - const serverIndex = context.enumSelectors?.server ?? 0; + const ctx = useOpenAPIContext(); + + const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); + console.log({ ctxState: ctx?.state }) return ( @@ -33,7 +39,8 @@ export function OpenAPIServerURL(props: { key={i} name={part.name} variable={server.variables[part.name]} - enumIndex={context.enumSelectors?.[part.name]} + selectionIndex={Number(ctx?.state?.[part.name])} + selectable={Boolean(ctx?.state?.edit)} /> ); } @@ -42,40 +49,3 @@ export function OpenAPIServerURL(props: { ); } - -/** - * Get the default URL for the server. - */ -export function getServersURL( - servers: OpenAPIV3.ServerObject[], - selectors?: Record, -): string { - const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; - const server = servers[serverIndex]; - const parts = parseServerURL(server?.url ?? ''); - - return parts - .map((part) => { - if (part.kind === 'text') { - return part.text; - } else { - return selectors && !isNaN(selectors[part.name]) - ? server.variables?.[part.name]?.enum?.[selectors[part.name]] - : (server.variables?.[part.name]?.default ?? `{${part.name}}`); - } - }) - .join(''); -} - -function parseServerURL(url: string) { - const parts = url.split(/{([^}]+)}/g); - const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; - for (let i = 0; i < parts.length; i++) { - if (i % 2 === 0) { - result.push({ kind: 'text', text: parts[i] }); - } else { - result.push({ kind: 'variable', name: parts[i] }); - } - } - return result; -} diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 5b16213a7..36ee6df57 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -1,10 +1,10 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; export function ServerURLForm(props: { children: React.ReactNode; @@ -13,50 +13,59 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const router = useRouter(); - const [isPending, startTransition] = React.useTransition(); - + const ctx = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { - startTransition(() => { - if (index !== serverIndex) { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${index ?? '0'}`, - ); - router.push(`?${params}`, { scroll: false }); - } - }); + if (index !== serverIndex) { + update({ + server: `${index}` ?? '0', + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } } function updateServerVariables(formData: FormData) { - startTransition(() => { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, - ); - const variableKeys = Object.keys(server.variables ?? {}); - for (const pair of formData.entries()) { - if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { - params.set(pair[0], `${pair[1]}`); - } + const variableKeys = Object.keys(server.variables ?? {}); + const variables: Record = {}; + for (const pair of formData) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + variables[pair[0]] = `${pair[1]}`; } - router.push(`?${params}`, { scroll: false }); + } + update({ + server: `${formData.get('server')}` ?? '0', + ...variables, + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } + + function update(variables?: Record) { + if (!context.blockKey) { return; } + ctx?.onUpdate({ + block: context.blockKey, + ...variables, }); } return ( -
-
+ { e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents"> +
{children} - {servers.length > 1 ? ( + {ctx?.state?.edit && servers.length > 1 ? ( ) : null} +
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 5bc9d62bc..83a316269 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,9 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; -import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -11,18 +9,23 @@ import { OpenAPIClientContext } from './types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; - enumIndex?: number; + selectionIndex?: number; + selectable: boolean; }) { - const { enumIndex, name, variable } = props; + const { selectable, selectionIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { + if (!selectable) { + return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + } + return ( - v === variable.default) } /> @@ -35,7 +38,7 @@ export function OpenAPIServerURLVariable(props: { /** * Render a select if there is an enum for a Server URL variable */ -function EnumSelect(props: { +function VariableSelector(props: { value?: number; name: string; variable: OpenAPIV3.ServerVariableObject; diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 60b7f1586..dcf1d7a70 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -12,6 +12,8 @@ import { import React from 'react'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; const ApiClientReact = React.lazy(async () => { const mod = await import('@scalar/api-client-react'); @@ -55,8 +57,10 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { - const { children, serverUrl } = props; +export function ScalarApiClient(props: { children: React.ReactNode; }) { + const { children } = props; + + const ctx = useOpenAPIContext(); const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: serverUrl ?? operationData.servers[0]?.url, + url: ctx?.serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active, serverUrl]); + }, [active, ctx?.state?.serverUrl]); return ( diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts new file mode 100644 index 000000000..923168320 --- /dev/null +++ b/packages/react-openapi/src/client.ts @@ -0,0 +1,2 @@ +'use client'; +export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 5bc3ee092..c94d11df2 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -11,3 +11,41 @@ export function noReference(input: T | OpenAPIV3.ReferenceObject): T { export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } + + +/** + * Get the default URL for the server. + */ +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { + const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; + const parts = parseServerURL(server?.url ?? ''); + + return parts + .map((part) => { + if (part.kind === 'text') { + return part.text; + } else { + return selectors && !isNaN(Number(selectors[part.name])) + ? server.variables?.[part.name]?.enum?.[Number(selectors[part.name])] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); + } + }) + .join(''); +} + +export function parseServerURL(url: string) { + const parts = url.split(/{([^}]+)}/g); + const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; + for (let i = 0; i < parts.length; i++) { + if (i % 2 === 0) { + result.push({ kind: 'text', text: parts[i] }); + } else { + result.push({ kind: 'variable', name: parts[i] }); + } + } + return result; +} \ No newline at end of file From d919d2ccfa56720af02a51332e312daac79f3805 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 11:27:27 +0100 Subject: [PATCH 07/55] Add icons for editing url --- .../components/DocumentView/OpenAPI/OpenAPI.tsx | 2 ++ .../components/DocumentView/OpenAPI/style.css | 17 ++++++++++++++--- packages/react-openapi/src/OpenAPIOperation.tsx | 2 +- .../react-openapi/src/OpenAPIServerURLForm.tsx | 5 +++-- packages/react-openapi/src/ServerSelector.tsx | 4 ++-- packages/react-openapi/src/types.ts | 2 ++ 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 05c094467..1c907c928 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -54,6 +54,8 @@ async function OpenAPIBody(props: BlockProps) { icons: { chevronDown: , chevronRight: , + edit: , + clear: }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 6bd0206ad..cd2c8b5ca 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -57,7 +57,7 @@ /** URL */ .openapi-url { - @apply font-mono text-sm text-dark/8 dark:text-light/8; + @apply flex items-center font-mono text-sm text-dark/8 dark:text-light/8; } .openapi-url-var { @@ -365,6 +365,17 @@ @apply mb-0; } -.openapi-server-button { - @apply disabled:opacity-5; +.openapi-select-button, +.openapi-edit-button { + @apply p-0.5 inline-flex text-dark/6 dark:text-light/6 hover:opacity-8; +} + +.openapi-edit-button { @apply p-0.5 ml-4 size-4; } + +.openapi-edit-button > * { + @apply size-full; +} + +.openapi-select-button { + @apply leading-[1cap] disabled:opacity-5; } diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index b0a2b0325..e4a643303 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -37,7 +37,7 @@ export async function OpenAPIOperation(props: { {operation.description ? ( ) : null} -
+
1 || server.variables; return (
{ e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents">
@@ -61,11 +62,11 @@ export function ServerURLForm(props: { onChange={switchServer} /> ) : null} - + }} title={ctx?.state?.edit ? undefined : "Try different server options"} aria-label={ctx?.state?.edit ? "Clear" : "Edit"}>{ctx?.state?.edit ? context.icons.clear : context.icons.edit} : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index 53051e3b8..bd2997619 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -18,7 +18,7 @@ export function ServerSelector(props: { : null} + {isEditable ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 83a316269..27e82a00c 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -16,7 +16,13 @@ export function OpenAPIServerURLVariable(props: { if (variable.enum && variable.enum.length > 0) { if (!selectable) { - return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + return ( + + {!isNaN(Number(selectionIndex)) + ? variable.enum[Number(selectionIndex)] + : variable.default} + + ); } return ( diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index dcf1d7a70..af6257fca 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -57,7 +57,7 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; }) { +export function ScalarApiClient(props: { children: React.ReactNode }) { const { children } = props; const ctx = useOpenAPIContext(); diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts index 923168320..17e54c128 100644 --- a/packages/react-openapi/src/client.ts +++ b/packages/react-openapi/src/client.ts @@ -1,2 +1,2 @@ 'use client'; -export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file +export { OpenAPIContextProvider } from './OpenAPIContextProvider'; diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index c94d11df2..d2d66e2be 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -12,7 +12,6 @@ export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } - /** * Get the default URL for the server. */ @@ -20,7 +19,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const serverIndex = + selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); @@ -48,4 +48,4 @@ export function parseServerURL(url: string) { } } return result; -} \ No newline at end of file +} From 3b0f5d82d58207a0459f8c66ea216859eaefc8c3 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:37:40 +0100 Subject: [PATCH 10/55] Add server url cache to update request code sample --- .../src/app/(space)/(content)/[[...pathname]]/page.tsx | 3 +++ .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 4 ++++ .../src/components/DocumentView/OpenAPI/ServerUrlCache.tsx | 5 +++++ packages/react-openapi/src/OpenAPICodeSample.tsx | 3 ++- packages/react-openapi/src/OpenAPIServerURL.tsx | 1 - packages/react-openapi/src/OpenAPIServerURLForm.tsx | 3 ++- packages/react-openapi/src/types.ts | 5 ++--- 7 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..4b126ee90 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -3,6 +3,7 @@ import { Metadata, Viewport } from 'next'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; +import { serverUrlCache } from '@/components/DocumentView/OpenAPI/ServerUrlCache'; import { PageAside } from '@/components/PageAside'; import { PageBody, PageCover } from '@/components/PageBody'; import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links'; @@ -26,6 +27,8 @@ export default async function Page(props: { }) { const { params, searchParams } = props; + serverUrlCache.parse(searchParams); + const { content: contentPointer, contentTarget, diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index bf18e3be5..7fd367184 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -8,6 +8,7 @@ import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; import OpenAPIContext from './OpenAPIContext'; +import { serverUrlCache } from './ServerUrlCache'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -46,6 +47,8 @@ async function OpenAPIBody(props: BlockProps) { return null; } + const serverUrl = serverUrlCache.get('serverUrl'); + return ( ) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, + serverUrl }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx new file mode 100644 index 000000000..b715ce3db --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -0,0 +1,5 @@ +import { createSearchParamsCache, parseAsString } from 'nuqs/server'; + +export const serverUrlCache = createSearchParamsCache({ + serverUrl: parseAsString +}) \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index b3021263a..ac63e4779 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -48,11 +48,12 @@ export function OpenAPICodeSample(props: { } }); + const serverUrl = context.serverUrl ?? getServersURL(data.servers); const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { url: - getServersURL(data.servers, context.enumSelectors) + + serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index ec225c430..5bce96035 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -22,7 +22,6 @@ export function OpenAPIServerURL(props: { const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); - console.log({ ctxState: ctx?.state }); return ( diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 98aa20534..8864f9b2d 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -5,6 +5,7 @@ import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; export function ServerURLForm(props: { children: React.ReactNode; @@ -80,7 +81,7 @@ export function ServerURLForm(props: { update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? undefined : { edit: 'true' }), + ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), }); }} title={ctx?.state?.edit ? undefined : 'Try different server options'} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index e8297efa4..415bf8e49 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -27,11 +27,10 @@ export interface OpenAPIClientContext { * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; - /** - * Selectors to update openapi enums, e.g. for server url variables + * Optional serverUrl to use with OpenAPI operations */ - enumSelectors?: Record; + serverUrl?: string | null; } export interface OpenAPIFetcher { From 087a783574aff351e1e7899b3f6217fd7b63e491 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:39:45 +0100 Subject: [PATCH 11/55] Formatting --- .../DocumentView/OpenAPI/OpenAPI.tsx | 2 +- .../DocumentView/OpenAPI/ServerUrlCache.tsx | 4 +-- .../react-openapi/src/OpenAPICodeSample.tsx | 5 +--- .../src/OpenAPIServerURLForm.tsx | 26 +++++++++++-------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 7fd367184..16dccc025 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -64,7 +64,7 @@ async function OpenAPIBody(props: BlockProps) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, - serverUrl + serverUrl, }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx index b715ce3db..47ae56d22 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -1,5 +1,5 @@ import { createSearchParamsCache, parseAsString } from 'nuqs/server'; export const serverUrlCache = createSearchParamsCache({ - serverUrl: parseAsString -}) \ No newline at end of file + serverUrl: parseAsString, +}); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index ac63e4779..6345dfe29 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -52,10 +52,7 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { - url: - serverUrl + - data.path + - (searchParams.size ? `?${searchParams.toString()}` : ''), + url: serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true }) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 8864f9b2d..45b277a69 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -14,7 +14,7 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const ctx = useOpenAPIContext(); + const stateContext = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); @@ -22,7 +22,7 @@ export function ServerURLForm(props: { if (index !== serverIndex) { update({ server: `${index}` ?? '0', - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } } @@ -38,7 +38,7 @@ export function ServerURLForm(props: { update({ server: `${formData.get('server')}` ?? '0', ...variables, - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } @@ -46,7 +46,7 @@ export function ServerURLForm(props: { if (!context.blockKey) { return; } - ctx?.onUpdate({ + stateContext?.onUpdate({ block: context.blockKey, ...variables, }); @@ -62,10 +62,10 @@ export function ServerURLForm(props: { }} className="contents" > -
+
{children} - {ctx?.state?.edit && servers.length > 1 ? ( + {stateContext?.state?.edit && servers.length > 1 ? ( { - const state = { ...ctx?.state }; + const state = { ...stateContext?.state }; delete state.edit; update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), + ...(stateContext?.state?.edit + ? { serverUrl: getServersURL(servers, state) } + : { edit: 'true' }), }); }} - title={ctx?.state?.edit ? undefined : 'Try different server options'} - aria-label={ctx?.state?.edit ? 'Clear' : 'Edit'} + title={ + stateContext?.state?.edit ? undefined : 'Try different server options' + } + aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {ctx?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} ) : null}
From 9573535eba2e94115fec15d4d13017b52d9661fb Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 12:05:59 +0100 Subject: [PATCH 12/55] Update icon --- .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 2 +- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 2 +- packages/react-openapi/src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 16dccc025..08fe0be58 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -58,7 +58,7 @@ async function OpenAPIBody(props: BlockProps) { chevronDown: , chevronRight: , edit: , - clear: , + editDone: , }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 45b277a69..6dc16c601 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -91,7 +91,7 @@ export function ServerURLForm(props: { } aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.editDone : context.icons.edit} ) : null}
diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 415bf8e49..7be2deb50 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -9,7 +9,7 @@ export interface OpenAPIClientContext { chevronDown: React.ReactNode; chevronRight: React.ReactNode; edit: React.ReactNode; - clear: React.ReactNode; + editDone: React.ReactNode; }; /** From e793a177f623a0bfa37988dfd52a7fdac618a0cf Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 27 Aug 2024 19:03:20 +0100 Subject: [PATCH 13/55] Select variables in server url --- .../src/OpenAPIServerURLVariable.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 408c84117..7bf93c997 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -12,5 +12,25 @@ export function OpenAPIServerURLVariable(props: { variable: OpenAPIV3.ServerVariableObject; }) { const { variable } = props; + + if (variable.enum && variable.enum.length > 0) { + return (); + + } return {variable.default}; } From b87ec84fd53fa4fe15f7b69681c31773563e8706 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 3 Sep 2024 10:11:58 +0100 Subject: [PATCH 14/55] Update server url based on variable selection --- .../(content)/[[...pathname]]/page.tsx | 1 + .../components/DocumentView/DocumentView.tsx | 2 + .../DocumentView/OpenAPI/OpenAPI.tsx | 31 ++++++++- .../src/components/PageBody/PageBody.tsx | 4 +- .../react-openapi/src/OpenAPICodeSample.tsx | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 22 ++++++- .../react-openapi/src/OpenAPIServerURL.tsx | 27 +++++--- .../src/OpenAPIServerURLForm.tsx | 43 ++++++++++++ .../src/OpenAPIServerURLVariable.tsx | 65 +++++++++++++------ packages/react-openapi/src/types.ts | 4 ++ 10 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 packages/react-openapi/src/OpenAPIServerURLForm.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..84a925860 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -90,6 +90,7 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } + searchParams={searchParams} /> {page.layout.outline ? ( boolean; + + searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 014a53193..ee96a211e 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,6 +1,6 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation } from '@gitbook/react-openapi'; +import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; @@ -45,6 +45,10 @@ async function OpenAPIBody(props: BlockProps) { return null; } + const enumSelectors = + context.searchParams && context.searchParams.block === block.key + ? parseModifiers(data, context.searchParams) + : undefined; return ( ) { CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, + enumSelectors, blockKey: block.key, }} className="openapi-block" @@ -91,3 +96,27 @@ function OpenAPIFallback() {

); } + +function parseModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return; + } + const { servers } = params; + const serverIndex = + servers && !isNaN(Number(servers)) + ? Math.min(0, Math.max(Number(servers), servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + if (server && server.variables) { + return Object.keys(server.variables).reduce>( + (result, key) => { + const selection = Number(params[key]); + if (!isNaN(selection)) { + result[key] = selection; + } + return result; + }, + { servers: serverIndex }, + ); + } +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 05440f9d9..98dd20f97 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,6 +34,7 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; + searchParams: Record; }) { const { space, @@ -44,6 +45,7 @@ export function PageBody(props: { page, document, withPageFeedback, + searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -54,7 +56,6 @@ export function PageBody(props: { 'siteId' in contentPointer ? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId } : undefined; - return ( <>
resolveContentRef(ref, context, options), shouldHighlightCode, + searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 265d57207..670d2712d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -54,7 +54,7 @@ export function OpenAPICodeSample(props: { const input: CodeSampleInput = { url: - getServersURL(data.servers) + + getServersURL(data.servers, context.enumSelectors) + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 365cede51..a6f495097 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPIServerURL } from './OpenAPIServerURL'; +import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton'; /** * Display an interactive OpenAPI operation. */ -export function OpenAPIOperation(props: { +export async function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; context: OpenAPIContextProps; @@ -25,8 +25,10 @@ export function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, + enumSelectors: context.enumSelectors, }; + const config = await getConfiguration(context); return (
@@ -47,7 +49,7 @@ export function OpenAPIOperation(props: { {method.toUpperCase()} - + {path}
@@ -67,3 +69,17 @@ export function OpenAPIOperation(props: {
); } + +async function getConfiguration(context: OpenAPIContextProps) { + const response = await fetch(context.specUrl); + const doc = await response.json(); + + return { + spec: { + content: { + ...doc, + servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], + }, + }, + }; +} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index e81fb47ba..79973d621 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,18 +1,23 @@ import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; +import { OpenAPIClientContext } from './types'; +import { ServerURLForm } from './OpenAPIServerURLForm'; /** * Show the url of the server with variables replaced by their default values. */ -export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { - const { servers } = props; - const server = servers[0]; - +export function OpenAPIServerURL(props: { + servers: OpenAPIV3.ServerObject[]; + context: OpenAPIClientContext; +}) { + const { servers, context } = props; + const serverIndex = context.enumSelectors?.servers ?? 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { if (part.kind === 'text') { return {part.text}; @@ -26,18 +31,22 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { key={i} name={part.name} variable={server.variables[part.name]} + enumIndex={context.enumSelectors?.[part.name]} /> ); } })} - + ); } /** * Get the default URL for the server. */ -export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { const server = servers[0]; const parts = parseServerURL(server?.url ?? ''); @@ -46,7 +55,9 @@ export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { if (part.kind === 'text') { return part.text; } else { - return server.variables?.[part.name]?.default ?? `{${part.name}}`; + return selectors && !isNaN(selectors[part.name]) + ? server.variables?.[part.name]?.enum?.[selectors[part.name]] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); } }) .join(''); diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx new file mode 100644 index 000000000..565108417 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { OpenAPIClientContext } from './types'; +import { OpenAPIV3 } from 'openapi-types'; +import { useApiClientModal } from '@scalar/api-client-react'; + +export function ServerURLForm(props: { + children: React.ReactNode; + context: OpenAPIClientContext; + server: OpenAPIV3.ServerObject; +}) { + const { children, context, server } = props; + const router = useRouter(); + const client = useApiClientModal(); + const [isPending, startTransition] = React.useTransition(); + + function updateServerUrl(formData: FormData) { + startTransition(() => { + if (!server.variables) { + return; + } + let params = new URLSearchParams(`block=${context.blockKey}`); + const variableKeys = Object.keys(server.variables); + for (const pair of formData.entries()) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + params.set(pair[0], `${pair[1]}`); + } + } + router.push(`?${params}`, { scroll: false }); + }); + } + + return ( +
+
+ + {children} +
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 7bf93c997..5bc9d62bc 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,8 +1,9 @@ 'use client'; - import * as React from 'react'; +import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; +import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -10,27 +11,53 @@ import { OpenAPIV3 } from 'openapi-types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; + enumIndex?: number; }) { - const { variable } = props; + const { enumIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { - return (); - + return ( + v === variable.default) + } + /> + ); } + return {variable.default}; } + +/** + * Render a select if there is an enum for a Server URL variable + */ +function EnumSelect(props: { + value?: number; + name: string; + variable: OpenAPIV3.ServerVariableObject; +}) { + const { value, name, variable } = props; + return ( + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 156a28ac0..b278f2efd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -21,6 +21,10 @@ export interface OpenAPIClientContext { blockKey?: string; /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; + + blockKey?: string; + + enumSelectors?: Record; } export interface OpenAPIFetcher { From 4f59d18bfc96070ca98ce732da6081003ef5af1c Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:46:18 +0100 Subject: [PATCH 15/55] Update server choice based on selection --- .../DocumentView/OpenAPI/OpenAPI.tsx | 13 ++++---- .../components/DocumentView/OpenAPI/style.css | 4 +++ .../react-openapi/src/OpenAPICodeSample.tsx | 1 - .../react-openapi/src/OpenAPIOperation.tsx | 20 ++---------- .../react-openapi/src/OpenAPIServerURL.tsx | 15 +++++---- .../src/OpenAPIServerURLForm.tsx | 32 ++++++++++++------- .../react-openapi/src/ScalarApiButton.tsx | 8 ++--- packages/react-openapi/src/ServerSelector.tsx | 26 +++++++++++++++ packages/react-openapi/src/types.ts | 11 +++++-- 9 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 packages/react-openapi/src/ServerSelector.tsx diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index ee96a211e..438c3ca27 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -49,6 +49,7 @@ async function OpenAPIBody(props: BlockProps) { context.searchParams && context.searchParams.block === block.key ? parseModifiers(data, context.searchParams) : undefined; + return ( >( + if (server) { + return Object.keys(server.variables ?? {}).reduce>( (result, key) => { const selection = Number(params[key]); if (!isNaN(selection)) { @@ -116,7 +117,7 @@ function parseModifiers(data: OpenAPIOperationData, params: Record *:last-child { @apply mb-0; } + +.openapi-server-button { + @apply disabled:opacity-5; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 670d2712d..eff0c9154 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -51,7 +51,6 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; - const input: CodeSampleInput = { url: getServersURL(data.servers, context.enumSelectors) + diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index a6f495097..fb1daa951 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -28,9 +28,8 @@ export async function OpenAPIOperation(props: { enumSelectors: context.enumSelectors, }; - const config = await getConfiguration(context); return ( - +

@@ -49,8 +48,7 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - - {path} +

@@ -69,17 +67,3 @@ export async function OpenAPIOperation(props: {
); } - -async function getConfiguration(context: OpenAPIContextProps) { - const response = await fetch(context.specUrl); - const doc = await response.json(); - - return { - spec: { - content: { - ...doc, - servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], - }, - }, - }; -} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 79973d621..c30517a9d 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -3,6 +3,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; +import { ServerSelector } from './ServerSelector'; /** * Show the url of the server with variables replaced by their default values. @@ -10,16 +11,17 @@ import { ServerURLForm } from './OpenAPIServerURLForm'; export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[]; context: OpenAPIClientContext; + path?: string; }) { - const { servers, context } = props; - const serverIndex = context.enumSelectors?.servers ?? 0; + const { path, servers, context } = props; + const serverIndex = context.enumSelectors?.server ?? 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -35,7 +37,7 @@ export function OpenAPIServerURL(props: { /> ); } - })} + })}{path} ); } @@ -47,7 +49,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const server = servers[0]; + const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return parts diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 565108417..2818bbbf6 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -4,25 +4,34 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; -import { useApiClientModal } from '@scalar/api-client-react'; +import { ServerSelector } from './ServerSelector'; export function ServerURLForm(props: { children: React.ReactNode; context: OpenAPIClientContext; - server: OpenAPIV3.ServerObject; + servers: OpenAPIV3.ServerObject[]; + serverIndex: number; }) { - const { children, context, server } = props; + const { children, context, servers, serverIndex } = props; const router = useRouter(); - const client = useApiClientModal(); const [isPending, startTransition] = React.useTransition(); + + const server = servers[serverIndex]; + const formRef = React.useRef(null); - function updateServerUrl(formData: FormData) { + function switchServer(index: number) { startTransition(() => { - if (!server.variables) { - return; + if (index !== serverIndex) { + let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + router.push(`?${params}`, { scroll: false }); } - let params = new URLSearchParams(`block=${context.blockKey}`); - const variableKeys = Object.keys(server.variables); + }); + } + + function updateServerVariables(formData: FormData) { + startTransition(() => { + let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { params.set(pair[0], `${pair[1]}`); @@ -33,10 +42,11 @@ export function ServerURLForm(props: { } return ( -
+
- {children} + {children} + { servers.length > 1 ? : null }
); diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 02d9ffaa1..60b7f1586 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -55,8 +55,8 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode }) { - const { children } = props; +export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { + const { children, serverUrl } = props; const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: operationData.servers[0]?.url, + url: serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active]); + }, [active, serverUrl]); return ( diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx new file mode 100644 index 000000000..a235b7b67 --- /dev/null +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; + +export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { + const { currentIndex, onChange, servers } = props; + const [index, setIndex] = React.useState(currentIndex); + + React.useEffect(() => { + onChange(index); + }, [index]); + + return + + + + ; +} \ No newline at end of file diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index b278f2efd..215d8ef56 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,15 +15,20 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; + /** * The key of the block */ blockKey?: string; - /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ - id?: string; - blockKey?: string; + /** + * Optional id attached to the OpenAPI Operation heading and used as an anchor + */ + id?: string; + /** + * Selectors to update openapi enums, e.g. for server url variables + */ enumSelectors?: Record; } From c338bca2bacd6407230a0257751ffa5274783913 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:48:47 +0100 Subject: [PATCH 16/55] Format --- .../components/DocumentView/OpenAPI/style.css | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 6 ++- .../react-openapi/src/OpenAPIServerURL.tsx | 5 +- .../src/OpenAPIServerURLForm.tsx | 18 +++++-- packages/react-openapi/src/ServerSelector.tsx | 52 +++++++++++++------ packages/react-openapi/src/types.ts | 4 +- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 1882a423c..6bd0206ad 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -367,4 +367,4 @@ .openapi-server-button { @apply disabled:opacity-5; -} \ No newline at end of file +} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index fb1daa951..f722ce213 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -48,7 +48,11 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - +
diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index c30517a9d..14d245aa4 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -21,7 +21,7 @@ export function OpenAPIServerURL(props: { return ( {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -37,7 +37,8 @@ export function OpenAPIServerURL(props: { /> ); } - })}{path} + })} + {path} ); } diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 2818bbbf6..5b16213a7 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -15,14 +15,16 @@ export function ServerURLForm(props: { const { children, context, servers, serverIndex } = props; const router = useRouter(); const [isPending, startTransition] = React.useTransition(); - + const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { startTransition(() => { if (index !== serverIndex) { - let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${index ?? '0'}`, + ); router.push(`?${params}`, { scroll: false }); } }); @@ -30,7 +32,9 @@ export function ServerURLForm(props: { function updateServerVariables(formData: FormData) { startTransition(() => { - let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, + ); const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { @@ -46,7 +50,13 @@ export function ServerURLForm(props: {
{children} - { servers.length > 1 ? : null } + {servers.length > 1 ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index a235b7b67..53051e3b8 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; -export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { +export function ServerSelector(props: { + currentIndex: number; + onChange: (value: number) => void; + servers: any[]; +}) { const { currentIndex, onChange, servers } = props; const [index, setIndex] = React.useState(currentIndex); @@ -10,17 +14,35 @@ export function ServerSelector(props: { currentIndex: number; onChange: (value:n onChange(index); }, [index]); - return - - - - ; -} \ No newline at end of file + return ( + + + + + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 215d8ef56..90d9889bd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,13 +15,13 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; - + /** * The key of the block */ blockKey?: string; - /** + /** * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; From 7244a0419a68170d4ad22fbebdc4cbc656be9688 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:58:10 +0100 Subject: [PATCH 17/55] Changeset --- .changeset/long-shrimps-judge.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/long-shrimps-judge.md diff --git a/.changeset/long-shrimps-judge.md b/.changeset/long-shrimps-judge.md new file mode 100644 index 000000000..edae3be68 --- /dev/null +++ b/.changeset/long-shrimps-judge.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': minor +'gitbook': minor +--- + +Allow selection of server url From d29a7a6f15e561ba05cacebb92e951c7d67ab990 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 09:35:43 +0100 Subject: [PATCH 18/55] Revised client state with context --- .../(content)/[[...pathname]]/page.tsx | 1 - .../components/DocumentView/DocumentView.tsx | 2 - .../DocumentView/OpenAPI/OpenAPI.tsx | 63 +++++------------- .../DocumentView/OpenAPI/OpenAPIContext.tsx | 21 ++++++ .../src/components/PageBody/PageBody.tsx | 3 - packages/react-openapi/package.json | 3 +- .../react-openapi/src/OpenAPICodeSample.tsx | 3 +- .../src/OpenAPIContextProvider.tsx | 52 +++++++++++++++ .../react-openapi/src/OpenAPIOperation.tsx | 5 +- .../react-openapi/src/OpenAPIServerURL.tsx | 50 +++----------- .../src/OpenAPIServerURLForm.tsx | 65 +++++++++++-------- .../src/OpenAPIServerURLVariable.tsx | 19 +++--- .../react-openapi/src/ScalarApiButton.tsx | 12 ++-- packages/react-openapi/src/client.ts | 2 + packages/react-openapi/src/utils.ts | 38 +++++++++++ 15 files changed, 202 insertions(+), 137 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx create mode 100644 packages/react-openapi/src/OpenAPIContextProvider.tsx create mode 100644 packages/react-openapi/src/client.ts diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index 84a925860..b5ab1424e 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -90,7 +90,6 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } - searchParams={searchParams} /> {page.layout.outline ? ( boolean; - - searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 438c3ca27..05c094467 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,12 +1,13 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; +import { OpenAPIOperation } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; +import OpenAPIContext from './OpenAPIContext'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -45,27 +46,23 @@ async function OpenAPIBody(props: BlockProps) { return null; } - const enumSelectors = - context.searchParams && context.searchParams.block === block.key - ? parseModifiers(data, context.searchParams) - : undefined; - return ( - , - chevronRight: , - }, - CodeBlock: PlainCodeBlock, - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - enumSelectors, - blockKey: block.key, - }} - className="openapi-block" - /> + + , + chevronRight: , + }, + CodeBlock: PlainCodeBlock, + defaultInteractiveOpened: context.mode === 'print', + id: block.meta?.id, + blockKey: block.key + }} + className="openapi-block" + /> + ); } @@ -97,27 +94,3 @@ function OpenAPIFallback() { ); } - -function parseModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { - return; - } - const { server: serverQueryParam } = params; - const serverIndex = - serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) - : 0; - const server = data.servers[serverIndex]; - if (server) { - return Object.keys(server.variables ?? {}).reduce>( - (result, key) => { - const selection = Number(params[key]); - if (!isNaN(selection)) { - result[key] = selection; - } - return result; - }, - { server: serverIndex }, - ); - } -} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx new file mode 100644 index 000000000..16a48beca --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx @@ -0,0 +1,21 @@ +'use client' + +import { DocumentBlock } from "@gitbook/api"; +import { OpenAPIOperationData } from "@gitbook/react-openapi"; +import {OpenAPIContextProvider} from "@gitbook/react-openapi/client"; +import { useRouter, useSearchParams } from 'next/navigation'; +import * as React from 'react'; + +export default function OpenAPIContext(props: { children: React.ReactNode; block: DocumentBlock; data: OpenAPIOperationData }) { + const { block, children, data } = props; + const [isPending, startTransition] = React.useTransition(); + const router = useRouter(); + const searchParams = useSearchParams(); + + return { + startTransition(() => { + const queryParams = new URLSearchParams(params ?? ''); + router.replace(`?${queryParams}`, { scroll: false }); + }) + }}>{children} +} \ No newline at end of file diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 98dd20f97..052d9abb2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,7 +34,6 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; - searchParams: Record; }) { const { space, @@ -45,7 +44,6 @@ export function PageBody(props: { page, document, withPageFeedback, - searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -98,7 +96,6 @@ export function PageBody(props: { resolveContentRef: (ref, options) => resolveContentRef(ref, context, options), shouldHighlightCode, - searchParams, }} /> ) : ( diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index fa08009bc..ba2de7a13 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -6,7 +6,8 @@ "types": "./dist/index.d.ts", "development": "./src/index.ts", "default": "./dist/index.js" - } + }, + "./client": "./src/client.ts" }, "version": "0.6.0", "dependencies": { diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index eff0c9154..b3021263a 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -4,10 +4,9 @@ import { CodeSampleInput, codeSampleGenerators } from './code-samples'; import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample'; import { InteractiveSection } from './InteractiveSection'; -import { getServersURL } from './OpenAPIServerURL'; import { ScalarApiButton } from './ScalarApiButton'; import { OpenAPIContextProps } from './types'; -import { noReference } from './utils'; +import { getServersURL, noReference } from './utils'; /** * Display code samples to execute the operation. diff --git a/packages/react-openapi/src/OpenAPIContextProvider.tsx b/packages/react-openapi/src/OpenAPIContextProvider.tsx new file mode 100644 index 000000000..3ee32c4f4 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIContextProvider.tsx @@ -0,0 +1,52 @@ +'use client' + +import * as React from 'react'; +import { OpenAPIOperationData } from './fetchOpenAPIOperation'; +import { getServersURL } from './utils'; + +type OpenAPIContextProps = { isPending?: boolean; serverUrl?: string; state?: Record | null, onUpdate: (params: Record | null) => void; } +const OpenAPIContext = React.createContext(null); + +export function useOpenAPIContext() { + return React.useContext(OpenAPIContext); +} + +export function OpenAPIContextProvider(props: { children: React.ReactNode; data: OpenAPIOperationData; isPending?: boolean; params?: Record; onUpdate: OpenAPIContextProps['onUpdate']; }) { + const { children, data, isPending, params, onUpdate } = props; + + const clientState = React.useMemo(() => { + if (!params) { return null; } + return parseClientStateModifiers(data, params); + }, [data, params]); + const serverUrl = getServersURL(data.servers, clientState ?? undefined) + + return {children} +} + + +function parseClientStateModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return null; + } + const serverQueryParam = params['server']; + const serverIndex = + serverQueryParam && !isNaN(Number(serverQueryParam)) + ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + return server ? Object.keys(server.variables ?? {}).reduce>( + (result, key) => { + const selection = params[key]; + if (!isNaN(Number(selection))) { + result[key] = selection; + } + return result; + }, + { server: `${serverIndex}`, edit: params['edit'] }, + ) : null; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index f722ce213..b0a2b0325 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; +import { OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -25,11 +25,10 @@ export async function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, - enumSelectors: context.enumSelectors, }; return ( - +

diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 14d245aa4..6b90a5c86 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,9 +1,12 @@ +'use client'; + import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; -import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { parseServerURL } from './utils'; /** * Show the url of the server with variables replaced by their default values. @@ -14,9 +17,12 @@ export function OpenAPIServerURL(props: { path?: string; }) { const { path, servers, context } = props; - const serverIndex = context.enumSelectors?.server ?? 0; + const ctx = useOpenAPIContext(); + + const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); + console.log({ ctxState: ctx?.state }) return ( @@ -33,7 +39,8 @@ export function OpenAPIServerURL(props: { key={i} name={part.name} variable={server.variables[part.name]} - enumIndex={context.enumSelectors?.[part.name]} + selectionIndex={Number(ctx?.state?.[part.name])} + selectable={Boolean(ctx?.state?.edit)} /> ); } @@ -42,40 +49,3 @@ export function OpenAPIServerURL(props: { ); } - -/** - * Get the default URL for the server. - */ -export function getServersURL( - servers: OpenAPIV3.ServerObject[], - selectors?: Record, -): string { - const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; - const server = servers[serverIndex]; - const parts = parseServerURL(server?.url ?? ''); - - return parts - .map((part) => { - if (part.kind === 'text') { - return part.text; - } else { - return selectors && !isNaN(selectors[part.name]) - ? server.variables?.[part.name]?.enum?.[selectors[part.name]] - : (server.variables?.[part.name]?.default ?? `{${part.name}}`); - } - }) - .join(''); -} - -function parseServerURL(url: string) { - const parts = url.split(/{([^}]+)}/g); - const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; - for (let i = 0; i < parts.length; i++) { - if (i % 2 === 0) { - result.push({ kind: 'text', text: parts[i] }); - } else { - result.push({ kind: 'variable', name: parts[i] }); - } - } - return result; -} diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 5b16213a7..36ee6df57 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -1,10 +1,10 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; export function ServerURLForm(props: { children: React.ReactNode; @@ -13,50 +13,59 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const router = useRouter(); - const [isPending, startTransition] = React.useTransition(); - + const ctx = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { - startTransition(() => { - if (index !== serverIndex) { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${index ?? '0'}`, - ); - router.push(`?${params}`, { scroll: false }); - } - }); + if (index !== serverIndex) { + update({ + server: `${index}` ?? '0', + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } } function updateServerVariables(formData: FormData) { - startTransition(() => { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, - ); - const variableKeys = Object.keys(server.variables ?? {}); - for (const pair of formData.entries()) { - if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { - params.set(pair[0], `${pair[1]}`); - } + const variableKeys = Object.keys(server.variables ?? {}); + const variables: Record = {}; + for (const pair of formData) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + variables[pair[0]] = `${pair[1]}`; } - router.push(`?${params}`, { scroll: false }); + } + update({ + server: `${formData.get('server')}` ?? '0', + ...variables, + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } + + function update(variables?: Record) { + if (!context.blockKey) { return; } + ctx?.onUpdate({ + block: context.blockKey, + ...variables, }); } return ( -
-
+ { e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents"> +
{children} - {servers.length > 1 ? ( + {ctx?.state?.edit && servers.length > 1 ? ( ) : null} +
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 5bc9d62bc..83a316269 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,9 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; -import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -11,18 +9,23 @@ import { OpenAPIClientContext } from './types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; - enumIndex?: number; + selectionIndex?: number; + selectable: boolean; }) { - const { enumIndex, name, variable } = props; + const { selectable, selectionIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { + if (!selectable) { + return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + } + return ( - v === variable.default) } /> @@ -35,7 +38,7 @@ export function OpenAPIServerURLVariable(props: { /** * Render a select if there is an enum for a Server URL variable */ -function EnumSelect(props: { +function VariableSelector(props: { value?: number; name: string; variable: OpenAPIV3.ServerVariableObject; diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 60b7f1586..dcf1d7a70 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -12,6 +12,8 @@ import { import React from 'react'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; const ApiClientReact = React.lazy(async () => { const mod = await import('@scalar/api-client-react'); @@ -55,8 +57,10 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { - const { children, serverUrl } = props; +export function ScalarApiClient(props: { children: React.ReactNode; }) { + const { children } = props; + + const ctx = useOpenAPIContext(); const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: serverUrl ?? operationData.servers[0]?.url, + url: ctx?.serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active, serverUrl]); + }, [active, ctx?.state?.serverUrl]); return ( diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts new file mode 100644 index 000000000..923168320 --- /dev/null +++ b/packages/react-openapi/src/client.ts @@ -0,0 +1,2 @@ +'use client'; +export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 5bc3ee092..c94d11df2 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -11,3 +11,41 @@ export function noReference(input: T | OpenAPIV3.ReferenceObject): T { export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } + + +/** + * Get the default URL for the server. + */ +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { + const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; + const parts = parseServerURL(server?.url ?? ''); + + return parts + .map((part) => { + if (part.kind === 'text') { + return part.text; + } else { + return selectors && !isNaN(Number(selectors[part.name])) + ? server.variables?.[part.name]?.enum?.[Number(selectors[part.name])] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); + } + }) + .join(''); +} + +export function parseServerURL(url: string) { + const parts = url.split(/{([^}]+)}/g); + const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; + for (let i = 0; i < parts.length; i++) { + if (i % 2 === 0) { + result.push({ kind: 'text', text: parts[i] }); + } else { + result.push({ kind: 'variable', name: parts[i] }); + } + } + return result; +} \ No newline at end of file From a6adfaaf27049be679cfd5491494bee411e1ac8d Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 11:27:27 +0100 Subject: [PATCH 19/55] Add icons for editing url --- .../components/DocumentView/OpenAPI/OpenAPI.tsx | 2 ++ .../components/DocumentView/OpenAPI/style.css | 17 ++++++++++++++--- packages/react-openapi/src/OpenAPIOperation.tsx | 2 +- .../react-openapi/src/OpenAPIServerURLForm.tsx | 5 +++-- packages/react-openapi/src/ServerSelector.tsx | 4 ++-- packages/react-openapi/src/types.ts | 2 ++ 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 05c094467..1c907c928 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -54,6 +54,8 @@ async function OpenAPIBody(props: BlockProps) { icons: { chevronDown: , chevronRight: , + edit: , + clear: }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 6bd0206ad..cd2c8b5ca 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -57,7 +57,7 @@ /** URL */ .openapi-url { - @apply font-mono text-sm text-dark/8 dark:text-light/8; + @apply flex items-center font-mono text-sm text-dark/8 dark:text-light/8; } .openapi-url-var { @@ -365,6 +365,17 @@ @apply mb-0; } -.openapi-server-button { - @apply disabled:opacity-5; +.openapi-select-button, +.openapi-edit-button { + @apply p-0.5 inline-flex text-dark/6 dark:text-light/6 hover:opacity-8; +} + +.openapi-edit-button { @apply p-0.5 ml-4 size-4; } + +.openapi-edit-button > * { + @apply size-full; +} + +.openapi-select-button { + @apply leading-[1cap] disabled:opacity-5; } diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index b0a2b0325..e4a643303 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -37,7 +37,7 @@ export async function OpenAPIOperation(props: { {operation.description ? ( ) : null} -
+
1 || server.variables; return (
{ e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents">
@@ -61,11 +62,11 @@ export function ServerURLForm(props: { onChange={switchServer} /> ) : null} - + }} title={ctx?.state?.edit ? undefined : "Try different server options"} aria-label={ctx?.state?.edit ? "Clear" : "Edit"}>{ctx?.state?.edit ? context.icons.clear : context.icons.edit} : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index 53051e3b8..bd2997619 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -18,7 +18,7 @@ export function ServerSelector(props: { : null} + {isEditable ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 83a316269..27e82a00c 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -16,7 +16,13 @@ export function OpenAPIServerURLVariable(props: { if (variable.enum && variable.enum.length > 0) { if (!selectable) { - return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + return ( + + {!isNaN(Number(selectionIndex)) + ? variable.enum[Number(selectionIndex)] + : variable.default} + + ); } return ( diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index dcf1d7a70..af6257fca 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -57,7 +57,7 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; }) { +export function ScalarApiClient(props: { children: React.ReactNode }) { const { children } = props; const ctx = useOpenAPIContext(); diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts index 923168320..17e54c128 100644 --- a/packages/react-openapi/src/client.ts +++ b/packages/react-openapi/src/client.ts @@ -1,2 +1,2 @@ 'use client'; -export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file +export { OpenAPIContextProvider } from './OpenAPIContextProvider'; diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index c94d11df2..d2d66e2be 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -12,7 +12,6 @@ export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } - /** * Get the default URL for the server. */ @@ -20,7 +19,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const serverIndex = + selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); @@ -48,4 +48,4 @@ export function parseServerURL(url: string) { } } return result; -} \ No newline at end of file +} From 96d55370870830edd6e8e5d6310aba6a566d4e03 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:37:40 +0100 Subject: [PATCH 22/55] Add server url cache to update request code sample --- .../src/app/(space)/(content)/[[...pathname]]/page.tsx | 3 +++ .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 4 ++++ .../src/components/DocumentView/OpenAPI/ServerUrlCache.tsx | 5 +++++ packages/react-openapi/src/OpenAPICodeSample.tsx | 3 ++- packages/react-openapi/src/OpenAPIServerURL.tsx | 1 - packages/react-openapi/src/OpenAPIServerURLForm.tsx | 3 ++- packages/react-openapi/src/types.ts | 5 ++--- 7 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..4b126ee90 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -3,6 +3,7 @@ import { Metadata, Viewport } from 'next'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; +import { serverUrlCache } from '@/components/DocumentView/OpenAPI/ServerUrlCache'; import { PageAside } from '@/components/PageAside'; import { PageBody, PageCover } from '@/components/PageBody'; import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links'; @@ -26,6 +27,8 @@ export default async function Page(props: { }) { const { params, searchParams } = props; + serverUrlCache.parse(searchParams); + const { content: contentPointer, contentTarget, diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index bf18e3be5..7fd367184 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -8,6 +8,7 @@ import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; import OpenAPIContext from './OpenAPIContext'; +import { serverUrlCache } from './ServerUrlCache'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -46,6 +47,8 @@ async function OpenAPIBody(props: BlockProps) { return null; } + const serverUrl = serverUrlCache.get('serverUrl'); + return ( ) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, + serverUrl }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx new file mode 100644 index 000000000..b715ce3db --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -0,0 +1,5 @@ +import { createSearchParamsCache, parseAsString } from 'nuqs/server'; + +export const serverUrlCache = createSearchParamsCache({ + serverUrl: parseAsString +}) \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index b3021263a..ac63e4779 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -48,11 +48,12 @@ export function OpenAPICodeSample(props: { } }); + const serverUrl = context.serverUrl ?? getServersURL(data.servers); const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { url: - getServersURL(data.servers, context.enumSelectors) + + serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index ec225c430..5bce96035 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -22,7 +22,6 @@ export function OpenAPIServerURL(props: { const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); - console.log({ ctxState: ctx?.state }); return ( diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 98aa20534..8864f9b2d 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -5,6 +5,7 @@ import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; export function ServerURLForm(props: { children: React.ReactNode; @@ -80,7 +81,7 @@ export function ServerURLForm(props: { update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? undefined : { edit: 'true' }), + ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), }); }} title={ctx?.state?.edit ? undefined : 'Try different server options'} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index e8297efa4..415bf8e49 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -27,11 +27,10 @@ export interface OpenAPIClientContext { * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; - /** - * Selectors to update openapi enums, e.g. for server url variables + * Optional serverUrl to use with OpenAPI operations */ - enumSelectors?: Record; + serverUrl?: string | null; } export interface OpenAPIFetcher { From 768b555f6678ce1b9099c37a7083266375f3ef3e Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:39:45 +0100 Subject: [PATCH 23/55] Formatting --- .../DocumentView/OpenAPI/OpenAPI.tsx | 2 +- .../DocumentView/OpenAPI/ServerUrlCache.tsx | 4 +-- .../react-openapi/src/OpenAPICodeSample.tsx | 5 +--- .../src/OpenAPIServerURLForm.tsx | 26 +++++++++++-------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 7fd367184..16dccc025 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -64,7 +64,7 @@ async function OpenAPIBody(props: BlockProps) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, - serverUrl + serverUrl, }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx index b715ce3db..47ae56d22 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -1,5 +1,5 @@ import { createSearchParamsCache, parseAsString } from 'nuqs/server'; export const serverUrlCache = createSearchParamsCache({ - serverUrl: parseAsString -}) \ No newline at end of file + serverUrl: parseAsString, +}); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index ac63e4779..6345dfe29 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -52,10 +52,7 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { - url: - serverUrl + - data.path + - (searchParams.size ? `?${searchParams.toString()}` : ''), + url: serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true }) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 8864f9b2d..45b277a69 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -14,7 +14,7 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const ctx = useOpenAPIContext(); + const stateContext = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); @@ -22,7 +22,7 @@ export function ServerURLForm(props: { if (index !== serverIndex) { update({ server: `${index}` ?? '0', - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } } @@ -38,7 +38,7 @@ export function ServerURLForm(props: { update({ server: `${formData.get('server')}` ?? '0', ...variables, - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } @@ -46,7 +46,7 @@ export function ServerURLForm(props: { if (!context.blockKey) { return; } - ctx?.onUpdate({ + stateContext?.onUpdate({ block: context.blockKey, ...variables, }); @@ -62,10 +62,10 @@ export function ServerURLForm(props: { }} className="contents" > -
+
{children} - {ctx?.state?.edit && servers.length > 1 ? ( + {stateContext?.state?.edit && servers.length > 1 ? ( { - const state = { ...ctx?.state }; + const state = { ...stateContext?.state }; delete state.edit; update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), + ...(stateContext?.state?.edit + ? { serverUrl: getServersURL(servers, state) } + : { edit: 'true' }), }); }} - title={ctx?.state?.edit ? undefined : 'Try different server options'} - aria-label={ctx?.state?.edit ? 'Clear' : 'Edit'} + title={ + stateContext?.state?.edit ? undefined : 'Try different server options' + } + aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {ctx?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} ) : null}
From ea4f70ec883b155c16e1410634e3543586ba99c9 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 12:05:59 +0100 Subject: [PATCH 24/55] Update icon --- .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 2 +- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 2 +- packages/react-openapi/src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 16dccc025..08fe0be58 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -58,7 +58,7 @@ async function OpenAPIBody(props: BlockProps) { chevronDown: , chevronRight: , edit: , - clear: , + editDone: , }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 45b277a69..6dc16c601 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -91,7 +91,7 @@ export function ServerURLForm(props: { } aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.editDone : context.icons.edit} ) : null}
diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 415bf8e49..7be2deb50 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -9,7 +9,7 @@ export interface OpenAPIClientContext { chevronDown: React.ReactNode; chevronRight: React.ReactNode; edit: React.ReactNode; - clear: React.ReactNode; + editDone: React.ReactNode; }; /** From f6b75e7dbde74e8b7184750298f2d0b6801b8117 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 16:35:31 +0100 Subject: [PATCH 25/55] Self review changes --- packages/react-openapi/src/OpenAPIOperation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index e4a643303..a37e33614 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton'; /** * Display an interactive OpenAPI operation. */ -export async function OpenAPIOperation(props: { +export function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; context: OpenAPIContextProps; From 8d1c349ed1ca641d4df2741e0f5b477b49017202 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Wed, 25 Sep 2024 09:36:37 +0100 Subject: [PATCH 26/55] Don't make server url editable if no update function or context --- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 6dc16c601..5abfad838 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -52,7 +52,7 @@ export function ServerURLForm(props: { }); } - const isEditable = servers.length > 1 || server.variables; + const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); return (
Date: Wed, 2 Oct 2024 11:44:10 +0100 Subject: [PATCH 27/55] Refactor - naming and comments --- .../DocumentView/OpenAPI/OpenAPI.tsx | 12 +++-- ...xt.tsx => OpenAPIClientStateContainer.tsx} | 19 ++++--- .../components/DocumentView/OpenAPI/style.css | 4 ++ ...ider.tsx => OpenAPIClientStateContext.tsx} | 52 +++++++++++++------ .../react-openapi/src/OpenAPIServerURL.tsx | 50 ++++++++++-------- .../src/OpenAPIServerURLForm.tsx | 21 ++++---- .../react-openapi/src/ScalarApiButton.tsx | 9 ++-- packages/react-openapi/src/ServerSelector.tsx | 6 +-- packages/react-openapi/src/client.ts | 2 +- 9 files changed, 106 insertions(+), 69 deletions(-) rename packages/gitbook/src/components/DocumentView/OpenAPI/{OpenAPIContext.tsx => OpenAPIClientStateContainer.tsx} (66%) rename packages/react-openapi/src/{OpenAPIContextProvider.tsx => OpenAPIClientStateContext.tsx} (50%) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 08fe0be58..a70d0f1f4 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -7,7 +7,7 @@ import { LoadingPane } from '@/components/primitives'; import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; -import OpenAPIContext from './OpenAPIContext'; +import OpenAPIClientStateContainer from './OpenAPIClientStateContainer'; import { serverUrlCache } from './ServerUrlCache'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -19,7 +19,7 @@ import './scalar.css'; * Render an OpenAPI block. */ export async function OpenAPI(props: BlockProps) { - const { block, style } = props; + const { style } = props; return (
}> @@ -47,10 +47,12 @@ async function OpenAPIBody(props: BlockProps) { return null; } - const serverUrl = serverUrlCache.get('serverUrl'); + // To update the code sample we need to re-render the server component + // so reading the cached value from search params + const serverUrl = serverUrlCache.get('serverUrl'); return ( - + ) { }} className="openapi-block" /> - + ); } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx similarity index 66% rename from packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx rename to packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx index de4b2d840..743fb79e1 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx @@ -2,24 +2,29 @@ import { DocumentBlock } from '@gitbook/api'; import { OpenAPIOperationData } from '@gitbook/react-openapi'; -import { OpenAPIContextProvider } from '@gitbook/react-openapi/client'; +import { OpenAPIClientState } from '@gitbook/react-openapi/client'; import { useRouter, useSearchParams } from 'next/navigation'; +import { OpenAPIV3 } from 'openapi-types'; import * as React from 'react'; -export default function OpenAPIContext(props: { +/** + * Client component that wraps `OpenAPIClientState` so we can + * use some hooks (e.g. useRouter) in the `onUpdate` callback. + */ +export default function OpenAPIClientStateContainer(props: { children: React.ReactNode; block: DocumentBlock; - data: OpenAPIOperationData; + servers: OpenAPIV3.ServerObject[]; }) { - const { block, children, data } = props; + const { block, children, servers } = props; const [isPending, startTransition] = React.useTransition(); const router = useRouter(); const searchParams = useSearchParams(); return ( - {children} - + ); } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 54ef1adaf..56e2295bb 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -381,3 +381,7 @@ .openapi-select-button { @apply leading-[1cap] disabled:opacity-5; } + +.openapi-pending { + @apply opacity-5; +} diff --git a/packages/react-openapi/src/OpenAPIContextProvider.tsx b/packages/react-openapi/src/OpenAPIClientStateContext.tsx similarity index 50% rename from packages/react-openapi/src/OpenAPIContextProvider.tsx rename to packages/react-openapi/src/OpenAPIClientStateContext.tsx index b09ee2933..e5d213dd0 100644 --- a/packages/react-openapi/src/OpenAPIContextProvider.tsx +++ b/packages/react-openapi/src/OpenAPIClientStateContext.tsx @@ -3,38 +3,56 @@ import * as React from 'react'; import { OpenAPIOperationData } from './fetchOpenAPIOperation'; import { getServersURL } from './utils'; +import { OpenAPIV3 } from 'openapi-types'; -type OpenAPIContextProps = { +type OpenAPIClientStateContextProps = { + /** + * Whether client state updates are in a pending state, + * i.e. is a transition in progress. + */ isPending?: boolean; + /** + * The server url + */ serverUrl?: string; + /** + * The current state + */ state?: Record | null; + /** + * Callback for when the client state is updated + */ onUpdate: (params: Record | null) => void; }; -const OpenAPIContext = React.createContext(null); -export function useOpenAPIContext() { - return React.useContext(OpenAPIContext); +const OpenAPIClientStateContext = React.createContext(null); + +export function useOpenAPIClientState() { + return React.useContext(OpenAPIClientStateContext); } -export function OpenAPIContextProvider(props: { +/** + * Control client state for an OpenAPI operation + */ +export function OpenAPIClientState(props: { children: React.ReactNode; - data: OpenAPIOperationData; + servers: OpenAPIV3.ServerObject[]; isPending?: boolean; params?: Record; - onUpdate: OpenAPIContextProps['onUpdate']; + onUpdate: OpenAPIClientStateContextProps['onUpdate']; }) { - const { children, data, isPending, params, onUpdate } = props; + const { children, servers, isPending, params, onUpdate } = props; const clientState = React.useMemo(() => { if (!params) { return null; } - return parseClientStateModifiers(data, params); - }, [data, params]); - const serverUrl = getServersURL(data.servers, clientState ?? undefined); + return parseClientStateModifiers(servers, params); + }, [servers, params]); + const serverUrl = getServersURL(servers, clientState ?? undefined); return ( - {children} - + ); } -function parseClientStateModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { +function parseClientStateModifiers(servers: OpenAPIV3.ServerObject[], params: Record) { + if (!servers) { return null; } const serverQueryParam = params['server']; const serverIndex = serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) + ? Math.max(0, Math.min(Number(serverQueryParam), servers.length - 1)) : 0; - const server = data.servers[serverIndex]; + const server = servers[serverIndex]; return server ? Object.keys(server.variables ?? {}).reduce>( (result, key) => { diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 5bce96035..786f49120 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -2,10 +2,12 @@ import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; +import classNames from 'classnames'; + import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; import { parseServerURL } from './utils'; /** @@ -17,34 +19,38 @@ export function OpenAPIServerURL(props: { path?: string; }) { const { path, servers, context } = props; - const ctx = useOpenAPIContext(); + const stateContext = useOpenAPIClientState(); - const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; + const serverIndex = !isNaN(Number(stateContext?.state?.server)) ? Number(stateContext?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); + if (!server) { return null; } + return ( - {parts.map((part, i) => { - if (part.kind === 'text') { - return {part.text}; - } else { - if (!server.variables?.[part.name]) { - return {`{${part.name}}`}; - } + + {parts.map((part, i) => { + if (part.kind === 'text') { + return {part.text}; + } else { + if (!server.variables?.[part.name]) { + return {`{${part.name}}`}; + } - return ( - - ); - } - })} - {path} + return ( + + ); + } + })} + {path} + ); } diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 5abfad838..9df610dd5 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; import { getServersURL } from './utils'; export function ServerURLForm(props: { @@ -14,7 +14,7 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const stateContext = useOpenAPIContext(); + const stateContext = useOpenAPIClientState(); const server = servers[serverIndex]; const formRef = React.useRef(null); @@ -52,7 +52,10 @@ export function ServerURLForm(props: { }); } - const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); + // Only make the server url editable if there is some onUpdate callback + // and if there are variations on the server url (e.g. an array of servers or url variables). + const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); + const isEditing = isEditable && stateContext?.state?.edit; return ( {children} - {stateContext?.state?.edit && servers.length > 1 ? ( + {isEditing && servers.length > 1 ? ( @@ -81,17 +84,17 @@ export function ServerURLForm(props: { update({ server: `${serverIndex}`, ...state, - ...(stateContext?.state?.edit + ...(isEditing ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), }); }} title={ - stateContext?.state?.edit ? undefined : 'Try different server options' + isEditing ? undefined : 'Try different server options' } - aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} + aria-label={isEditing ? 'Clear' : 'Edit'} > - {stateContext?.state?.edit ? context.icons.editDone : context.icons.edit} + {isEditing ? context.icons.editDone : context.icons.edit} ) : null} diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index af6257fca..ca0a9869c 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -12,8 +12,7 @@ import { import React from 'react'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; -import { getServersURL } from './utils'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; const ApiClientReact = React.lazy(async () => { const mod = await import('@scalar/api-client-react'); @@ -60,7 +59,7 @@ export function ScalarApiButton(props: { export function ScalarApiClient(props: { children: React.ReactNode }) { const { children } = props; - const ctx = useOpenAPIContext(); + const stateCtx = useOpenAPIClientState(); const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: ctx?.serverUrl ?? operationData.servers[0]?.url, + url: stateCtx?.serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active, ctx?.state?.serverUrl]); + }, [active, stateCtx?.serverUrl]); return ( diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index bd2997619..17037ffbf 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -4,10 +4,10 @@ import * as React from 'react'; export function ServerSelector(props: { currentIndex: number; + lastIndex: number; onChange: (value: number) => void; - servers: any[]; }) { - const { currentIndex, onChange, servers } = props; + const { currentIndex, onChange, lastIndex } = props; const [index, setIndex] = React.useState(currentIndex); React.useEffect(() => { @@ -32,7 +32,7 @@ export function ServerSelector(props: {
); } + +function parseModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return; + } + const { servers } = params; + const serverIndex = + servers && !isNaN(Number(servers)) + ? Math.min(0, Math.max(Number(servers), servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + if (server && server.variables) { + return Object.keys(server.variables).reduce>( + (result, key) => { + const selection = Number(params[key]); + if (!isNaN(selection)) { + result[key] = selection; + } + return result; + }, + { servers: serverIndex }, + ); + } +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 05440f9d9..98dd20f97 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,6 +34,7 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; + searchParams: Record; }) { const { space, @@ -44,6 +45,7 @@ export function PageBody(props: { page, document, withPageFeedback, + searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -54,7 +56,6 @@ export function PageBody(props: { 'siteId' in contentPointer ? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId } : undefined; - return ( <>
resolveContentRef(ref, context, options), shouldHighlightCode, + searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 265d57207..670d2712d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -54,7 +54,7 @@ export function OpenAPICodeSample(props: { const input: CodeSampleInput = { url: - getServersURL(data.servers) + + getServersURL(data.servers, context.enumSelectors) + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 365cede51..a6f495097 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPIServerURL } from './OpenAPIServerURL'; +import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton'; /** * Display an interactive OpenAPI operation. */ -export function OpenAPIOperation(props: { +export async function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; context: OpenAPIContextProps; @@ -25,8 +25,10 @@ export function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, + enumSelectors: context.enumSelectors, }; + const config = await getConfiguration(context); return (
@@ -47,7 +49,7 @@ export function OpenAPIOperation(props: { {method.toUpperCase()} - + {path}
@@ -67,3 +69,17 @@ export function OpenAPIOperation(props: {
); } + +async function getConfiguration(context: OpenAPIContextProps) { + const response = await fetch(context.specUrl); + const doc = await response.json(); + + return { + spec: { + content: { + ...doc, + servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], + }, + }, + }; +} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index e81fb47ba..79973d621 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,18 +1,23 @@ import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; +import { OpenAPIClientContext } from './types'; +import { ServerURLForm } from './OpenAPIServerURLForm'; /** * Show the url of the server with variables replaced by their default values. */ -export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { - const { servers } = props; - const server = servers[0]; - +export function OpenAPIServerURL(props: { + servers: OpenAPIV3.ServerObject[]; + context: OpenAPIClientContext; +}) { + const { servers, context } = props; + const serverIndex = context.enumSelectors?.servers ?? 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { if (part.kind === 'text') { return {part.text}; @@ -26,18 +31,22 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) { key={i} name={part.name} variable={server.variables[part.name]} + enumIndex={context.enumSelectors?.[part.name]} /> ); } })} - + ); } /** * Get the default URL for the server. */ -export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { const server = servers[0]; const parts = parseServerURL(server?.url ?? ''); @@ -46,7 +55,9 @@ export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { if (part.kind === 'text') { return part.text; } else { - return server.variables?.[part.name]?.default ?? `{${part.name}}`; + return selectors && !isNaN(selectors[part.name]) + ? server.variables?.[part.name]?.enum?.[selectors[part.name]] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); } }) .join(''); diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx new file mode 100644 index 000000000..565108417 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { OpenAPIClientContext } from './types'; +import { OpenAPIV3 } from 'openapi-types'; +import { useApiClientModal } from '@scalar/api-client-react'; + +export function ServerURLForm(props: { + children: React.ReactNode; + context: OpenAPIClientContext; + server: OpenAPIV3.ServerObject; +}) { + const { children, context, server } = props; + const router = useRouter(); + const client = useApiClientModal(); + const [isPending, startTransition] = React.useTransition(); + + function updateServerUrl(formData: FormData) { + startTransition(() => { + if (!server.variables) { + return; + } + let params = new URLSearchParams(`block=${context.blockKey}`); + const variableKeys = Object.keys(server.variables); + for (const pair of formData.entries()) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + params.set(pair[0], `${pair[1]}`); + } + } + router.push(`?${params}`, { scroll: false }); + }); + } + + return ( + +
+ + {children} +
+ + ); +} diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 7bf93c997..5bc9d62bc 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,8 +1,9 @@ 'use client'; - import * as React from 'react'; +import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; +import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -10,27 +11,53 @@ import { OpenAPIV3 } from 'openapi-types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; + enumIndex?: number; }) { - const { variable } = props; + const { enumIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { - return (); - + return ( + v === variable.default) + } + /> + ); } + return {variable.default}; } + +/** + * Render a select if there is an enum for a Server URL variable + */ +function EnumSelect(props: { + value?: number; + name: string; + variable: OpenAPIV3.ServerVariableObject; +}) { + const { value, name, variable } = props; + return ( + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 156a28ac0..b278f2efd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -21,6 +21,10 @@ export interface OpenAPIClientContext { blockKey?: string; /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; + + blockKey?: string; + + enumSelectors?: Record; } export interface OpenAPIFetcher { From 77d64d06b930ef07e4f96a87510e42920593be80 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:46:18 +0100 Subject: [PATCH 34/55] Update server choice based on selection --- .../DocumentView/OpenAPI/OpenAPI.tsx | 13 ++++---- .../components/DocumentView/OpenAPI/style.css | 4 +++ .../react-openapi/src/OpenAPICodeSample.tsx | 1 - .../react-openapi/src/OpenAPIOperation.tsx | 20 ++---------- .../react-openapi/src/OpenAPIServerURL.tsx | 15 +++++---- .../src/OpenAPIServerURLForm.tsx | 32 ++++++++++++------- .../react-openapi/src/ScalarApiButton.tsx | 8 ++--- packages/react-openapi/src/ServerSelector.tsx | 26 +++++++++++++++ packages/react-openapi/src/types.ts | 11 +++++-- 9 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 packages/react-openapi/src/ServerSelector.tsx diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index ee96a211e..438c3ca27 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -49,6 +49,7 @@ async function OpenAPIBody(props: BlockProps) { context.searchParams && context.searchParams.block === block.key ? parseModifiers(data, context.searchParams) : undefined; + return ( >( + if (server) { + return Object.keys(server.variables ?? {}).reduce>( (result, key) => { const selection = Number(params[key]); if (!isNaN(selection)) { @@ -116,7 +117,7 @@ function parseModifiers(data: OpenAPIOperationData, params: Record *:last-child { @apply mb-0; } + +.openapi-server-button { + @apply disabled:opacity-5; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 670d2712d..eff0c9154 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -51,7 +51,6 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; - const input: CodeSampleInput = { url: getServersURL(data.servers, context.enumSelectors) + diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index a6f495097..fb1daa951 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -28,9 +28,8 @@ export async function OpenAPIOperation(props: { enumSelectors: context.enumSelectors, }; - const config = await getConfiguration(context); return ( - +

@@ -49,8 +48,7 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - - {path} +

@@ -69,17 +67,3 @@ export async function OpenAPIOperation(props: {
); } - -async function getConfiguration(context: OpenAPIContextProps) { - const response = await fetch(context.specUrl); - const doc = await response.json(); - - return { - spec: { - content: { - ...doc, - servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], - }, - }, - }; -} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 79973d621..c30517a9d 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -3,6 +3,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; +import { ServerSelector } from './ServerSelector'; /** * Show the url of the server with variables replaced by their default values. @@ -10,16 +11,17 @@ import { ServerURLForm } from './OpenAPIServerURLForm'; export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[]; context: OpenAPIClientContext; + path?: string; }) { - const { servers, context } = props; - const serverIndex = context.enumSelectors?.servers ?? 0; + const { path, servers, context } = props; + const serverIndex = context.enumSelectors?.server ?? 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return ( - + {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -35,7 +37,7 @@ export function OpenAPIServerURL(props: { /> ); } - })} + })}{path} ); } @@ -47,7 +49,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const server = servers[0]; + const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); return parts diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 565108417..2818bbbf6 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -4,25 +4,34 @@ import * as React from 'react'; import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; -import { useApiClientModal } from '@scalar/api-client-react'; +import { ServerSelector } from './ServerSelector'; export function ServerURLForm(props: { children: React.ReactNode; context: OpenAPIClientContext; - server: OpenAPIV3.ServerObject; + servers: OpenAPIV3.ServerObject[]; + serverIndex: number; }) { - const { children, context, server } = props; + const { children, context, servers, serverIndex } = props; const router = useRouter(); - const client = useApiClientModal(); const [isPending, startTransition] = React.useTransition(); + + const server = servers[serverIndex]; + const formRef = React.useRef(null); - function updateServerUrl(formData: FormData) { + function switchServer(index: number) { startTransition(() => { - if (!server.variables) { - return; + if (index !== serverIndex) { + let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + router.push(`?${params}`, { scroll: false }); } - let params = new URLSearchParams(`block=${context.blockKey}`); - const variableKeys = Object.keys(server.variables); + }); + } + + function updateServerVariables(formData: FormData) { + startTransition(() => { + let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { params.set(pair[0], `${pair[1]}`); @@ -33,10 +42,11 @@ export function ServerURLForm(props: { } return ( -
+
- {children} + {children} + { servers.length > 1 ? : null }
); diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 02d9ffaa1..60b7f1586 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -55,8 +55,8 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode }) { - const { children } = props; +export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { + const { children, serverUrl } = props; const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: operationData.servers[0]?.url, + url: serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active]); + }, [active, serverUrl]); return ( diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx new file mode 100644 index 000000000..a235b7b67 --- /dev/null +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -0,0 +1,26 @@ +'use client'; + +import * as React from 'react'; + +export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { + const { currentIndex, onChange, servers } = props; + const [index, setIndex] = React.useState(currentIndex); + + React.useEffect(() => { + onChange(index); + }, [index]); + + return + + + + ; +} \ No newline at end of file diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index b278f2efd..215d8ef56 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,15 +15,20 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; + /** * The key of the block */ blockKey?: string; - /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ - id?: string; - blockKey?: string; + /** + * Optional id attached to the OpenAPI Operation heading and used as an anchor + */ + id?: string; + /** + * Selectors to update openapi enums, e.g. for server url variables + */ enumSelectors?: Record; } From 9437aa25e82c1d87218b82a3b2a0552a9c12a425 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:48:47 +0100 Subject: [PATCH 35/55] Format --- .../components/DocumentView/OpenAPI/style.css | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 6 ++- .../react-openapi/src/OpenAPIServerURL.tsx | 5 +- .../src/OpenAPIServerURLForm.tsx | 18 +++++-- packages/react-openapi/src/ServerSelector.tsx | 52 +++++++++++++------ packages/react-openapi/src/types.ts | 4 +- 6 files changed, 62 insertions(+), 25 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 1882a423c..6bd0206ad 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -367,4 +367,4 @@ .openapi-server-button { @apply disabled:opacity-5; -} \ No newline at end of file +} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index fb1daa951..f722ce213 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -48,7 +48,11 @@ export async function OpenAPIOperation(props: { {method.toUpperCase()} - +

diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index c30517a9d..14d245aa4 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -21,7 +21,7 @@ export function OpenAPIServerURL(props: { return ( {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { @@ -37,7 +37,8 @@ export function OpenAPIServerURL(props: { /> ); } - })}{path} + })} + {path} ); } diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 2818bbbf6..5b16213a7 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -15,14 +15,16 @@ export function ServerURLForm(props: { const { children, context, servers, serverIndex } = props; const router = useRouter(); const [isPending, startTransition] = React.useTransition(); - + const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { startTransition(() => { if (index !== serverIndex) { - let params = new URLSearchParams(`block=${context.blockKey}&server=${index ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${index ?? '0'}`, + ); router.push(`?${params}`, { scroll: false }); } }); @@ -30,7 +32,9 @@ export function ServerURLForm(props: { function updateServerVariables(formData: FormData) { startTransition(() => { - let params = new URLSearchParams(`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`); + let params = new URLSearchParams( + `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, + ); const variableKeys = Object.keys(server.variables ?? {}); for (const pair of formData.entries()) { if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { @@ -46,7 +50,13 @@ export function ServerURLForm(props: {
{children} - { servers.length > 1 ? : null } + {servers.length > 1 ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index a235b7b67..53051e3b8 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; -export function ServerSelector(props: { currentIndex: number; onChange: (value:number) => void; servers: any[] }) { +export function ServerSelector(props: { + currentIndex: number; + onChange: (value: number) => void; + servers: any[]; +}) { const { currentIndex, onChange, servers } = props; const [index, setIndex] = React.useState(currentIndex); @@ -10,17 +14,35 @@ export function ServerSelector(props: { currentIndex: number; onChange: (value:n onChange(index); }, [index]); - return - - - - ; -} \ No newline at end of file + return ( + + + + + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 215d8ef56..90d9889bd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -15,13 +15,13 @@ export interface OpenAPIClientContext { * @default false */ defaultInteractiveOpened?: boolean; - + /** * The key of the block */ blockKey?: string; - /** + /** * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; From f6823e92bb5b59afa4ed5db9b87e1db6f198a399 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:58:10 +0100 Subject: [PATCH 36/55] Changeset --- .changeset/long-shrimps-judge.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/long-shrimps-judge.md diff --git a/.changeset/long-shrimps-judge.md b/.changeset/long-shrimps-judge.md new file mode 100644 index 000000000..edae3be68 --- /dev/null +++ b/.changeset/long-shrimps-judge.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': minor +'gitbook': minor +--- + +Allow selection of server url From ac492e094d8fa0e702f1394f578bca712934cfb1 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 09:35:43 +0100 Subject: [PATCH 37/55] Revised client state with context --- .../(content)/[[...pathname]]/page.tsx | 1 - .../components/DocumentView/DocumentView.tsx | 2 - .../DocumentView/OpenAPI/OpenAPI.tsx | 63 +++++------------- .../DocumentView/OpenAPI/OpenAPIContext.tsx | 21 ++++++ .../src/components/PageBody/PageBody.tsx | 3 - packages/react-openapi/package.json | 3 +- .../react-openapi/src/OpenAPICodeSample.tsx | 3 +- .../src/OpenAPIContextProvider.tsx | 52 +++++++++++++++ .../react-openapi/src/OpenAPIOperation.tsx | 5 +- .../react-openapi/src/OpenAPIServerURL.tsx | 50 +++----------- .../src/OpenAPIServerURLForm.tsx | 65 +++++++++++-------- .../src/OpenAPIServerURLVariable.tsx | 19 +++--- .../react-openapi/src/ScalarApiButton.tsx | 12 ++-- packages/react-openapi/src/client.ts | 2 + packages/react-openapi/src/utils.ts | 38 +++++++++++ 15 files changed, 202 insertions(+), 137 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx create mode 100644 packages/react-openapi/src/OpenAPIContextProvider.tsx create mode 100644 packages/react-openapi/src/client.ts diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index 84a925860..b5ab1424e 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -90,7 +90,6 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } - searchParams={searchParams} /> {page.layout.outline ? ( boolean; - - searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 438c3ca27..05c094467 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,12 +1,13 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; +import { OpenAPIOperation } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; +import OpenAPIContext from './OpenAPIContext'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -45,27 +46,23 @@ async function OpenAPIBody(props: BlockProps) { return null; } - const enumSelectors = - context.searchParams && context.searchParams.block === block.key - ? parseModifiers(data, context.searchParams) - : undefined; - return ( - , - chevronRight: , - }, - CodeBlock: PlainCodeBlock, - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - enumSelectors, - blockKey: block.key, - }} - className="openapi-block" - /> + + , + chevronRight: , + }, + CodeBlock: PlainCodeBlock, + defaultInteractiveOpened: context.mode === 'print', + id: block.meta?.id, + blockKey: block.key + }} + className="openapi-block" + /> + ); } @@ -97,27 +94,3 @@ function OpenAPIFallback() { ); } - -function parseModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { - return; - } - const { server: serverQueryParam } = params; - const serverIndex = - serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) - : 0; - const server = data.servers[serverIndex]; - if (server) { - return Object.keys(server.variables ?? {}).reduce>( - (result, key) => { - const selection = Number(params[key]); - if (!isNaN(selection)) { - result[key] = selection; - } - return result; - }, - { server: serverIndex }, - ); - } -} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx new file mode 100644 index 000000000..16a48beca --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx @@ -0,0 +1,21 @@ +'use client' + +import { DocumentBlock } from "@gitbook/api"; +import { OpenAPIOperationData } from "@gitbook/react-openapi"; +import {OpenAPIContextProvider} from "@gitbook/react-openapi/client"; +import { useRouter, useSearchParams } from 'next/navigation'; +import * as React from 'react'; + +export default function OpenAPIContext(props: { children: React.ReactNode; block: DocumentBlock; data: OpenAPIOperationData }) { + const { block, children, data } = props; + const [isPending, startTransition] = React.useTransition(); + const router = useRouter(); + const searchParams = useSearchParams(); + + return { + startTransition(() => { + const queryParams = new URLSearchParams(params ?? ''); + router.replace(`?${queryParams}`, { scroll: false }); + }) + }}>{children} +} \ No newline at end of file diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 98dd20f97..052d9abb2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,7 +34,6 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; - searchParams: Record; }) { const { space, @@ -45,7 +44,6 @@ export function PageBody(props: { page, document, withPageFeedback, - searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -98,7 +96,6 @@ export function PageBody(props: { resolveContentRef: (ref, options) => resolveContentRef(ref, context, options), shouldHighlightCode, - searchParams, }} /> ) : ( diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index fa08009bc..ba2de7a13 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -6,7 +6,8 @@ "types": "./dist/index.d.ts", "development": "./src/index.ts", "default": "./dist/index.js" - } + }, + "./client": "./src/client.ts" }, "version": "0.6.0", "dependencies": { diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index eff0c9154..b3021263a 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -4,10 +4,9 @@ import { CodeSampleInput, codeSampleGenerators } from './code-samples'; import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample'; import { InteractiveSection } from './InteractiveSection'; -import { getServersURL } from './OpenAPIServerURL'; import { ScalarApiButton } from './ScalarApiButton'; import { OpenAPIContextProps } from './types'; -import { noReference } from './utils'; +import { getServersURL, noReference } from './utils'; /** * Display code samples to execute the operation. diff --git a/packages/react-openapi/src/OpenAPIContextProvider.tsx b/packages/react-openapi/src/OpenAPIContextProvider.tsx new file mode 100644 index 000000000..3ee32c4f4 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIContextProvider.tsx @@ -0,0 +1,52 @@ +'use client' + +import * as React from 'react'; +import { OpenAPIOperationData } from './fetchOpenAPIOperation'; +import { getServersURL } from './utils'; + +type OpenAPIContextProps = { isPending?: boolean; serverUrl?: string; state?: Record | null, onUpdate: (params: Record | null) => void; } +const OpenAPIContext = React.createContext(null); + +export function useOpenAPIContext() { + return React.useContext(OpenAPIContext); +} + +export function OpenAPIContextProvider(props: { children: React.ReactNode; data: OpenAPIOperationData; isPending?: boolean; params?: Record; onUpdate: OpenAPIContextProps['onUpdate']; }) { + const { children, data, isPending, params, onUpdate } = props; + + const clientState = React.useMemo(() => { + if (!params) { return null; } + return parseClientStateModifiers(data, params); + }, [data, params]); + const serverUrl = getServersURL(data.servers, clientState ?? undefined) + + return {children} +} + + +function parseClientStateModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return null; + } + const serverQueryParam = params['server']; + const serverIndex = + serverQueryParam && !isNaN(Number(serverQueryParam)) + ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + return server ? Object.keys(server.variables ?? {}).reduce>( + (result, key) => { + const selection = params[key]; + if (!isNaN(Number(selection))) { + result[key] = selection; + } + return result; + }, + { server: `${serverIndex}`, edit: params['edit'] }, + ) : null; +} \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index f722ce213..b0a2b0325 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; +import { OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -25,11 +25,10 @@ export async function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, - enumSelectors: context.enumSelectors, }; return ( - +

diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 14d245aa4..6b90a5c86 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -1,9 +1,12 @@ +'use client'; + import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; -import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { parseServerURL } from './utils'; /** * Show the url of the server with variables replaced by their default values. @@ -14,9 +17,12 @@ export function OpenAPIServerURL(props: { path?: string; }) { const { path, servers, context } = props; - const serverIndex = context.enumSelectors?.server ?? 0; + const ctx = useOpenAPIContext(); + + const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); + console.log({ ctxState: ctx?.state }) return ( @@ -33,7 +39,8 @@ export function OpenAPIServerURL(props: { key={i} name={part.name} variable={server.variables[part.name]} - enumIndex={context.enumSelectors?.[part.name]} + selectionIndex={Number(ctx?.state?.[part.name])} + selectable={Boolean(ctx?.state?.edit)} /> ); } @@ -42,40 +49,3 @@ export function OpenAPIServerURL(props: { ); } - -/** - * Get the default URL for the server. - */ -export function getServersURL( - servers: OpenAPIV3.ServerObject[], - selectors?: Record, -): string { - const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0; - const server = servers[serverIndex]; - const parts = parseServerURL(server?.url ?? ''); - - return parts - .map((part) => { - if (part.kind === 'text') { - return part.text; - } else { - return selectors && !isNaN(selectors[part.name]) - ? server.variables?.[part.name]?.enum?.[selectors[part.name]] - : (server.variables?.[part.name]?.default ?? `{${part.name}}`); - } - }) - .join(''); -} - -function parseServerURL(url: string) { - const parts = url.split(/{([^}]+)}/g); - const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; - for (let i = 0; i < parts.length; i++) { - if (i % 2 === 0) { - result.push({ kind: 'text', text: parts[i] }); - } else { - result.push({ kind: 'variable', name: parts[i] }); - } - } - return result; -} diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 5b16213a7..36ee6df57 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -1,10 +1,10 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; export function ServerURLForm(props: { children: React.ReactNode; @@ -13,50 +13,59 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const router = useRouter(); - const [isPending, startTransition] = React.useTransition(); - + const ctx = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); function switchServer(index: number) { - startTransition(() => { - if (index !== serverIndex) { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${index ?? '0'}`, - ); - router.push(`?${params}`, { scroll: false }); - } - }); + if (index !== serverIndex) { + update({ + server: `${index}` ?? '0', + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } } function updateServerVariables(formData: FormData) { - startTransition(() => { - let params = new URLSearchParams( - `block=${context.blockKey}&server=${formData.get('server') ?? '0'}`, - ); - const variableKeys = Object.keys(server.variables ?? {}); - for (const pair of formData.entries()) { - if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { - params.set(pair[0], `${pair[1]}`); - } + const variableKeys = Object.keys(server.variables ?? {}); + const variables: Record = {}; + for (const pair of formData) { + if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) { + variables[pair[0]] = `${pair[1]}`; } - router.push(`?${params}`, { scroll: false }); + } + update({ + server: `${formData.get('server')}` ?? '0', + ...variables, + ...(ctx?.state?.edit ? { edit: 'true' } : undefined) + }); + } + + function update(variables?: Record) { + if (!context.blockKey) { return; } + ctx?.onUpdate({ + block: context.blockKey, + ...variables, }); } return ( -
-
+ { e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents"> +
{children} - {servers.length > 1 ? ( + {ctx?.state?.edit && servers.length > 1 ? ( ) : null} +
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 5bc9d62bc..83a316269 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,9 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; -import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. @@ -11,18 +9,23 @@ import { OpenAPIClientContext } from './types'; export function OpenAPIServerURLVariable(props: { name: string; variable: OpenAPIV3.ServerVariableObject; - enumIndex?: number; + selectionIndex?: number; + selectable: boolean; }) { - const { enumIndex, name, variable } = props; + const { selectable, selectionIndex, name, variable } = props; if (variable.enum && variable.enum.length > 0) { + if (!selectable) { + return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + } + return ( - v === variable.default) } /> @@ -35,7 +38,7 @@ export function OpenAPIServerURLVariable(props: { /** * Render a select if there is an enum for a Server URL variable */ -function EnumSelect(props: { +function VariableSelector(props: { value?: number; name: string; variable: OpenAPIV3.ServerVariableObject; diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 60b7f1586..dcf1d7a70 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -12,6 +12,8 @@ import { import React from 'react'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; +import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; const ApiClientReact = React.lazy(async () => { const mod = await import('@scalar/api-client-react'); @@ -55,8 +57,10 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) { - const { children, serverUrl } = props; +export function ScalarApiClient(props: { children: React.ReactNode; }) { + const { children } = props; + + const ctx = useOpenAPIContext(); const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: serverUrl ?? operationData.servers[0]?.url, + url: ctx?.serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active, serverUrl]); + }, [active, ctx?.state?.serverUrl]); return ( diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts new file mode 100644 index 000000000..923168320 --- /dev/null +++ b/packages/react-openapi/src/client.ts @@ -0,0 +1,2 @@ +'use client'; +export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 5bc3ee092..c94d11df2 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -11,3 +11,41 @@ export function noReference(input: T | OpenAPIV3.ReferenceObject): T { export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } + + +/** + * Get the default URL for the server. + */ +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): string { + const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const server = servers[serverIndex]; + const parts = parseServerURL(server?.url ?? ''); + + return parts + .map((part) => { + if (part.kind === 'text') { + return part.text; + } else { + return selectors && !isNaN(Number(selectors[part.name])) + ? server.variables?.[part.name]?.enum?.[Number(selectors[part.name])] + : (server.variables?.[part.name]?.default ?? `{${part.name}}`); + } + }) + .join(''); +} + +export function parseServerURL(url: string) { + const parts = url.split(/{([^}]+)}/g); + const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = []; + for (let i = 0; i < parts.length; i++) { + if (i % 2 === 0) { + result.push({ kind: 'text', text: parts[i] }); + } else { + result.push({ kind: 'variable', name: parts[i] }); + } + } + return result; +} \ No newline at end of file From f65e3f5fbbde07d73cd1590e231852959b437011 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 11:27:27 +0100 Subject: [PATCH 38/55] Add icons for editing url --- .../components/DocumentView/OpenAPI/OpenAPI.tsx | 2 ++ .../components/DocumentView/OpenAPI/style.css | 17 ++++++++++++++--- packages/react-openapi/src/OpenAPIOperation.tsx | 2 +- .../react-openapi/src/OpenAPIServerURLForm.tsx | 5 +++-- packages/react-openapi/src/ServerSelector.tsx | 4 ++-- packages/react-openapi/src/types.ts | 2 ++ 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 05c094467..1c907c928 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -54,6 +54,8 @@ async function OpenAPIBody(props: BlockProps) { icons: { chevronDown: , chevronRight: , + edit: , + clear: }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 6bd0206ad..cd2c8b5ca 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -57,7 +57,7 @@ /** URL */ .openapi-url { - @apply font-mono text-sm text-dark/8 dark:text-light/8; + @apply flex items-center font-mono text-sm text-dark/8 dark:text-light/8; } .openapi-url-var { @@ -365,6 +365,17 @@ @apply mb-0; } -.openapi-server-button { - @apply disabled:opacity-5; +.openapi-select-button, +.openapi-edit-button { + @apply p-0.5 inline-flex text-dark/6 dark:text-light/6 hover:opacity-8; +} + +.openapi-edit-button { @apply p-0.5 ml-4 size-4; } + +.openapi-edit-button > * { + @apply size-full; +} + +.openapi-select-button { + @apply leading-[1cap] disabled:opacity-5; } diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index b0a2b0325..e4a643303 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -37,7 +37,7 @@ export async function OpenAPIOperation(props: { {operation.description ? ( ) : null} -
+
1 || server.variables; return (
{ e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents">
@@ -61,11 +62,11 @@ export function ServerURLForm(props: { onChange={switchServer} /> ) : null} - + }} title={ctx?.state?.edit ? undefined : "Try different server options"} aria-label={ctx?.state?.edit ? "Clear" : "Edit"}>{ctx?.state?.edit ? context.icons.clear : context.icons.edit} : null}
); diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index 53051e3b8..bd2997619 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -18,7 +18,7 @@ export function ServerSelector(props: { : null} + {isEditable ? ( + + ) : null}
); diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 83a316269..27e82a00c 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -16,7 +16,13 @@ export function OpenAPIServerURLVariable(props: { if (variable.enum && variable.enum.length > 0) { if (!selectable) { - return {!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}; + return ( + + {!isNaN(Number(selectionIndex)) + ? variable.enum[Number(selectionIndex)] + : variable.default} + + ); } return ( diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index dcf1d7a70..af6257fca 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -57,7 +57,7 @@ export function ScalarApiButton(props: { /** * Wrap the rendering with a context to open the scalar modal. */ -export function ScalarApiClient(props: { children: React.ReactNode; }) { +export function ScalarApiClient(props: { children: React.ReactNode }) { const { children } = props; const ctx = useOpenAPIContext(); diff --git a/packages/react-openapi/src/client.ts b/packages/react-openapi/src/client.ts index 923168320..17e54c128 100644 --- a/packages/react-openapi/src/client.ts +++ b/packages/react-openapi/src/client.ts @@ -1,2 +1,2 @@ 'use client'; -export {OpenAPIContextProvider} from './OpenAPIContextProvider'; \ No newline at end of file +export { OpenAPIContextProvider } from './OpenAPIContextProvider'; diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index c94d11df2..d2d66e2be 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -12,7 +12,6 @@ export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } - /** * Get the default URL for the server. */ @@ -20,7 +19,8 @@ export function getServersURL( servers: OpenAPIV3.ServerObject[], selectors?: Record, ): string { - const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; + const serverIndex = + selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); @@ -48,4 +48,4 @@ export function parseServerURL(url: string) { } } return result; -} \ No newline at end of file +} From 184d616701955d78e09f94545e36e8d17756392c Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:37:40 +0100 Subject: [PATCH 41/55] Add server url cache to update request code sample --- .../src/app/(space)/(content)/[[...pathname]]/page.tsx | 3 +++ .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 4 ++++ .../src/components/DocumentView/OpenAPI/ServerUrlCache.tsx | 5 +++++ packages/react-openapi/src/OpenAPICodeSample.tsx | 3 ++- packages/react-openapi/src/OpenAPIServerURL.tsx | 1 - packages/react-openapi/src/OpenAPIServerURLForm.tsx | 3 ++- packages/react-openapi/src/types.ts | 5 ++--- 7 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..4b126ee90 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -3,6 +3,7 @@ import { Metadata, Viewport } from 'next'; import { notFound, redirect } from 'next/navigation'; import React from 'react'; +import { serverUrlCache } from '@/components/DocumentView/OpenAPI/ServerUrlCache'; import { PageAside } from '@/components/PageAside'; import { PageBody, PageCover } from '@/components/PageBody'; import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links'; @@ -26,6 +27,8 @@ export default async function Page(props: { }) { const { params, searchParams } = props; + serverUrlCache.parse(searchParams); + const { content: contentPointer, contentTarget, diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index bf18e3be5..7fd367184 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -8,6 +8,7 @@ import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; import OpenAPIContext from './OpenAPIContext'; +import { serverUrlCache } from './ServerUrlCache'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -46,6 +47,8 @@ async function OpenAPIBody(props: BlockProps) { return null; } + const serverUrl = serverUrlCache.get('serverUrl'); + return ( ) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, + serverUrl }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx new file mode 100644 index 000000000..b715ce3db --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -0,0 +1,5 @@ +import { createSearchParamsCache, parseAsString } from 'nuqs/server'; + +export const serverUrlCache = createSearchParamsCache({ + serverUrl: parseAsString +}) \ No newline at end of file diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index b3021263a..ac63e4779 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -48,11 +48,12 @@ export function OpenAPICodeSample(props: { } }); + const serverUrl = context.serverUrl ?? getServersURL(data.servers); const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { url: - getServersURL(data.servers, context.enumSelectors) + + serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index ec225c430..5bce96035 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -22,7 +22,6 @@ export function OpenAPIServerURL(props: { const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); - console.log({ ctxState: ctx?.state }); return ( diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 98aa20534..8864f9b2d 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -5,6 +5,7 @@ import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { getServersURL } from './utils'; export function ServerURLForm(props: { children: React.ReactNode; @@ -80,7 +81,7 @@ export function ServerURLForm(props: { update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? undefined : { edit: 'true' }), + ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), }); }} title={ctx?.state?.edit ? undefined : 'Try different server options'} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index e8297efa4..415bf8e49 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -27,11 +27,10 @@ export interface OpenAPIClientContext { * Optional id attached to the OpenAPI Operation heading and used as an anchor */ id?: string; - /** - * Selectors to update openapi enums, e.g. for server url variables + * Optional serverUrl to use with OpenAPI operations */ - enumSelectors?: Record; + serverUrl?: string | null; } export interface OpenAPIFetcher { From 74392eccee4de3ac73239734c073e40ca5f950f5 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 11:39:45 +0100 Subject: [PATCH 42/55] Formatting --- .../DocumentView/OpenAPI/OpenAPI.tsx | 2 +- .../DocumentView/OpenAPI/ServerUrlCache.tsx | 4 +-- .../react-openapi/src/OpenAPICodeSample.tsx | 5 +--- .../src/OpenAPIServerURLForm.tsx | 26 +++++++++++-------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 7fd367184..16dccc025 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -64,7 +64,7 @@ async function OpenAPIBody(props: BlockProps) { defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, blockKey: block.key, - serverUrl + serverUrl, }} className="openapi-block" /> diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx index b715ce3db..47ae56d22 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/ServerUrlCache.tsx @@ -1,5 +1,5 @@ import { createSearchParamsCache, parseAsString } from 'nuqs/server'; export const serverUrlCache = createSearchParamsCache({ - serverUrl: parseAsString -}) \ No newline at end of file + serverUrl: parseAsString, +}); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index ac63e4779..6345dfe29 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -52,10 +52,7 @@ export function OpenAPICodeSample(props: { const requestBody = noReference(data.operation.requestBody); const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined; const input: CodeSampleInput = { - url: - serverUrl + - data.path + - (searchParams.size ? `?${searchParams.toString()}` : ''), + url: serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''), method: data.method, body: requestBodyContent ? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true }) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 8864f9b2d..45b277a69 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -14,7 +14,7 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const ctx = useOpenAPIContext(); + const stateContext = useOpenAPIContext(); const server = servers[serverIndex]; const formRef = React.useRef(null); @@ -22,7 +22,7 @@ export function ServerURLForm(props: { if (index !== serverIndex) { update({ server: `${index}` ?? '0', - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } } @@ -38,7 +38,7 @@ export function ServerURLForm(props: { update({ server: `${formData.get('server')}` ?? '0', ...variables, - ...(ctx?.state?.edit ? { edit: 'true' } : undefined), + ...(stateContext?.state?.edit ? { edit: 'true' } : undefined), }); } @@ -46,7 +46,7 @@ export function ServerURLForm(props: { if (!context.blockKey) { return; } - ctx?.onUpdate({ + stateContext?.onUpdate({ block: context.blockKey, ...variables, }); @@ -62,10 +62,10 @@ export function ServerURLForm(props: { }} className="contents" > -
+
{children} - {ctx?.state?.edit && servers.length > 1 ? ( + {stateContext?.state?.edit && servers.length > 1 ? ( { - const state = { ...ctx?.state }; + const state = { ...stateContext?.state }; delete state.edit; update({ server: `${serverIndex}`, ...state, - ...(ctx?.state?.edit ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), + ...(stateContext?.state?.edit + ? { serverUrl: getServersURL(servers, state) } + : { edit: 'true' }), }); }} - title={ctx?.state?.edit ? undefined : 'Try different server options'} - aria-label={ctx?.state?.edit ? 'Clear' : 'Edit'} + title={ + stateContext?.state?.edit ? undefined : 'Try different server options' + } + aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {ctx?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} ) : null}
From 7b02f72660316271c7220c2b93f9ad933e15717d Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 12:05:59 +0100 Subject: [PATCH 43/55] Update icon --- .../gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx | 2 +- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 2 +- packages/react-openapi/src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 16dccc025..08fe0be58 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -58,7 +58,7 @@ async function OpenAPIBody(props: BlockProps) { chevronDown: , chevronRight: , edit: , - clear: , + editDone: , }, CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 45b277a69..6dc16c601 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -91,7 +91,7 @@ export function ServerURLForm(props: { } aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} > - {stateContext?.state?.edit ? context.icons.clear : context.icons.edit} + {stateContext?.state?.edit ? context.icons.editDone : context.icons.edit} ) : null}
diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 415bf8e49..7be2deb50 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -9,7 +9,7 @@ export interface OpenAPIClientContext { chevronDown: React.ReactNode; chevronRight: React.ReactNode; edit: React.ReactNode; - clear: React.ReactNode; + editDone: React.ReactNode; }; /** From dce39c4c0ddf4f196ca423f9ffc03dc8dcb5a449 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 3 Sep 2024 10:11:58 +0100 Subject: [PATCH 44/55] Update server url based on variable selection --- .../(content)/[[...pathname]]/page.tsx | 1 + .../components/DocumentView/DocumentView.tsx | 2 ++ .../DocumentView/OpenAPI/OpenAPI.tsx | 26 ++++++++++++++++++- .../src/components/PageBody/PageBody.tsx | 3 +++ .../react-openapi/src/OpenAPIOperation.tsx | 18 ++++++++++++- .../src/OpenAPIServerURLVariable.tsx | 2 ++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index 4b126ee90..176bb07e2 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -93,6 +93,7 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } + searchParams={searchParams} /> {page.layout.outline ? ( boolean; + + searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 08fe0be58..e2a2256c7 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,6 +1,6 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation } from '@gitbook/react-openapi'; +import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; @@ -100,3 +100,27 @@ function OpenAPIFallback() {

); } + +function parseModifiers(data: OpenAPIOperationData, params: Record) { + if (!data) { + return; + } + const { servers } = params; + const serverIndex = + servers && !isNaN(Number(servers)) + ? Math.min(0, Math.max(Number(servers), servers.length - 1)) + : 0; + const server = data.servers[serverIndex]; + if (server && server.variables) { + return Object.keys(server.variables).reduce>( + (result, key) => { + const selection = Number(params[key]); + if (!isNaN(selection)) { + result[key] = selection; + } + return result; + }, + { servers: serverIndex }, + ); + } +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 052d9abb2..98dd20f97 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,6 +34,7 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; + searchParams: Record; }) { const { space, @@ -44,6 +45,7 @@ export function PageBody(props: { page, document, withPageFeedback, + searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -96,6 +98,7 @@ export function PageBody(props: { resolveContentRef: (ref, options) => resolveContentRef(ref, context, options), shouldHighlightCode, + searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index e4a643303..48d9e9390 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPIServerURL } from './OpenAPIServerURL'; +import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -25,8 +25,10 @@ export async function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, + enumSelectors: context.enumSelectors, }; + const config = await getConfiguration(context); return (
@@ -70,3 +72,17 @@ export async function OpenAPIOperation(props: { ); } + +async function getConfiguration(context: OpenAPIContextProps) { + const response = await fetch(context.specUrl); + const doc = await response.json(); + + return { + spec: { + content: { + ...doc, + servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], + }, + }, + }; +} diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 27e82a00c..eae2732e4 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,7 +1,9 @@ 'use client'; import * as React from 'react'; +import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; +import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. From e86608086784a45361810cbcaaf04ef634994a58 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:46:18 +0100 Subject: [PATCH 45/55] Update server choice based on selection --- .../components/DocumentView/OpenAPI/OpenAPI.tsx | 12 ++++++------ packages/react-openapi/src/OpenAPIOperation.tsx | 17 +---------------- packages/react-openapi/src/OpenAPIServerURL.tsx | 2 +- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index e2a2256c7..b49afa70b 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -105,14 +105,14 @@ function parseModifiers(data: OpenAPIOperationData, params: Record>( + if (server) { + return Object.keys(server.variables ?? {}).reduce>( (result, key) => { const selection = Number(params[key]); if (!isNaN(selection)) { @@ -120,7 +120,7 @@ function parseModifiers(data: OpenAPIOperationData, params: Record +

@@ -72,17 +71,3 @@ export async function OpenAPIOperation(props: { ); } - -async function getConfiguration(context: OpenAPIContextProps) { - const response = await fetch(context.specUrl); - const doc = await response.json(); - - return { - spec: { - content: { - ...doc, - servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }], - }, - }, - }; -} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 5bce96035..2c322436e 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -26,7 +26,7 @@ export function OpenAPIServerURL(props: { return ( {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { From 662df6f9aab02dd2cc3b69cc2cb42bad862b6bcf Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Thu, 12 Sep 2024 16:48:47 +0100 Subject: [PATCH 46/55] Format --- packages/react-openapi/src/OpenAPIServerURL.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 2c322436e..5bce96035 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -26,7 +26,7 @@ export function OpenAPIServerURL(props: { return ( {parts.map((part, i) => { - if (part.kind === 'text') { + if (part.kind === 'text') { return {part.text}; } else { if (!server.variables?.[part.name]) { From 65d77295e5745c40976c70acac5769d2ab780643 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 09:35:43 +0100 Subject: [PATCH 47/55] Revised client state with context --- .../(content)/[[...pathname]]/page.tsx | 1 - .../components/DocumentView/DocumentView.tsx | 2 -- .../DocumentView/OpenAPI/OpenAPI.tsx | 26 +------------------ .../src/components/PageBody/PageBody.tsx | 3 --- .../react-openapi/src/OpenAPIOperation.tsx | 5 ++-- .../react-openapi/src/OpenAPIServerURL.tsx | 1 - .../src/OpenAPIServerURLForm.tsx | 6 ++--- .../src/OpenAPIServerURLVariable.tsx | 2 -- 8 files changed, 6 insertions(+), 40 deletions(-) diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index 176bb07e2..4b126ee90 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -93,7 +93,6 @@ export default async function Page(props: { // Display the page feedback in the page footer if the aside is not visible withPageFeedback && !page.layout.outline } - searchParams={searchParams} /> {page.layout.outline ? ( boolean; - - searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index b49afa70b..08fe0be58 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -1,6 +1,6 @@ import { DocumentBlockSwagger } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; -import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi'; +import { OpenAPIOperation } from '@gitbook/react-openapi'; import React from 'react'; import { LoadingPane } from '@/components/primitives'; @@ -100,27 +100,3 @@ function OpenAPIFallback() {

); } - -function parseModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { - return; - } - const { server: serverQueryParam } = params; - const serverIndex = - serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) - : 0; - const server = data.servers[serverIndex]; - if (server) { - return Object.keys(server.variables ?? {}).reduce>( - (result, key) => { - const selection = Number(params[key]); - if (!isNaN(selection)) { - result[key] = selection; - } - return result; - }, - { server: serverIndex }, - ); - } -} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 98dd20f97..052d9abb2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,7 +34,6 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; - searchParams: Record; }) { const { space, @@ -45,7 +44,6 @@ export function PageBody(props: { page, document, withPageFeedback, - searchParams, } = props; const asFullWidth = document ? hasFullWidthBlock(document) : false; @@ -98,7 +96,6 @@ export function PageBody(props: { resolveContentRef: (ref, options) => resolveContentRef(ref, context, options), shouldHighlightCode, - searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index cda323e84..e4a643303 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL'; +import { OpenAPIServerURL } from './OpenAPIServerURL'; import { OpenAPISpec } from './OpenAPISpec'; import { OpenAPIClientContext, OpenAPIContextProps } from './types'; import { ScalarApiClient } from './ScalarApiButton'; @@ -25,11 +25,10 @@ export async function OpenAPIOperation(props: { defaultInteractiveOpened: context.defaultInteractiveOpened, icons: context.icons, blockKey: context.blockKey, - enumSelectors: context.enumSelectors, }; return ( - +

diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index 5bce96035..d0f8e9d53 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -18,7 +18,6 @@ export function OpenAPIServerURL(props: { }) { const { path, servers, context } = props; const ctx = useOpenAPIContext(); - const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 6dc16c601..d8f2569bd 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -67,9 +67,9 @@ export function ServerURLForm(props: { {children} {stateContext?.state?.edit && servers.length > 1 ? ( ) : null} {isEditable ? ( diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index eae2732e4..27e82a00c 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -1,9 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; import classNames from 'classnames'; import { OpenAPIV3 } from 'openapi-types'; -import { OpenAPIClientContext } from './types'; /** * Interactive component to show the value of a server variable and let the user change it. From 2d4ddc9d95046e23c2fd76a3e0ea7c28ea8cc90a Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Fri, 20 Sep 2024 11:41:38 +0100 Subject: [PATCH 48/55] Formatting --- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index d8f2569bd..6dc16c601 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -67,9 +67,9 @@ export function ServerURLForm(props: { {children} {stateContext?.state?.edit && servers.length > 1 ? ( ) : null} {isEditable ? ( From c7ca921d7ff187bc955820a5b9bbce6b88b507d0 Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 24 Sep 2024 16:35:31 +0100 Subject: [PATCH 49/55] Self review changes --- packages/react-openapi/src/OpenAPIOperation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index e4a643303..a37e33614 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton'; /** * Display an interactive OpenAPI operation. */ -export async function OpenAPIOperation(props: { +export function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; context: OpenAPIContextProps; From 9675290e61ad0fe9c6e750a13613724e54c0fc1a Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Wed, 25 Sep 2024 09:36:37 +0100 Subject: [PATCH 50/55] Don't make server url editable if no update function or context --- packages/react-openapi/src/OpenAPIServerURLForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 6dc16c601..5abfad838 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -52,7 +52,7 @@ export function ServerURLForm(props: { }); } - const isEditable = servers.length > 1 || server.variables; + const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); return (
Date: Wed, 2 Oct 2024 11:44:10 +0100 Subject: [PATCH 51/55] Refactor - naming and comments --- .../DocumentView/OpenAPI/OpenAPI.tsx | 12 +++-- ...xt.tsx => OpenAPIClientStateContainer.tsx} | 19 ++++--- .../components/DocumentView/OpenAPI/style.css | 4 ++ ...ider.tsx => OpenAPIClientStateContext.tsx} | 52 +++++++++++++------ .../react-openapi/src/OpenAPIServerURL.tsx | 51 ++++++++++-------- .../src/OpenAPIServerURLForm.tsx | 21 ++++---- .../react-openapi/src/ScalarApiButton.tsx | 9 ++-- packages/react-openapi/src/ServerSelector.tsx | 6 +-- packages/react-openapi/src/client.ts | 2 +- 9 files changed, 107 insertions(+), 69 deletions(-) rename packages/gitbook/src/components/DocumentView/OpenAPI/{OpenAPIContext.tsx => OpenAPIClientStateContainer.tsx} (66%) rename packages/react-openapi/src/{OpenAPIContextProvider.tsx => OpenAPIClientStateContext.tsx} (50%) diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 08fe0be58..a70d0f1f4 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -7,7 +7,7 @@ import { LoadingPane } from '@/components/primitives'; import { fetchOpenAPIBlock } from '@/lib/openapi'; import { tcls } from '@/lib/tailwind'; -import OpenAPIContext from './OpenAPIContext'; +import OpenAPIClientStateContainer from './OpenAPIClientStateContainer'; import { serverUrlCache } from './ServerUrlCache'; import { BlockProps } from '../Block'; import { PlainCodeBlock } from '../CodeBlock'; @@ -19,7 +19,7 @@ import './scalar.css'; * Render an OpenAPI block. */ export async function OpenAPI(props: BlockProps) { - const { block, style } = props; + const { style } = props; return (
}> @@ -47,10 +47,12 @@ async function OpenAPIBody(props: BlockProps) { return null; } - const serverUrl = serverUrlCache.get('serverUrl'); + // To update the code sample we need to re-render the server component + // so reading the cached value from search params + const serverUrl = serverUrlCache.get('serverUrl'); return ( - + ) { }} className="openapi-block" /> - + ); } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx similarity index 66% rename from packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx rename to packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx index de4b2d840..743fb79e1 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIContext.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIClientStateContainer.tsx @@ -2,24 +2,29 @@ import { DocumentBlock } from '@gitbook/api'; import { OpenAPIOperationData } from '@gitbook/react-openapi'; -import { OpenAPIContextProvider } from '@gitbook/react-openapi/client'; +import { OpenAPIClientState } from '@gitbook/react-openapi/client'; import { useRouter, useSearchParams } from 'next/navigation'; +import { OpenAPIV3 } from 'openapi-types'; import * as React from 'react'; -export default function OpenAPIContext(props: { +/** + * Client component that wraps `OpenAPIClientState` so we can + * use some hooks (e.g. useRouter) in the `onUpdate` callback. + */ +export default function OpenAPIClientStateContainer(props: { children: React.ReactNode; block: DocumentBlock; - data: OpenAPIOperationData; + servers: OpenAPIV3.ServerObject[]; }) { - const { block, children, data } = props; + const { block, children, servers } = props; const [isPending, startTransition] = React.useTransition(); const router = useRouter(); const searchParams = useSearchParams(); return ( - {children} - + ); } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 54ef1adaf..56e2295bb 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -381,3 +381,7 @@ .openapi-select-button { @apply leading-[1cap] disabled:opacity-5; } + +.openapi-pending { + @apply opacity-5; +} diff --git a/packages/react-openapi/src/OpenAPIContextProvider.tsx b/packages/react-openapi/src/OpenAPIClientStateContext.tsx similarity index 50% rename from packages/react-openapi/src/OpenAPIContextProvider.tsx rename to packages/react-openapi/src/OpenAPIClientStateContext.tsx index b09ee2933..e5d213dd0 100644 --- a/packages/react-openapi/src/OpenAPIContextProvider.tsx +++ b/packages/react-openapi/src/OpenAPIClientStateContext.tsx @@ -3,38 +3,56 @@ import * as React from 'react'; import { OpenAPIOperationData } from './fetchOpenAPIOperation'; import { getServersURL } from './utils'; +import { OpenAPIV3 } from 'openapi-types'; -type OpenAPIContextProps = { +type OpenAPIClientStateContextProps = { + /** + * Whether client state updates are in a pending state, + * i.e. is a transition in progress. + */ isPending?: boolean; + /** + * The server url + */ serverUrl?: string; + /** + * The current state + */ state?: Record | null; + /** + * Callback for when the client state is updated + */ onUpdate: (params: Record | null) => void; }; -const OpenAPIContext = React.createContext(null); -export function useOpenAPIContext() { - return React.useContext(OpenAPIContext); +const OpenAPIClientStateContext = React.createContext(null); + +export function useOpenAPIClientState() { + return React.useContext(OpenAPIClientStateContext); } -export function OpenAPIContextProvider(props: { +/** + * Control client state for an OpenAPI operation + */ +export function OpenAPIClientState(props: { children: React.ReactNode; - data: OpenAPIOperationData; + servers: OpenAPIV3.ServerObject[]; isPending?: boolean; params?: Record; - onUpdate: OpenAPIContextProps['onUpdate']; + onUpdate: OpenAPIClientStateContextProps['onUpdate']; }) { - const { children, data, isPending, params, onUpdate } = props; + const { children, servers, isPending, params, onUpdate } = props; const clientState = React.useMemo(() => { if (!params) { return null; } - return parseClientStateModifiers(data, params); - }, [data, params]); - const serverUrl = getServersURL(data.servers, clientState ?? undefined); + return parseClientStateModifiers(servers, params); + }, [servers, params]); + const serverUrl = getServersURL(servers, clientState ?? undefined); return ( - {children} - + ); } -function parseClientStateModifiers(data: OpenAPIOperationData, params: Record) { - if (!data) { +function parseClientStateModifiers(servers: OpenAPIV3.ServerObject[], params: Record) { + if (!servers) { return null; } const serverQueryParam = params['server']; const serverIndex = serverQueryParam && !isNaN(Number(serverQueryParam)) - ? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1)) + ? Math.max(0, Math.min(Number(serverQueryParam), servers.length - 1)) : 0; - const server = data.servers[serverIndex]; + const server = servers[serverIndex]; return server ? Object.keys(server.variables ?? {}).reduce>( (result, key) => { diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index d0f8e9d53..786f49120 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -2,10 +2,12 @@ import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types'; +import classNames from 'classnames'; + import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIClientContext } from './types'; import { ServerURLForm } from './OpenAPIServerURLForm'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; import { parseServerURL } from './utils'; /** @@ -17,33 +19,38 @@ export function OpenAPIServerURL(props: { path?: string; }) { const { path, servers, context } = props; - const ctx = useOpenAPIContext(); - const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0; + const stateContext = useOpenAPIClientState(); + + const serverIndex = !isNaN(Number(stateContext?.state?.server)) ? Number(stateContext?.state?.server) : 0; const server = servers[serverIndex]; const parts = parseServerURL(server?.url ?? ''); + if (!server) { return null; } + return ( - {parts.map((part, i) => { - if (part.kind === 'text') { - return {part.text}; - } else { - if (!server.variables?.[part.name]) { - return {`{${part.name}}`}; - } + + {parts.map((part, i) => { + if (part.kind === 'text') { + return {part.text}; + } else { + if (!server.variables?.[part.name]) { + return {`{${part.name}}`}; + } - return ( - - ); - } - })} - {path} + return ( + + ); + } + })} + {path} + ); } diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx index 5abfad838..9df610dd5 100644 --- a/packages/react-openapi/src/OpenAPIServerURLForm.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { OpenAPIClientContext } from './types'; import { OpenAPIV3 } from 'openapi-types'; import { ServerSelector } from './ServerSelector'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; import { getServersURL } from './utils'; export function ServerURLForm(props: { @@ -14,7 +14,7 @@ export function ServerURLForm(props: { serverIndex: number; }) { const { children, context, servers, serverIndex } = props; - const stateContext = useOpenAPIContext(); + const stateContext = useOpenAPIClientState(); const server = servers[serverIndex]; const formRef = React.useRef(null); @@ -52,7 +52,10 @@ export function ServerURLForm(props: { }); } - const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); + // Only make the server url editable if there is some onUpdate callback + // and if there are variations on the server url (e.g. an array of servers or url variables). + const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables); + const isEditing = isEditable && stateContext?.state?.edit; return ( {children} - {stateContext?.state?.edit && servers.length > 1 ? ( + {isEditing && servers.length > 1 ? ( @@ -81,17 +84,17 @@ export function ServerURLForm(props: { update({ server: `${serverIndex}`, ...state, - ...(stateContext?.state?.edit + ...(isEditing ? { serverUrl: getServersURL(servers, state) } : { edit: 'true' }), }); }} title={ - stateContext?.state?.edit ? undefined : 'Try different server options' + isEditing ? undefined : 'Try different server options' } - aria-label={stateContext?.state?.edit ? 'Clear' : 'Edit'} + aria-label={isEditing ? 'Clear' : 'Edit'} > - {stateContext?.state?.edit ? context.icons.editDone : context.icons.edit} + {isEditing ? context.icons.editDone : context.icons.edit} ) : null} diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index af6257fca..ca0a9869c 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -12,8 +12,7 @@ import { import React from 'react'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; -import { useOpenAPIContext } from './OpenAPIContextProvider'; -import { getServersURL } from './utils'; +import { useOpenAPIClientState } from './OpenAPIClientStateContext'; const ApiClientReact = React.lazy(async () => { const mod = await import('@scalar/api-client-react'); @@ -60,7 +59,7 @@ export function ScalarApiButton(props: { export function ScalarApiClient(props: { children: React.ReactNode }) { const { children } = props; - const ctx = useOpenAPIContext(); + const stateCtx = useOpenAPIClientState(); const [active, setActive] = React.useState { return { ...header, enabled: true }; }), - url: ctx?.serverUrl ?? operationData.servers[0]?.url, + url: stateCtx?.serverUrl ?? operationData.servers[0]?.url, body: request.postData?.text, }; return data; - }, [active, ctx?.state?.serverUrl]); + }, [active, stateCtx?.serverUrl]); return ( diff --git a/packages/react-openapi/src/ServerSelector.tsx b/packages/react-openapi/src/ServerSelector.tsx index bd2997619..17037ffbf 100644 --- a/packages/react-openapi/src/ServerSelector.tsx +++ b/packages/react-openapi/src/ServerSelector.tsx @@ -4,10 +4,10 @@ import * as React from 'react'; export function ServerSelector(props: { currentIndex: number; + lastIndex: number; onChange: (value: number) => void; - servers: any[]; }) { - const { currentIndex, onChange, servers } = props; + const { currentIndex, onChange, lastIndex } = props; const [index, setIndex] = React.useState(currentIndex); React.useEffect(() => { @@ -32,7 +32,7 @@ export function ServerSelector(props: {