OpenAPI: Add reset value button to non-required fields

This commit is contained in:
Fuma Nama
2025-10-18 22:16:34 +08:00
parent 726deaab2c
commit ef9737d46c
3 changed files with 117 additions and 88 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Add reset value button to non-required fields
+80 -85
View File
@@ -6,13 +6,8 @@ import {
useMemo,
useState,
} from 'react';
import { ChevronDown, Plus, Trash2 } from 'lucide-react';
import {
Controller,
useController,
useFieldArray,
useFormContext,
} from 'react-hook-form';
import { ChevronDown, Plus, Trash2, X } from 'lucide-react';
import { useController, useFieldArray, useFormContext } from 'react-hook-form';
import {
Select,
SelectContent,
@@ -252,98 +247,98 @@ export function FieldInput({
isRequired?: boolean;
fieldName: string;
}) {
const { control, register } = useFormContext();
const { control } = useFormContext();
const {
field: { value, onChange, ...restField },
} = useController({
control,
name: fieldName,
});
if (field.type === 'string' && field.format === 'binary') {
return (
<Controller
control={control}
name={fieldName}
render={({ field: { value, onChange, ...restField } }) => (
<div {...props}>
<label
htmlFor={fieldName}
className={cn(
buttonVariants({
color: 'secondary',
className: 'w-full h-9 gap-2 truncate',
}),
)}
>
{value instanceof File ? (
<>
<span className="text-fd-muted-foreground text-xs">
Selected
</span>
<span className="truncate w-0 flex-1 text-end">
{value.name}
</span>
</>
) : (
<span className="text-fd-muted-foreground">Upload</span>
)}
</label>
<input
id={fieldName}
type="file"
multiple={false}
onChange={(e) => {
if (!e.target.files) return;
onChange(e.target.files.item(0));
}}
hidden
{...restField}
/>
</div>
)}
/>
<div {...props}>
<label
htmlFor={fieldName}
className={cn(
buttonVariants({
color: 'secondary',
className: 'w-full h-9 gap-2 truncate',
}),
)}
>
{value instanceof File ? (
<>
<span className="text-fd-muted-foreground text-xs">Selected</span>
<span className="truncate w-0 flex-1 text-end">{value.name}</span>
</>
) : (
<span className="text-fd-muted-foreground">Upload</span>
)}
</label>
<input
id={fieldName}
type="file"
multiple={false}
onChange={(e) => {
if (!e.target.files) return;
onChange(e.target.files.item(0));
}}
hidden
{...restField}
/>
</div>
);
}
if (field.type === 'boolean') {
return (
<Controller
control={control}
name={fieldName}
render={({ field: { value, onChange, ...restField } }) => (
<Select
value={String(value)}
onValueChange={(value) =>
onChange(value === 'null' ? null : value === 'true')
}
disabled={restField.disabled}
>
<SelectTrigger
id={fieldName}
className={props.className}
{...restField}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
{!isRequired && <SelectItem value="null">Null</SelectItem>}
</SelectContent>
</Select>
)}
/>
<Select
value={String(value)}
onValueChange={(value) =>
onChange(value === 'null' ? null : value === 'true')
}
disabled={restField.disabled}
>
<SelectTrigger
id={fieldName}
className={props.className}
{...restField}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
{!isRequired && <SelectItem value="null">Null</SelectItem>}
</SelectContent>
</Select>
);
}
if (field.type === 'null') return;
return (
<Input
id={fieldName}
placeholder="Enter value"
type={field.type === 'string' ? 'text' : 'number'}
step={field.type === 'number' ? 'any' : undefined}
{...register(fieldName, {
valueAsNumber: field.type === 'number' || field.type === 'integer',
})}
{...props}
/>
<div {...props} className={cn('flex flex-row gap-2', props.className)}>
<Input
id={fieldName}
placeholder="Enter value"
type={field.type === 'string' ? 'text' : 'number'}
step={field.type === 'number' ? 'any' : undefined}
value={value ?? ''}
onChange={onChange}
{...restField}
/>
{!isRequired && value !== undefined && (
<button
type="button"
onClick={() => onChange(undefined)}
className="text-fd-muted-foreground"
>
<X className="size-4" />
</button>
)}
</div>
);
}
+32 -3
View File
@@ -1,5 +1,13 @@
'use client';
import { createContext, Fragment, type ReactNode, use, useState } from 'react';
import {
createContext,
Fragment,
type ReactNode,
use,
useEffect,
useRef,
useState,
} from 'react';
import {
Tabs,
TabsContent,
@@ -31,7 +39,18 @@ const PropertyContext = createContext<PropertyContextType>({
<PopoverTrigger className="font-mono underline text-sm text-fd-muted-foreground hover:text-fd-accent-foreground data-[state=open]:text-fd-accent-foreground">
{text}
</PopoverTrigger>
<PopoverContent className="w-[600px] h-[400px] p-0">
<PopoverContent
className="w-[600px] min-h-(--initial-height,0) max-h-[460px] p-0"
ref={(element) => {
if (!element || element.style.getPropertyValue('--initial-height'))
return;
element.style.setProperty(
'--initial-height',
`${element.clientHeight}px`,
);
}}
>
<SchemaUIPopover
initialPath={[
{
@@ -145,6 +164,7 @@ function SchemaUIProperty({
key={item.$type}
value={item.$type}
forceMount={undefined}
className="py-0"
>
<SchemaUIProperty variant="ghost" {...item} />
</TabsContent>
@@ -227,8 +247,17 @@ function SchemaUIPopover({
}) {
const ctx = useProperty();
const [path, setPath] = useState(initialPath);
const ref = useRef<HTMLDivElement>(null);
const last = path.findLast((item) => item.$ref !== undefined);
useEffect(() => {
const element = ref.current;
if (!element || !element.parentElement) return;
// reset scroll
element.parentElement.scrollTop = 0;
}, [last?.$ref]);
if (!last) return;
return (
@@ -283,7 +312,7 @@ function SchemaUIPopover({
},
}}
>
<div key={last.$ref} className="px-2">
<div ref={ref} className="px-2">
<SchemaUIProperty
name={last.name}
$type={last.$ref!}