OpenAPI: support schemaUI option for customising schema UI

This commit is contained in:
Fuma Nama
2025-11-06 20:02:57 +08:00
parent 733c149d42
commit ccae0ac18e
5 changed files with 81 additions and 57 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': major
---
Rename option `content.showExampleInFields` to `schemaUI.showExample`.
+26 -9
View File
@@ -2,11 +2,15 @@
import Slugger from 'github-slugger';
import { Operation } from '@/ui/operation';
import type { MethodInformation, RenderContext } from '@/types';
import { createMethod, NoReference } from '@/utils/schema';
import {
createMethod,
type NoReference,
type ResolvedSchema,
} from '@/utils/schema';
import type { OpenAPIV3_1 } from 'openapi-types';
import type { ProcessedDocument } from '@/utils/process-document';
import { defaultAdapters, MediaAdapter } from '@/requests/media/adapter';
import { ComponentProps, FC, ReactNode } from 'react';
import type { ComponentProps, FC, ReactNode } from 'react';
import {
highlight,
type HighlightOptionsCommon,
@@ -32,6 +36,7 @@ import remarkRehype from 'remark-rehype';
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
import * as JsxRuntime from 'react/jsx-runtime';
import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
import type { SchemaUIProps } from './schema/client';
type Awaitable<T> = T | Promise<T>;
@@ -77,13 +82,6 @@ export interface CreateAPIPageOptions {
* Customise page content
*/
content?: {
/**
* Show examples under the generated content of JSON schemas.
*
* @defaultValue false
*/
showExampleInFields?: boolean;
renderResponseTabs?: (
tabs: ResponseTab[],
ctx: RenderContext,
@@ -107,6 +105,25 @@ export interface CreateAPIPageOptions {
) => Awaitable<ReactNode>;
};
/**
* Info UI for JSON schemas
*/
schemaUI?: {
render?: (
options: Omit<SchemaUIProps, 'generated'> & {
root: ResolvedSchema;
},
ctx: RenderContext,
) => Awaitable<ReactNode>;
/**
* Show examples under the generated content of JSON schemas.
*
* @defaultValue false
*/
showExample?: boolean;
};
/**
* Customise API playground
*/
+33 -22
View File
@@ -16,7 +16,7 @@ import {
TabsList,
TabsTrigger,
} from 'fumadocs-ui/components/tabs';
import type { SchemaData, SchemaUIData } from '@/ui/schema';
import type { SchemaData, SchemaUIGeneratedData } from '@/ui/schema';
import {
Collapsible,
CollapsibleContent,
@@ -33,7 +33,7 @@ import {
import { cn } from 'fumadocs-ui/utils/cn';
import { cva } from 'class-variance-authority';
interface DataContextType extends SchemaUIData {
interface DataContextType extends SchemaUIGeneratedData {
readOnly?: boolean;
writeOnly?: boolean;
}
@@ -63,13 +63,6 @@ const PropertyContext = createContext<PropertyContextType>({
const DataContext = createContext<DataContextType | null>(null);
export function SchemaUIProvider(props: {
value: DataContextType;
children: ReactNode;
}) {
return <DataContext value={props.value}>{props.children}</DataContext>;
}
function useData() {
return use(DataContext)!;
}
@@ -82,29 +75,47 @@ export interface SchemaUIProps {
name: string;
required?: boolean;
as?: 'property' | 'body';
readOnly?: boolean;
writeOnly?: boolean;
generated: SchemaUIGeneratedData;
}
export function SchemaUI({
name,
required = false,
as = 'property',
generated,
readOnly,
writeOnly,
}: SchemaUIProps) {
const { $root, refs } = useData();
const schema = refs[$root];
const context: DataContextType = useMemo(
() => ({
...generated,
readOnly,
writeOnly,
}),
[generated, readOnly, writeOnly],
);
const isProperty = as === 'property' || !isExpandable(schema);
if (as === 'property' || !isExpandable(schema)) {
return (
<SchemaUIProperty
name={name}
$type={$root}
overrides={{
required,
}}
/>
);
}
return <SchemaUIContent $type={$root} />;
return (
<DataContext value={context}>
{isProperty ? (
<SchemaUIProperty
name={name}
$type={$root}
overrides={{
required,
}}
/>
) : (
<SchemaUIContent $type={$root} />
)}
</DataContext>
);
}
function SchemaUIContent({ $type }: { $type: string }) {
+17 -22
View File
@@ -4,7 +4,7 @@ import type { RenderContext } from '@/types';
import { FormatFlags, schemaToString } from '@/utils/schema-to-string';
import { combineSchema } from '@/utils/combine-schema';
import type { SchemaUIProps } from '@/ui/schema/client';
import { SchemaUILazy, SchemaUIProviderLazy } from '@/ui/schema/lazy';
import { SchemaUILazy } from '@/ui/schema/lazy';
export interface FieldBase {
description?: ReactNode;
@@ -51,36 +51,31 @@ export interface SchemaUIOptions {
ctx: RenderContext;
}
export interface SchemaUIData {
export interface SchemaUIGeneratedData {
$root: string;
refs: Record<string, SchemaData>;
}
export function Schema(
props: SchemaUIOptions &
SchemaUIProps & {
readOnly?: boolean;
writeOnly?: boolean;
},
) {
const data = generateSchemaUI(props);
export function Schema({
ctx,
root,
...props
}: SchemaUIOptions & Omit<SchemaUIProps, 'generated'>) {
if (ctx.schemaUI?.render) {
return ctx.schemaUI.render({ root, ...props }, ctx);
}
return (
<SchemaUIProviderLazy
value={{
...data,
readOnly: props.readOnly,
writeOnly: props.writeOnly,
}}
>
<SchemaUILazy name={props.name} required={props.required} as={props.as} />
</SchemaUIProviderLazy>
<SchemaUILazy {...props} generated={generateSchemaUI({ ctx, root })} />
);
}
function generateSchemaUI({ ctx, root }: SchemaUIOptions): SchemaUIData {
function generateSchemaUI({
ctx,
root,
}: SchemaUIOptions): SchemaUIGeneratedData {
const refs: Record<string, SchemaData> = {};
const { content: { showExampleInFields = false } = {} } = ctx;
const { showExample = false } = ctx.schemaUI ?? {};
function generateInfoTags(schema: Exclude<ResolvedSchema, boolean>) {
const fields: ReactNode[] = [];
@@ -155,7 +150,7 @@ function generateSchemaUI({ ctx, root }: SchemaUIOptions): SchemaUIData {
);
}
if (showExampleInFields && schema.examples) {
if (showExample && schema.examples) {
for (const example of schema.examples) {
fields.push(field('Example', JSON.stringify(example, null, 2)));
}
-4
View File
@@ -4,7 +4,3 @@ import { wrapLazy } from '../../utils/lazy';
export const SchemaUILazy = wrapLazy(() =>
import('./client').then((mod) => ({ default: mod.SchemaUI })),
);
export const SchemaUIProviderLazy = wrapLazy(() =>
import('./client').then((mod) => ({ default: mod.SchemaUIProvider })),
);