OpenAPI: support lazy field reveal on playground

This commit is contained in:
Fuma Nama
2026-02-26 18:34:19 +08:00
parent fcdaa9f7fd
commit b01665e14b
6 changed files with 74 additions and 124 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
support lazy field reveal on playground
@@ -38,17 +38,45 @@ export function ObjectInput({
fieldName: FieldKey;
} & ComponentProps<'div'>) {
const field = useResolvedSchema(_field);
const { patternProperties = {}, additionalProperties } = field;
const schemaPropKeys = field.properties ? Object.keys(field.properties) : [];
const isLazy = field['x-playground-lazy'] ?? schemaPropKeys.length > 100;
const isDynamic = Object.keys(patternProperties).length > 0 || additionalProperties !== undefined;
const [nextName, setNextName] = useState('');
const { properties, onAppend, onDelete } = useObject(fieldName, {
const { properties, onAppend, onDelete, _objectKeys } = useObject(fieldName, {
lazy: isLazy,
defaultValue: () => getDefaultValue(field) as object,
properties: field.properties ?? {},
fallback: field.additionalProperties,
patternProperties: field.patternProperties,
fallback: additionalProperties,
patternProperties: patternProperties,
});
const isDynamic = field.patternProperties ?? field.additionalProperties;
const hiddenProperties = isLazy ? schemaPropKeys.filter((key) => !_objectKeys.includes(key)) : [];
return (
<div {...props} className={cn('grid grid-cols-1 gap-4 @md:grid-cols-2', props.className)}>
<div
{...props}
className={cn(
'grid grid-cols-1 gap-4 @md:grid-cols-2 *:data-[collapsible=true]:order-last',
props.className,
)}
>
{isLazy && hiddenProperties.length > 0 && (
<Select value="" onValueChange={onAppend}>
<SelectTrigger className="col-span-full">
<SelectValue placeholder="Show Property" />
</SelectTrigger>
<SelectContent>
{hiddenProperties.map((key) => (
<SelectItem key={key} value={key}>
{key}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{properties.map((child) => {
let toolbar: ReactNode = null;
if (child.kind === 'pattern' || child.kind === 'fallback') {
@@ -83,7 +111,7 @@ export function ObjectInput({
);
})}
{isDynamic && (
<div className="flex gap-2 col-span-full">
<div className="flex gap-2 order-last col-span-full">
<Input
value={nextName}
placeholder="Enter Property Name"
@@ -190,9 +218,12 @@ export function FieldInput({
const idx = field.enum.indexOf(value);
return (
<Select value={String(idx)} onValueChange={(v) => setValue(field.enum![Number(v)])}>
<Select
value={idx === -1 && isRequired ? '' : String(idx)}
onValueChange={(v) => setValue(field.enum![Number(v)])}
>
<SelectTrigger id={id} {...props}>
<SelectValue />
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
{field.enum.map((item, i) => (
@@ -413,6 +444,7 @@ export function FieldSet({
return (
<fieldset
{...props}
data-collapsible={collapsible}
className={cn('flex flex-col gap-1.5 col-span-full @container', props.className)}
>
<div className={fieldLabelVariants()}>
@@ -434,7 +466,11 @@ export function FieldSet({
if (field.type === 'array') {
return (
<fieldset {...props} className={cn('flex flex-col gap-1.5 col-span-full', props.className)}>
<fieldset
{...props}
data-collapsible={collapsible}
className={cn('flex flex-col gap-1.5 col-span-full', props.className)}
>
<div className={fieldLabelVariants()}>
{renderLabelTrigger()}
{slotType ?? <FieldLabelType>{schemaToString(field)}</FieldLabelType>}
@@ -16,7 +16,7 @@ const SelectTrigger = forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex items-center w-full rounded-md border p-2 gap-2 text-start text-sm text-fd-secondary-foreground bg-fd-secondary hover:bg-fd-accent focus:outline-none focus:ring focus:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex items-center w-full rounded-md border p-2 gap-2 text-start text-sm text-fd-secondary-foreground bg-fd-secondary hover:bg-fd-accent focus:outline-none focus:ring focus:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50 data-placeholder:text-fd-muted-foreground',
className,
)}
{...props}
+3 -1
View File
@@ -28,7 +28,9 @@ type NoReferenceJSONSchema<T> = T extends (infer I)[]
? Omit<T, '$ref'>
: T;
export type ParsedSchema = JSONSchema;
export type ParsedSchema = JSONSchema & {
'x-playground-lazy'?: boolean;
};
export type ResolvedSchema = NoReferenceJSONSchema<ParsedSchema>;
export function getPreferredType(body: Record<string, unknown>): string | undefined {
+18 -6
View File
@@ -101,14 +101,23 @@ export function useObject<T>(
field: FieldKey,
options: {
defaultValue?: DefaultValue<object>;
/** ignore fixed properties unless defined */
lazy?: boolean;
properties: Record<string, T>;
patternProperties?: Record<string, T>;
fallback?: T;
},
) {
const {
properties: definedProps,
patternProperties: definedPatternProps = {},
defaultValue,
fallback,
lazy,
} = options;
const engine = useDataEngine();
const [objectKeys] = useFieldValue(field, {
defaultValue: options.defaultValue,
defaultValue,
compute(currentValue) {
return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
},
@@ -120,8 +129,10 @@ export function useObject<T>(
const properties = useMemo(() => {
const properties: PropertyItemInfo<T>[] = [];
const unknownKeys = new Set(objectKeys);
for (const [key, prop] of Object.entries(options.properties)) {
for (const [key, prop] of Object.entries(definedProps)) {
if (lazy && !unknownKeys.has(key)) continue;
unknownKeys.delete(key);
properties.push({
kind: 'fixed',
field: [...field, key],
@@ -130,7 +141,7 @@ export function useObject<T>(
});
}
for (const [pattern, prop] of Object.entries(options.patternProperties ?? {})) {
for (const [pattern, prop] of Object.entries(definedPatternProps)) {
const regex = RegExp(pattern);
for (const key of unknownKeys) {
@@ -146,22 +157,23 @@ export function useObject<T>(
}
}
if (options.fallback) {
if (fallback) {
for (const key of unknownKeys) {
properties.push({
kind: 'fallback',
field: [...field, key],
key,
info: options.fallback,
info: fallback,
});
}
}
return properties;
}, [field, objectKeys, options.fallback, options.patternProperties, options.properties]);
}, [definedPatternProps, definedProps, fallback, field, lazy, objectKeys]);
return {
properties,
_objectKeys: objectKeys,
onAppend(name: string, value?: unknown) {
name = name.trim();
if (name.length === 0) return;
+2 -107
View File
@@ -94,7 +94,7 @@ importers:
version: 3.22.0
'@takumi-rs/image-response':
specifier: ^0.68.4
version: 0.68.4
version: 0.68.17
ai:
specifier: ^6.0.86
version: 6.0.86([email protected])
@@ -6447,122 +6447,61 @@ packages:
cpu: [arm64]
os: [darwin]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-JCk4F53rB9h9lFkLj1S55CwYKO4fB2OqQhnakZMgEPs1HvdpH0xYLjzxGWrG075jhFL1alvZrAQxQYG5e8RJUw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [darwin]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-z4rQ4QLCIeHm9AWLiPBU8gG4gPTScFUC8zKFgCxlYMORgNcnija7VoeBfhc04rLL3h7LWWHPrp7Q2V8r7cpMHw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [darwin]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-/cvi1wtDcLJjnRQk2wtS1iDq2CRZQzIr7ndPVqFjRYbCh2xWCl66s8KBCIiXgkaqWpC85adW0e2pBilIoj5GyQ==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [darwin]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-PRyL7KEujZDskgVf16KVkjVRgTw2QhznRmYYa/1YI2PWXcYvheEtOMRCL2B7dLbmFVsh1agXYMQrBEBEyH0YUA==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-birLK4nOFgd0qfVWsHIsfl7R14vMaqFOevklF437JH526BUuDLnWvOFc6iyckb5vtIGDQy9Z3NrP/KgeoIqyRw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-4CiEF518wDnujF0fjql2XN6uO+OXl0svy0WgAF2656dCx2gJtWscaHytT2rsQ0ZmoFWE0dyWcDW1g/FBVPvuvA==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-Y1DImklVf06pLUYk0TUNLy/DwYtis0ewqECe0sdY7yHWA2n+4lAIff74uJT2tmLqOG0AmxONSFtvy1lOGfNdiQ==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-jm8lTe2E6Tfq2b97GJC31TWK1JAEv+MsVbvL9DCLlYcafgYFlMXDUnOkZFMjlrmh0HcFAYDaBkniNDgIQfXqzg==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-uxFeTPcw/nYvAkDFp2F2LAMS1bmCOrRwJsF5IiK8mJAmrr4iqRfKAKk7S1B1Lbsw4n1SQslbbb0yD853F/UTKA==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-nbdzQgC4ywzltDDV1fer1cKswwGE+xXZHdDiacdd7RM5XBng209Bmo3j1iv9dsX+4xXhByzCCGbxdWhhHqVXmw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-cBtwuxUMg7M7w5ih8FURUmIViqTt7Id5ityLd1qgR3EDbN6TtClJnG0jDUnuPX2JRbZd/BpZWZxDJbT65G+HGQ==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [linux]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-kE4F0LRmuhSwiNkFG7dTY9ID8+B7zb97QedyN/IO2fBJmRQDkqCGcip2gloh8YPPhCuKGjCqqqh2L+Tg9PKW7w==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [win32]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-Zuvkvgv2o+UMaSUpJKXY+9HLJrSc+9yhvVtG9h8+9CKyz7k9kOJKWrGsumU2xCVmHHVk58o0dOushIRwCc8VIQ==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [arm64]
os: [win32]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-oPbpEsc/POZcTzE1Om+nEI3IQs687iD/W2DMzs1LRlG4VY1vDF2m2mI7a0R3nQgFdOYKX3kkFIEsxY5qSTstnw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [win32]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-r7o+R/UT2QSdxOzXjNk1+phxgqlP3cfOSvCzRjko6vbWnoXLqX38uvzOFWnUpgiBOwKUO9xAGmx6KfjpFxYUtA==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
cpu: [x64]
os: [win32]
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-kW4Z/93RkBZHVF5aoc4mh9kbyEJam0kGv/RbZKdUpg7ZQ97uetgJ/pY3OwcSsRY9bL5WmlBm2+s+EW814WXqpQ==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-3Uj7kzdyVpRq66uW2MGVD2bcURaQN86HARNNAhHBDbV45jfnx0BwCQfrTHPvSQzGxZ0sbrynwwTVretXyAATOw==}
engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-aREzIBHpGaUKqvl371UFx+HspMtcN9Y6SgwQ2HQZ8VcLn/C4uJYHkk4vg2Z0cIJsiMxxvc+8d58J/9sxzwc0vA==}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-vwhfTk+DXJ9WFBmAhWR38mFlYzWgy60oqcF84SNxtBd3vFF8misJIGSEEYlIZjj+1EKYCmszKbrH+CLvxWySUw==}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-Crnmfe7+ml3zACcS+mpgi7yJ0oTtJgXRsdphsDv0wY5LWNdKrNNMCsUE5Qp1zs7AdcuzWL0bYyBctRlImRTTdQ==}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-LiQ243RC4wNQKLKC8HH6wRyBsTSKLyBeLGwfm0UzGOMUWMeAtVbCzcy5G6nPifdPcr01btmV2LouEP6CCBBLog==}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-5fUGeQg+T8+m4huLxN6CjGvhCSD400WfO8J0X9Ud8ULpefFj3rP3a+eh9AhezlF9HqeltS+ODzB2BFbmmHCTVA==}
'@takumi-rs/[email protected]':
resolution: {integrity: sha512-eklfvfrWjwhR1XQHMI0Tt+1WZINsvMeKBrT3pq7G5IyM8P9e61mH28NRs0cyFCDxHu+lgGqHzyJylFBp7iKgtQ==}
'@tanstack/[email protected]':
resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==}
engines: {node: '>=18'}
@@ -7588,6 +7527,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-v5hkh1us7sMNjfimWE70flYbD5I1/qWQaqmJ45q2qk5H/7muQVa478LSVRSFyGTBUBog2LsPQnfIRdjyWJRY+A==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -15878,51 +15818,27 @@ snapshots:
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optional: true
'@takumi-rs/[email protected]':
optionalDependencies:
'@takumi-rs/core-darwin-arm64': 0.68.17
@@ -15934,37 +15850,16 @@ snapshots:
'@takumi-rs/core-win32-arm64-msvc': 0.68.17
'@takumi-rs/core-win32-x64-msvc': 0.68.17
'@takumi-rs/[email protected]':
optionalDependencies:
'@takumi-rs/core-darwin-arm64': 0.68.4
'@takumi-rs/core-darwin-x64': 0.68.4
'@takumi-rs/core-linux-arm64-gnu': 0.68.4
'@takumi-rs/core-linux-arm64-musl': 0.68.4
'@takumi-rs/core-linux-x64-gnu': 0.68.4
'@takumi-rs/core-linux-x64-musl': 0.68.4
'@takumi-rs/core-win32-arm64-msvc': 0.68.4
'@takumi-rs/core-win32-x64-msvc': 0.68.4
'@takumi-rs/[email protected]': {}
'@takumi-rs/[email protected]': {}
'@takumi-rs/[email protected]':
dependencies:
'@takumi-rs/core': 0.68.17
'@takumi-rs/helpers': 0.68.17
'@takumi-rs/wasm': 0.68.17
'@takumi-rs/[email protected]':
dependencies:
'@takumi-rs/core': 0.68.4
'@takumi-rs/helpers': 0.68.4
'@takumi-rs/wasm': 0.68.4
'@takumi-rs/[email protected]': {}
'@takumi-rs/[email protected]': {}
'@tanstack/[email protected]':
dependencies:
'@tanstack/devtools-event-client': 0.4.0