OpenAPI: support JSON mode for body input

This commit is contained in:
Fuma Nama
2025-04-14 18:02:48 +08:00
parent daddc93eab
commit 24ea50d249
7 changed files with 115 additions and 44 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
support JSON mode for body input
@@ -63,7 +63,7 @@ export function OauthDialog({
redirect_uri: state.redirect_uri,
client_id: state.client_id,
client_secret: state.client_secret,
}).toString(),
}),
});
if (!res.ok) throw new Error(await res.text());
@@ -125,7 +125,7 @@ export function OauthDialog({
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: Object.keys(value.scopes).join('+'),
}).toString(),
}),
});
}
});
+71 -31
View File
@@ -1,33 +1,33 @@
'use client';
import {
type ReactElement,
type HTMLAttributes,
useMemo,
useRef,
useEffect,
createContext,
type FC,
Fragment,
type HTMLAttributes,
type ReactElement,
type ReactNode,
type RefObject,
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type {
ControllerFieldState,
ControllerRenderProps,
FieldPath,
UseFormStateReturn,
} from 'react-hook-form';
import {
Controller,
FormProvider,
useForm,
useFormContext,
} from 'react-hook-form';
import type {
FieldPath,
UseFormStateReturn,
ControllerFieldState,
ControllerRenderProps,
} from 'react-hook-form';
import { useApiContext, useServerSelectContext } from '@/ui/contexts/api';
import type { FetchResult } from '@/playground/fetcher';
import { FieldSet, ObjectInput } from './inputs';
import { FieldSet, JsonInput, ObjectInput } from './inputs';
import type {
PrimitiveRequestField,
ReferenceSchema,
@@ -55,6 +55,7 @@ import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
import type { RequestData } from '@/requests/_shared';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { cn } from 'fumadocs-ui/utils/cn';
import { cva } from 'class-variance-authority';
interface FormValues {
authorization:
@@ -245,7 +246,7 @@ export default function Client({
<form
{...rest}
className={cn(
'not-prose flex flex-col rounded-xl border p-3 gap-3 shadow-md overflow-hidden',
'not-prose flex flex-col rounded-xl border p-3 gap-4 shadow-md overflow-hidden bg-fd-card text-fd-card-foreground',
rest.className,
)}
onSubmit={onSubmit}
@@ -396,20 +397,62 @@ function FormBody({
})}
{body ? (
<CollapsiblePanel title="Body">
{fields.body ? (
renderCustomField('body', body, fields.body)
) : body.type === 'object' ? (
<ObjectInput field={body} fieldName="body" />
) : (
<FieldSet field={body} fieldName="body" />
)}
</CollapsiblePanel>
fields.body ? (
<CollapsiblePanel title="Body">
{renderCustomField('body', body, fields.body)}
</CollapsiblePanel>
) : (
<BodyInput field={body} />
)
) : null}
</>
);
}
const bodyTypeVariants = cva(
'p-1 rounded-lg font-medium text-xs transition-colors',
{
variants: {
active: {
true: 'bg-fd-primary/10 text-fd-primary',
false: 'text-fd-muted-foreground',
},
},
},
);
function BodyInput({ field }: { field: RequestSchema }) {
const [isJson, setIsJson] = useState(false);
return (
<CollapsiblePanel title="Body">
<div className="grid grid-cols-2 border bg-fd-muted rounded-lg">
<button
className={cn(bodyTypeVariants({ active: !isJson }))}
onClick={() => setIsJson(false)}
type="button"
>
Simple
</button>
<button
className={cn(bodyTypeVariants({ active: isJson }))}
onClick={() => setIsJson(true)}
type="button"
>
JSON Mode
</button>
</div>
{isJson ? (
<JsonInput fieldName="body" />
) : field.type === 'object' ? (
<ObjectInput field={field} fieldName="body" />
) : (
<FieldSet field={field} fieldName="body" />
)}
</CollapsiblePanel>
);
}
function renderCustomField(
fieldName: string,
info: RequestSchema & { name?: string },
@@ -546,20 +589,17 @@ function CollapsiblePanel({
title,
children,
...props
}: HTMLAttributes<HTMLDivElement> & {
}: Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
title: ReactNode;
}) {
return (
<Collapsible
{...props}
className="rounded-xl bg-fd-card border text-fd-card-foreground"
>
<CollapsibleTrigger className="group w-full inline-flex items-center gap-2 p-3 text-sm font-medium">
<Collapsible {...props} className="border-b -m-2 last:border-b-0">
<CollapsibleTrigger className="group w-full flex items-center gap-2 p-2 text-sm font-medium">
{title}
<ChevronDown className="ms-auto size-4 text-fd-muted-foreground group-data-[state=open]:rotate-180" />
<ChevronDown className="ms-auto size-3.5 text-fd-muted-foreground group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-3 p-3">{children}</div>
<div className="flex flex-col gap-3 p-2">{children}</div>
</CollapsibleContent>
</Collapsible>
);
+31 -1
View File
@@ -6,7 +6,12 @@ import {
useState,
} from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
import {
Controller,
useController,
useFieldArray,
useFormContext,
} from 'react-hook-form';
import {
Select,
SelectContent,
@@ -81,6 +86,31 @@ export function ObjectInput({
);
}
export function JsonInput({ fieldName }: { fieldName: string }) {
const controller = useController({
name: fieldName,
});
const [value, setValue] = useState(() =>
JSON.stringify(controller.field.value, null, 2),
);
return (
<textarea
{...controller.field}
value={value}
className="w-full h-[400px] resize-none rounded-lg border p-2 bg-fd-secondary text-fd-secondary-foreground focus-visible:outline-none"
onChange={(v) => {
setValue(v.target.value);
try {
controller.field.onChange(JSON.parse(v.target.value));
} catch {
// ignore
}
}}
/>
);
}
function AdditionalProperties({
fieldName,
type,
+1 -4
View File
@@ -14,10 +14,7 @@ export function getSampleRequest(url: string, data: RequestData): string {
}
if (Object.keys(headers).length > 0) {
options.set(
'headers',
JSON.stringify(headers, null, 2).replaceAll('\n', '\n '),
);
options.set('headers', JSON.stringify(headers, null, 2));
}
if (data.bodyMediaType === 'multipart/form-data' && data.body) {
+1 -1
View File
@@ -5,7 +5,7 @@ import { cva } from 'class-variance-authority';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export const labelVariants = cva(
'text-[13px] text-fd-card-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
'text-xs font-medium text-fd-foreground/80 peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
);
const Input = React.forwardRef<HTMLInputElement, InputProps>(
@@ -101,16 +101,15 @@ const SelectItem = forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'flex select-none flex-row items-center rounded-md py-1.5 pe-2 ps-6 text-[13px] outline-none focus:bg-fd-accent focus:text-fd-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'flex select-none flex-row items-center rounded-md py-1.5 px-2 text-[13px] outline-none focus:bg-fd-accent focus:text-fd-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<SelectPrimitive.ItemIndicator className="absolute start-2">
<Check className="size-4" />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator className="ms-auto">
<Check className="size-3.5 text-fd-primary" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;