mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Fix schema scope in API playgrounds
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Fix schema scope in API playgrounds
|
||||
@@ -2,13 +2,13 @@
|
||||
import {
|
||||
type FC,
|
||||
Fragment,
|
||||
type HTMLAttributes,
|
||||
lazy,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffectEvent,
|
||||
type ComponentProps,
|
||||
} from 'react';
|
||||
import type {
|
||||
FieldPath,
|
||||
@@ -31,11 +31,7 @@ import {
|
||||
JsonInput,
|
||||
ObjectInput,
|
||||
} from './components/inputs';
|
||||
import type {
|
||||
ParameterField,
|
||||
RequestSchema,
|
||||
SecurityEntry,
|
||||
} from '@/playground/index';
|
||||
import type { ParameterField, SecurityEntry } from '@/playground/index';
|
||||
import { getStatusInfo } from './status-info';
|
||||
import {
|
||||
joinURL,
|
||||
@@ -58,6 +54,7 @@ import { cn } from 'fumadocs-ui/utils/cn';
|
||||
import {
|
||||
type FieldInfo,
|
||||
SchemaProvider,
|
||||
SchemaScope,
|
||||
useResolvedSchema,
|
||||
} from '@/playground/schema';
|
||||
import {
|
||||
@@ -87,19 +84,21 @@ export interface FormValues {
|
||||
_encoded?: RequestData;
|
||||
}
|
||||
|
||||
export interface PlaygroundClientProps extends HTMLAttributes<HTMLFormElement> {
|
||||
export interface PlaygroundClientProps
|
||||
extends ComponentProps<'form'>,
|
||||
SchemaScope {
|
||||
route: string;
|
||||
method: string;
|
||||
parameters?: ParameterField[];
|
||||
securities: SecurityEntry[][];
|
||||
body?: {
|
||||
schema: RequestSchema;
|
||||
schema: ParsedSchema;
|
||||
mediaType: string;
|
||||
};
|
||||
/**
|
||||
* Resolver for $ref schemas you've passed
|
||||
*/
|
||||
references: Record<string, RequestSchema>;
|
||||
references: Record<string, ParsedSchema>;
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
@@ -140,7 +139,7 @@ export interface PlaygroundClientOptions {
|
||||
renderBodyField?: (
|
||||
fieldName: 'body',
|
||||
info: {
|
||||
schema: RequestSchema;
|
||||
schema: ParsedSchema;
|
||||
mediaType: string;
|
||||
},
|
||||
) => ReactNode;
|
||||
@@ -165,6 +164,8 @@ export default function PlaygroundClient({
|
||||
body,
|
||||
references,
|
||||
proxyUrl,
|
||||
writeOnly,
|
||||
readOnly,
|
||||
...rest
|
||||
}: PlaygroundClientProps) {
|
||||
const { example: exampleId, examples, setExampleData } = useExampleRequests();
|
||||
@@ -306,7 +307,12 @@ export default function PlaygroundClient({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<SchemaProvider fieldInfoMap={fieldInfoMap} references={references}>
|
||||
<SchemaProvider
|
||||
fieldInfoMap={fieldInfoMap}
|
||||
references={references}
|
||||
writeOnly={writeOnly}
|
||||
readOnly={readOnly}
|
||||
>
|
||||
<form
|
||||
{...rest}
|
||||
className={cn(
|
||||
@@ -494,7 +500,7 @@ function FormBody({
|
||||
);
|
||||
}
|
||||
|
||||
function BodyInput({ field: _field }: { field: RequestSchema }) {
|
||||
function BodyInput({ field: _field }: { field: ParsedSchema }) {
|
||||
const field = useResolvedSchema(_field);
|
||||
const [isJson, setIsJson] = useState(false);
|
||||
|
||||
@@ -736,10 +742,7 @@ function useAuthInputs(
|
||||
return { inputs, mapInputs, initAuthValues };
|
||||
}
|
||||
|
||||
function Route({
|
||||
route,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & { route: string }) {
|
||||
function Route({ route, ...props }: ComponentProps<'div'> & { route: string }) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@@ -815,7 +818,7 @@ function CollapsiblePanel({
|
||||
title,
|
||||
children,
|
||||
...props
|
||||
}: Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
|
||||
}: Omit<ComponentProps<'div'>, 'title'> & {
|
||||
title: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/ui/components/select';
|
||||
import type { RequestSchema } from '@/playground/index';
|
||||
import { Input, labelVariants } from '@/ui/components/input';
|
||||
import { getDefaultValue } from '../get-default-values';
|
||||
import { cn } from 'fumadocs-ui/utils/cn';
|
||||
@@ -24,7 +23,9 @@ import {
|
||||
anyFields,
|
||||
useFieldInfo,
|
||||
useResolvedSchema,
|
||||
useSchemaScope,
|
||||
} from '@/playground/schema';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
|
||||
function FieldLabel(props: ComponentProps<'label'>) {
|
||||
return (
|
||||
@@ -68,7 +69,7 @@ export function ObjectInput({
|
||||
fieldName,
|
||||
...props
|
||||
}: {
|
||||
field: Exclude<RequestSchema, boolean>;
|
||||
field: Exclude<ParsedSchema, boolean>;
|
||||
fieldName: string;
|
||||
} & ComponentProps<'div'>) {
|
||||
const field = useResolvedSchema(_field);
|
||||
@@ -149,7 +150,7 @@ function DynamicProperties({
|
||||
}: {
|
||||
fieldName: string;
|
||||
filterKey?: (key: string) => boolean;
|
||||
getType: (key: string) => RequestSchema;
|
||||
getType: (key: string) => ParsedSchema;
|
||||
}) {
|
||||
const { control, setValue, getValues } = useFormContext();
|
||||
const [nextName, setNextName] = useState('');
|
||||
@@ -239,7 +240,7 @@ export function FieldInput({
|
||||
isRequired,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLElement> & {
|
||||
field: Exclude<RequestSchema, boolean>;
|
||||
field: Exclude<ParsedSchema, boolean>;
|
||||
isRequired?: boolean;
|
||||
fieldName: string;
|
||||
}) {
|
||||
@@ -364,7 +365,7 @@ export function FieldSet({
|
||||
}: HTMLAttributes<HTMLElement> & {
|
||||
isRequired?: boolean;
|
||||
name?: ReactNode;
|
||||
field: RequestSchema;
|
||||
field: ParsedSchema;
|
||||
fieldName: string;
|
||||
depth?: number;
|
||||
|
||||
@@ -372,11 +373,14 @@ export function FieldSet({
|
||||
toolbar?: ReactNode;
|
||||
collapsible?: boolean;
|
||||
}) {
|
||||
const { readOnly, writeOnly } = useSchemaScope();
|
||||
const field = useResolvedSchema(_field);
|
||||
const [show, setShow] = useState(!collapsible);
|
||||
const { info, updateInfo } = useFieldInfo(fieldName, field, depth);
|
||||
|
||||
if (_field === false) return null;
|
||||
if (_field === false) return;
|
||||
if (field.readOnly && !readOnly) return;
|
||||
if (field.writeOnly && !writeOnly) return;
|
||||
|
||||
if (info.unionField) {
|
||||
const union = field[info.unionField]!;
|
||||
@@ -559,7 +563,7 @@ function ArrayInput({
|
||||
...props
|
||||
}: {
|
||||
fieldName: string;
|
||||
items: RequestSchema;
|
||||
items: ParsedSchema;
|
||||
} & ComponentProps<'div'>) {
|
||||
const name = fieldName.split('.').at(-1) ?? '';
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestSchema } from '@/playground/index';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
|
||||
export function getDefaultValue(schema: RequestSchema): unknown {
|
||||
export function getDefaultValue(schema: ParsedSchema): unknown {
|
||||
if (typeof schema === 'boolean') return null;
|
||||
|
||||
const type = schema.type;
|
||||
|
||||
@@ -17,10 +17,8 @@ export type ParameterField = NoReference<ParameterObject> & {
|
||||
in: 'cookie' | 'header' | 'query' | 'path';
|
||||
};
|
||||
|
||||
export type RequestSchema = ParsedSchema;
|
||||
|
||||
interface Context {
|
||||
references: Record<string, RequestSchema>;
|
||||
references: Record<string, ParsedSchema>;
|
||||
registered: WeakMap<Exclude<ParsedSchema, boolean>, string>;
|
||||
nextId: () => string;
|
||||
}
|
||||
@@ -70,6 +68,8 @@ export async function APIPlayground({ path, method, ctx }: APIPlaygroundProps) {
|
||||
: undefined,
|
||||
references: context.references,
|
||||
proxyUrl: ctx.proxyUrl,
|
||||
writeOnly: true,
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
return <ClientLazy {...props} />;
|
||||
@@ -79,7 +79,7 @@ function writeReferences(
|
||||
schema: ParsedSchema,
|
||||
ctx: Context,
|
||||
stack: WeakMap<object, object> = new WeakMap(),
|
||||
): RequestSchema {
|
||||
): ParsedSchema {
|
||||
if (typeof schema !== 'object' || !schema) return schema;
|
||||
if (stack.has(schema)) {
|
||||
const out = stack.get(schema)!;
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Ajv2020 } from 'ajv/dist/2020';
|
||||
import type { RequestSchema } from '@/playground/index';
|
||||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react';
|
||||
import { createContext, ReactNode, use, useMemo, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { getDefaultValue } from '@/playground/get-default-values';
|
||||
import type { ParsedSchema } from '@/utils/schema';
|
||||
import { mergeAllOf } from '@/utils/merge-schema';
|
||||
|
||||
interface SchemaContextType {
|
||||
references: Record<string, RequestSchema>;
|
||||
interface SchemaContextType extends SchemaScope {
|
||||
references: Record<string, ParsedSchema>;
|
||||
fieldInfoMap: Map<string, FieldInfo>;
|
||||
ajv: Ajv2020;
|
||||
}
|
||||
|
||||
export interface SchemaScope {
|
||||
/**
|
||||
* show write only fields
|
||||
*/
|
||||
writeOnly: boolean;
|
||||
|
||||
/**
|
||||
* show read only fields
|
||||
*/
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
type UnionField = 'anyOf' | 'oneOf';
|
||||
|
||||
export interface FieldInfo {
|
||||
@@ -24,7 +35,7 @@ export interface FieldInfo {
|
||||
unionField?: UnionField;
|
||||
|
||||
intersection?: {
|
||||
merged: Exclude<RequestSchema, boolean>;
|
||||
merged: Exclude<ParsedSchema, boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,11 +44,13 @@ export const anyFields = {
|
||||
type: ['string', 'number', 'boolean', 'array', 'object'],
|
||||
items: true,
|
||||
additionalProperties: true,
|
||||
} satisfies RequestSchema;
|
||||
} satisfies ParsedSchema;
|
||||
|
||||
export function SchemaProvider({
|
||||
references,
|
||||
fieldInfoMap,
|
||||
readOnly,
|
||||
writeOnly,
|
||||
children,
|
||||
}: Omit<SchemaContextType, 'ajv'> & { children: ReactNode }) {
|
||||
const ajv = useMemo(
|
||||
@@ -54,8 +67,8 @@ export function SchemaProvider({
|
||||
return (
|
||||
<SchemaContext.Provider
|
||||
value={useMemo(
|
||||
() => ({ references, fieldInfoMap, ajv }),
|
||||
[fieldInfoMap, references, ajv],
|
||||
() => ({ references, fieldInfoMap, ajv, readOnly, writeOnly }),
|
||||
[references, fieldInfoMap, ajv, readOnly, writeOnly],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -63,6 +76,10 @@ export function SchemaProvider({
|
||||
);
|
||||
}
|
||||
|
||||
export function useSchemaScope(): SchemaScope {
|
||||
return use(SchemaContext)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to store dynamic info of a field, such as selected schema of `oneOf`.
|
||||
*
|
||||
@@ -72,13 +89,13 @@ export function SchemaProvider({
|
||||
*/
|
||||
export function useFieldInfo(
|
||||
fieldName: string,
|
||||
schema: Exclude<RequestSchema, boolean>,
|
||||
schema: Exclude<ParsedSchema, boolean>,
|
||||
depth: number,
|
||||
): {
|
||||
info: FieldInfo;
|
||||
updateInfo: (value: Partial<FieldInfo>) => void;
|
||||
} {
|
||||
const { fieldInfoMap, ajv } = useContext(SchemaContext)!;
|
||||
const { fieldInfoMap, ajv } = use(SchemaContext)!;
|
||||
const form = useFormContext();
|
||||
const keyName = `${fieldName}:${depth}`;
|
||||
const [info, setInfo] = useState<FieldInfo>(() => {
|
||||
@@ -157,9 +174,9 @@ export function useFieldInfo(
|
||||
* Resolve `$ref` in the schema, **not recursive**.
|
||||
*/
|
||||
export function useResolvedSchema(
|
||||
schema: RequestSchema,
|
||||
): Exclude<RequestSchema, boolean> {
|
||||
const { references } = useContext(SchemaContext)!;
|
||||
schema: ParsedSchema,
|
||||
): Exclude<ParsedSchema, boolean> {
|
||||
const { references } = use(SchemaContext)!;
|
||||
|
||||
return useMemo(() => {
|
||||
if (typeof schema === 'boolean') return anyFields;
|
||||
@@ -169,13 +186,13 @@ export function useResolvedSchema(
|
||||
}
|
||||
|
||||
export function fallbackAny(
|
||||
schema: RequestSchema,
|
||||
): Exclude<RequestSchema, boolean> {
|
||||
schema: ParsedSchema,
|
||||
): Exclude<ParsedSchema, boolean> {
|
||||
return typeof schema === 'boolean' ? anyFields : schema;
|
||||
}
|
||||
|
||||
function getUnion(
|
||||
schema: Exclude<RequestSchema, boolean>,
|
||||
schema: Exclude<ParsedSchema, boolean>,
|
||||
): [readonly ParsedSchema[], UnionField] | undefined {
|
||||
if (schema.anyOf) {
|
||||
return [schema.anyOf, 'anyOf'];
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { OpenAPIOptions } from '@/server';
|
||||
import type { CreateAPIPageOptions } from './ui/api-page';
|
||||
import type { CodeUsageGenerator } from './ui/operation/usage-tabs';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
export type Document = V3_1.Document;
|
||||
export type OperationObject = V3_1.OperationObject;
|
||||
@@ -40,7 +40,11 @@ export interface RenderContext
|
||||
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
|
||||
renderHeading: (depth: number, text: string) => ReactNode;
|
||||
renderHeading: (
|
||||
depth: number,
|
||||
text: string,
|
||||
props?: HTMLAttributes<HTMLHeadingElement>,
|
||||
) => ReactNode;
|
||||
renderMarkdown: (text: string) => ReactNode;
|
||||
renderCodeBlock: (lang: string, code: string) => ReactNode;
|
||||
}
|
||||
|
||||
@@ -276,11 +276,11 @@ export function createAPIPage(
|
||||
...options.mediaAdapters,
|
||||
},
|
||||
slugger,
|
||||
renderHeading(depth, text) {
|
||||
renderHeading(depth, text, props) {
|
||||
const id = slugger.slug(text);
|
||||
|
||||
return (
|
||||
<Heading id={id} key={id} as={`h${depth}` as `h1`}>
|
||||
<Heading id={id} key={id} as={`h${depth}` as `h1`} {...props}>
|
||||
{text}
|
||||
</Heading>
|
||||
);
|
||||
|
||||
@@ -91,19 +91,13 @@ export async function Operation({
|
||||
bodyNode = (
|
||||
<SelectTabs defaultValue={defaultValue}>
|
||||
<div className="flex gap-2 items-end justify-between">
|
||||
{ctx.renderHeading(headingLevel, 'Request Body')}
|
||||
{ctx.renderHeading(headingLevel, 'Request Body', {
|
||||
className: 'my-0!',
|
||||
})}
|
||||
{contentTypes.length > 1 ? (
|
||||
<SelectTabTrigger
|
||||
items={contentTypes.map(([key]) => key)}
|
||||
className="mb-4"
|
||||
/>
|
||||
<SelectTabTrigger items={contentTypes.map(([key]) => key)} />
|
||||
) : (
|
||||
<p
|
||||
className={cn(
|
||||
'text-sm text-fd-muted-foreground font-medium not-prose',
|
||||
headingLevel === 2 ? 'mb-7' : 'mb-4',
|
||||
)}
|
||||
>
|
||||
<p className="text-sm text-fd-muted-foreground font-medium not-prose">
|
||||
{defaultValue}
|
||||
</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user