OpenAPI: improve UX (#2656)

This commit is contained in:
Nicholas
2025-11-23 09:02:27 +08:00
committed by GitHub
parent 8ce32a5169
commit 6635eb5ab8
5 changed files with 82 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Improve UX
+27 -6
View File
@@ -51,7 +51,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from 'fumadocs-ui/components/ui/collapsible';
import { ChevronDown, LoaderCircle } from 'lucide-react';
import { X, ChevronDown, LoaderCircle } from 'lucide-react';
import { encodeRequestData } from '@/requests/media/encode';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { cn } from 'fumadocs-ui/utils/cn';
@@ -351,7 +351,9 @@ export default function PlaygroundClient({
</SecurityTabs>
)}
<FormBody body={body} parameters={parameters} />
{testQuery.data ? <ResultDisplay data={testQuery.data} /> : null}
{testQuery.data ? (
<ResultDisplay data={testQuery.data} reset={testQuery.reset} />
) : null}
</form>
</SchemaProvider>
</FormProvider>
@@ -764,15 +766,34 @@ function Route({
);
}
function DefaultResultDisplay({ data }: { data: FetchResult }) {
function DefaultResultDisplay({
data,
reset,
}: {
data: FetchResult;
reset: () => void;
}) {
const statusInfo = useMemo(() => getStatusInfo(data.status), [data.status]);
const { shikiOptions } = useApiContext();
return (
<div className="flex flex-col gap-3 p-3">
<div className="inline-flex items-center gap-1.5 text-sm font-medium text-fd-foreground">
<statusInfo.icon className={cn('size-4', statusInfo.color)} />
{statusInfo.description}
<div className="flex justify-between items-center">
<div className="inline-flex items-center gap-1.5 text-sm font-medium text-fd-foreground">
<statusInfo.icon className={cn('size-4', statusInfo.color)} />
{statusInfo.description}
</div>
<button
type="button"
className={cn(
buttonVariants({ size: 'icon-xs' }),
'p-0 text-fd-muted-foreground hover:text-fd-accent-foreground [&_svg]:size-3.5',
)}
onClick={() => reset()}
aria-label="Dismiss response"
>
<X />
</button>
</div>
<p className="text-sm text-fd-muted-foreground">{data.status}</p>
{data.data !== undefined && (
@@ -408,7 +408,11 @@ export function FieldSet({
}}
>
{union.map((item, i) => (
<option key={i} value={i}>
<option
key={i}
value={i}
className="bg-fd-popover text-fd-popover-foreground"
>
{schemaToString(item, undefined, FormatFlags.UseAlias)}
</option>
))}
@@ -449,7 +453,11 @@ export function FieldSet({
}}
>
{field.type.map((item) => (
<option key={item} value={item}>
<option
key={item}
value={item}
className="bg-fd-popover text-fd-popover-foreground"
>
{item}
</option>
))}
+34 -11
View File
@@ -90,10 +90,21 @@ export async function Operation({
<SelectTabs defaultValue={defaultValue}>
<div className="flex gap-2 items-end justify-between">
{ctx.renderHeading(headingLevel, 'Request Body')}
<SelectTabTrigger
items={contentTypes.map(([key]) => key)}
className="mb-4"
/>
{contentTypes.length > 1 ? (
<SelectTabTrigger
items={contentTypes.map(([key]) => key)}
className="mb-4"
/>
) : (
<p
className={cn(
'text-sm text-fd-muted-foreground font-medium not-prose',
headingLevel === 2 ? 'mb-7' : 'mb-4',
)}
>
{defaultValue}
</p>
)}
</div>
{body.description && ctx.renderMarkdown(body.description)}
{contentTypes.map(([type, content]) => {
@@ -186,7 +197,13 @@ export async function Operation({
<SelectTabs defaultValue={names[0]}>
<div className="flex items-end justify-between gap-2">
{ctx.renderHeading(headingLevel, 'Authorization')}
<SelectTabTrigger items={names} className="mb-4" />
{names.length > 1 ? (
<SelectTabTrigger items={names} className="mb-4" />
) : (
<p className="text-sm text-fd-muted-foreground font-medium not-prose mb-7">
{names[0]}
</p>
)}
</div>
{securities.map((security, i) => (
<SelectTab key={i} value={names[i]}>
@@ -217,10 +234,16 @@ export async function Operation({
<SelectTabs defaultValue={defaultValue}>
<div className="flex justify-between gap-2 items-end">
{ctx.renderHeading(headingLevel, 'Callbacks')}
<SelectTabTrigger
items={callbacks.map(([key]) => key)}
className="mb-4"
/>
{callbacks.length > 1 ? (
<SelectTabTrigger
items={callbacks.map(([key]) => key)}
className="mb-4"
/>
) : (
<p className="text-sm text-fd-muted-foreground font-medium not-prose mb-7">
{defaultValue}
</p>
)}
</div>
{callbacks.map(([name, callback]) => (
<SelectTab key={name} value={name}>
@@ -356,7 +379,7 @@ async function ResponseAccordion({
</AccordionHeader>
<AccordionContent className="ps-4.5">
{response.description && (
<div className="prose-no-margin">
<div className="prose-no-margin mb-2">
{ctx.renderMarkdown(response.description)}
</div>
)}
@@ -371,7 +394,7 @@ async function ResponseAccordion({
}
return (
<SelectTab key={type} value={type} className="my-2">
<SelectTab key={type} value={type} className="mb-2">
{ts && <CopyResponseTypeScript code={ts} />}
{schema && (
<div className="border px-3 py-2 rounded-lg">
+6
View File
@@ -4,6 +4,7 @@ export function useQuery<I extends unknown[], T>(
fn: (...input: I) => Promise<T>,
): {
start: (...input: I) => void;
reset: () => void;
data?: T;
error?: unknown;
isLoading: boolean;
@@ -36,6 +37,11 @@ export function useQuery<I extends unknown[], T>(
setLoading(false);
});
},
reset() {
setData(undefined);
setError(undefined);
setLoading(false);
},
}),
[error, data, loading],
);