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