OpenAPI: Improve experience to customise API Playground

This commit is contained in:
Fuma Nama
2025-02-22 23:08:50 +08:00
parent 3d211f3f34
commit 5730116660
14 changed files with 112 additions and 115 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Improve experience to customise API Playground
+1 -1
View File
@@ -137,7 +137,7 @@ export function Rate({
autoFocus
value={message}
onChange={(e) => setMessage(e.target.value)}
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 text-sm resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
placeholder="Leave your feedback..."
onKeyDown={(e) => {
if (!e.shiftKey && e.key === 'Enter') {
+4
View File
@@ -24,6 +24,10 @@
"import": "./dist/ui/client.js",
"types": "./dist/ui/client.d.ts"
},
"./playground": {
"import": "./dist/playground/index.js",
"types": "./dist/playground/index.d.ts"
},
"./scalar": {
"import": "./dist/scalar/index.js",
"types": "./dist/scalar/index.d.ts"
@@ -0,0 +1,5 @@
import dynamic from 'next/dynamic';
export const Client = dynamic(() =>
import('./client').then((mod) => mod.Client),
);
@@ -8,6 +8,9 @@ import {
type FC,
Fragment,
type ReactNode,
type RefObject,
createContext,
useContext,
} from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { cn, buttonVariants } from 'fumadocs-ui/components/api';
@@ -18,19 +21,18 @@ import type {
ControllerRenderProps,
} from 'react-hook-form';
import { useApiContext } from '@/ui/contexts/api';
import type { FetchResult } from '@/ui/playground/fetcher';
import type { FetchResult } from '@/playground/fetcher';
import {
getDefaultValue,
getDefaultValues,
} from '@/ui/playground/get-default-values';
import { FieldSet, ObjectInput } from '@/ui/playground/inputs';
} from '@/playground/get-default-values';
import { FieldSet, ObjectInput } from '@/playground/inputs';
import type {
APIPlaygroundProps,
PrimitiveRequestField,
ReferenceSchema,
RequestSchema,
} from '@/render/operation/playground';
import { type DynamicField, SchemaContext } from '../contexts/schema';
import { getStatusInfo } from '@/ui/playground/status-info';
} from '@/playground/index';
import { getStatusInfo } from '@/playground/status-info';
import { getUrl } from '@/utils/server-url';
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
import { MethodLabel } from '@/ui/components/method-label';
@@ -42,6 +44,7 @@ import {
CollapsibleTrigger,
} from 'fumadocs-ui/components/ui/collapsible';
import { ChevronDown } from 'lucide-react';
import type { Security } from '@/utils/get-security';
interface FormValues {
authorization:
@@ -68,7 +71,56 @@ export interface CustomField<TName extends FieldPath<FormValues>, Info> {
}) => ReactElement;
}
function defaultAuthValue(auth: APIPlaygroundProps['authorization']) {
export type ClientProps = HTMLAttributes<HTMLFormElement> & {
route: string;
method: string;
authorization?: Security;
path?: PrimitiveRequestField[];
query?: PrimitiveRequestField[];
header?: PrimitiveRequestField[];
body?: RequestSchema & {
mediaType: string;
};
schemas: Record<string, RequestSchema>;
proxyUrl?: string;
fields?: {
auth?: CustomField<'authorization', RequestSchema>;
path?: CustomField<`path.${string}`, PrimitiveRequestField>;
query?: CustomField<`query.${string}`, PrimitiveRequestField>;
header?: CustomField<`header.${string}`, PrimitiveRequestField>;
body?: CustomField<'body', RequestSchema>;
};
components?: Partial<{
ResultDisplay: FC<{ data: FetchResult }>;
}>;
};
interface SchemaContextType {
references: Record<string, RequestSchema>;
dynamic: RefObject<Map<string, DynamicField>>;
}
export type DynamicField =
| {
type: 'object';
properties: string[];
}
| {
type: 'field';
schema: RequestSchema | ReferenceSchema;
};
const SchemaContext = createContext<SchemaContextType | undefined>(undefined);
export function useSchemaContext(): SchemaContextType {
const ctx = useContext(SchemaContext);
if (!ctx) throw new Error('Missing provider');
return ctx;
}
function defaultAuthValue(auth: ClientProps['authorization']) {
if (!auth || auth.type === 'apiKey') return '';
if (auth.type === 'http' && auth.scheme === 'basic') {
return {
@@ -80,7 +132,7 @@ function defaultAuthValue(auth: APIPlaygroundProps['authorization']) {
return 'Bearer';
}
export function APIPlayground({
export function Client({
route,
method = 'GET',
authorization,
@@ -93,19 +145,7 @@ export function APIPlayground({
proxyUrl,
components: { ResultDisplay = DefaultResultDisplay } = {},
...props
}: APIPlaygroundProps & {
fields?: {
auth?: CustomField<'authorization', RequestSchema>;
path?: CustomField<`path.${string}`, PrimitiveRequestField>;
query?: CustomField<`query.${string}`, PrimitiveRequestField>;
header?: CustomField<`header.${string}`, PrimitiveRequestField>;
body?: CustomField<'body', RequestSchema>;
};
components?: Partial<{
ResultDisplay: FC<{ data: FetchResult }>;
}>;
} & HTMLAttributes<HTMLFormElement>) {
}: ClientProps) {
const { serverRef, servers } = useApiContext();
const dynamicRef = useRef(new Map<string, DynamicField>());
const form = useForm<FormValues>({
@@ -453,7 +493,7 @@ function DefaultResultDisplay({ data }: { data: FetchResult }) {
);
}
export function CollapsiblePanel({
function CollapsiblePanel({
title,
children,
...props
@@ -1,9 +1,6 @@
import { resolve } from '@/ui/playground/resolve';
import {
type ReferenceSchema,
type RequestSchema,
} from '@/render/operation/playground';
import { type DynamicField } from '@/ui/contexts/schema';
import { resolve } from '@/playground/resolve';
import { type ReferenceSchema, type RequestSchema } from '@/playground/index';
import type { DynamicField } from '@/playground/client';
export interface FetchOptions {
url: string;
@@ -1,8 +1,5 @@
import type {
PrimitiveRequestField,
RequestSchema,
} from '@/render/operation/playground';
import { resolve } from '@/ui/playground/resolve';
import type { PrimitiveRequestField, RequestSchema } from '@/playground/index';
import { resolve } from '@/playground/resolve';
export function getDefaultValue(
item: RequestSchema,
@@ -10,7 +10,8 @@ import {
type ParsedSchema,
} from '@/utils/schema';
import { getSecurities, type Security } from '@/utils/get-security';
import { APIPlayground } from '@/ui/client';
import { Client } from './client.lazy';
import { type ClientProps } from './client';
interface BaseRequestField {
name: string;
@@ -82,28 +83,21 @@ interface Context {
}
export interface APIPlaygroundProps {
route: string;
method: string;
authorization?: Security;
path?: PrimitiveRequestField[];
query?: PrimitiveRequestField[];
header?: PrimitiveRequestField[];
body?: RequestSchema & {
mediaType: string;
};
schemas: Record<string, RequestSchema>;
proxyUrl?: string;
}
export function Playground({
path,
method,
ctx,
}: {
path: string;
method: MethodInformation;
ctx: RenderContext;
}): ReactNode {
client?: Partial<ClientProps>;
}
export type { ClientProps, CustomField } from './client';
export function APIPlayground({
path,
method,
ctx,
client,
}: APIPlaygroundProps): ReactNode {
let currentId = 0;
const bodyContent = method.requestBody?.content;
const mediaType = bodyContent ? getPreferredType(bodyContent) : undefined;
@@ -123,7 +117,7 @@ export function Playground({
? toSchema(bodyContent[mediaType].schema, true, context)
: undefined;
const props: APIPlaygroundProps = {
const props: ClientProps = {
authorization: getAuthorizationField(method, ctx),
method: method.method,
route: path,
@@ -145,9 +139,10 @@ export function Playground({
: undefined,
schemas: context.references,
proxyUrl: ctx.proxyUrl,
...client,
};
return <APIPlayground {...props} />;
return <Client {...props} />;
}
function getAuthorizationField(
@@ -1,3 +1,4 @@
'use client';
import {
type HTMLAttributes,
type LabelHTMLAttributes,
@@ -14,11 +15,11 @@ import {
SelectTrigger,
SelectValue,
} from '@/ui/components/select';
import type { RequestSchema } from '@/render/operation/playground';
import { resolve } from '@/ui/playground/resolve';
import type { RequestSchema } from '@/playground/index';
import { resolve } from '@/playground/resolve';
import { Input, labelVariants } from '@/ui/components/input';
import { getDefaultValue } from '@/ui/playground/get-default-values';
import { useSchemaContext } from '@/ui/contexts/schema';
import { getDefaultValue } from '@/playground/get-default-values';
import { useSchemaContext } from '@/playground/client';
type FieldOfType<Type> = Extract<RequestSchema, { type: Type }>;
interface InputHeaderProps {
@@ -1,7 +1,4 @@
import {
type ReferenceSchema,
type RequestSchema,
} from '@/render/operation/playground';
import { type ReferenceSchema, type RequestSchema } from '@/playground/index';
/**
* Resolve reference
+4 -15
View File
@@ -10,8 +10,8 @@ import {
Property,
ObjectCollapsible,
} from '@/ui';
import type { MethodInformation, RenderContext, ServerObject } from '@/types';
import { Playground } from '@/render/operation/playground';
import type { RenderContext, ServerObject } from '@/types';
import { APIPlayground, type APIPlaygroundProps } from '@/playground';
import { Sample, Samples } from '@/ui/client';
export interface ResponsesProps {
@@ -99,20 +99,9 @@ export interface Renderer {
*/
ObjectCollapsible: ComponentType<ObjectCollapsibleProps>;
Property: ComponentType<PropertyProps>;
APIPlayground: ComponentType<{
path: string;
method: MethodInformation;
ctx: RenderContext;
}>;
APIPlayground: ComponentType<APIPlaygroundProps>;
}
export type {
APIPlaygroundProps,
RequestSchema,
PrimitiveRequestField,
ReferenceSchema,
} from '@/render/operation/playground';
export function createRenders(
shikiOptions: RenderContext['shikiOptions'],
): Renderer {
@@ -161,6 +150,6 @@ export function createRenders(
/>
</Tab>
),
APIPlayground: Playground,
APIPlayground,
};
}
+2 -5
View File
@@ -1,10 +1,6 @@
'use client';
import dynamic from 'next/dynamic';
export const APIPlayground = dynamic(() =>
import('./playground').then((mod) => mod.APIPlayground),
);
export const Samples = dynamic(() =>
import('./sample-select').then((mod) => mod.Samples),
);
@@ -13,4 +9,5 @@ export const Sample = dynamic(() =>
import('./sample-select').then((mod) => mod.Sample),
);
export { useSchemaContext } from './contexts/schema';
// for compatibility
export { Client as APIPlayground } from '@/playground/client.lazy';
@@ -1,30 +0,0 @@
import { createContext, type RefObject, useContext } from 'react';
import {
type ReferenceSchema,
type RequestSchema,
} from '@/render/operation/playground';
interface SchemaContextType {
references: Record<string, RequestSchema>;
dynamic: RefObject<Map<string, DynamicField>>;
}
export type DynamicField =
| {
type: 'object';
properties: string[];
}
| {
type: 'field';
schema: RequestSchema | ReferenceSchema;
};
export const SchemaContext = createContext<SchemaContextType | undefined>(
undefined,
);
export function useSchemaContext(): SchemaContextType {
const ctx = useContext(SchemaContext);
if (!ctx) throw new Error('Missing provider');
return ctx;
}