|
| 1 | +'use client'; |
| 2 | +import { Custom } from 'fumadocs-openapi/playground/client'; |
| 3 | +import { useState } from 'react'; |
| 4 | + |
| 5 | +export function BodyFieldWithExpandedParams({ |
| 6 | + fieldName, |
| 7 | + info |
| 8 | +}: { |
| 9 | + fieldName: 'body'; |
| 10 | + info: { schema: any; mediaType: string } |
| 11 | +}) { |
| 12 | + const { field } = Custom.useController({ name: fieldName }); |
| 13 | + const [expandedFields, setExpandedFields] = useState<Set<string>>(new Set(['params'])); |
| 14 | + |
| 15 | + const toggleField = (key: string) => { |
| 16 | + setExpandedFields(prev => { |
| 17 | + const newSet = new Set(prev); |
| 18 | + if (newSet.has(key)) { |
| 19 | + newSet.delete(key); |
| 20 | + } else { |
| 21 | + newSet.add(key); |
| 22 | + } |
| 23 | + return newSet; |
| 24 | + }); |
| 25 | + }; |
| 26 | + |
| 27 | + const renderSchema = (schema: any, path: string = '', level: number = 0): React.ReactNode => { |
| 28 | + if (!schema || typeof schema !== 'object') return null; |
| 29 | + |
| 30 | + const properties = schema.properties || {}; |
| 31 | + const required = schema.required || []; |
| 32 | + |
| 33 | + return Object.entries(properties).map(([key, propSchema]: [string, any]) => { |
| 34 | + const fieldPath = path ? `${path}.${key}` : key; |
| 35 | + const isRequired = required.includes(key); |
| 36 | + const isObject = propSchema.type === 'object'; |
| 37 | + const isExpanded = expandedFields.has(key); |
| 38 | + |
| 39 | + return ( |
| 40 | + <fieldset key={fieldPath} className={`flex flex-col gap-1.5 ${isObject && isExpanded ? 'col-span-full @container' : ''}`}> |
| 41 | + <label htmlFor={`body.${fieldPath}`} className="w-full inline-flex items-center gap-0.5"> |
| 42 | + {isObject && ( |
| 43 | + <button |
| 44 | + type="button" |
| 45 | + onClick={() => toggleField(key)} |
| 46 | + className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring hover:bg-fd-accent hover:text-fd-accent-foreground p-1 [&_svg]:size-4 text-fd-muted-foreground -ms-1" |
| 47 | + > |
| 48 | + <svg |
| 49 | + xmlns="http://www.w3.org/2000/svg" |
| 50 | + width="24" |
| 51 | + height="24" |
| 52 | + viewBox="0 0 24 24" |
| 53 | + fill="none" |
| 54 | + stroke="currentColor" |
| 55 | + strokeWidth="2" |
| 56 | + strokeLinecap="round" |
| 57 | + strokeLinejoin="round" |
| 58 | + style={{ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }} |
| 59 | + > |
| 60 | + <path d="m6 9 6 6 6-6"></path> |
| 61 | + </svg> |
| 62 | + </button> |
| 63 | + )} |
| 64 | + <span className="text-xs font-medium text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70 font-mono me-auto"> |
| 65 | + {key} |
| 66 | + {isRequired && <span className="text-red-400/80 mx-1">*</span>} |
| 67 | + </span> |
| 68 | + <code className="text-xs text-fd-muted-foreground">{propSchema.type || 'any'}</code> |
| 69 | + </label> |
| 70 | + |
| 71 | + {!isObject && ( |
| 72 | + <div className="flex flex-row gap-2"> |
| 73 | + <input |
| 74 | + className="flex h-9 w-full rounded-md border bg-fd-secondary px-2 py-1.5 text-[13px] text-fd-secondary-foreground transition-colors placeholder:text-fd-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50" |
| 75 | + id={`body.${fieldPath}`} |
| 76 | + placeholder={propSchema.default || "Enter value"} |
| 77 | + type={propSchema.type === 'integer' || propSchema.type === 'number' ? 'number' : 'text'} |
| 78 | + value={(field.value && typeof field.value === 'object' && key in field.value ? (field.value as Record<string, any>)[key] : propSchema.default) || ''} |
| 79 | + onChange={(e) => { |
| 80 | + const newValue = propSchema.type === 'integer' || propSchema.type === 'number' |
| 81 | + ? Number(e.target.value) |
| 82 | + : e.target.value; |
| 83 | + const currentValue = (field.value && typeof field.value === 'object') ? field.value as Record<string, any> : {}; |
| 84 | + field.onChange({ ...currentValue, [key]: newValue }); |
| 85 | + }} |
| 86 | + name={`body.${fieldPath}`} |
| 87 | + {...(propSchema.type === 'number' || propSchema.type === 'integer' ? { step: propSchema.type === 'integer' ? '1' : 'any' } : {})} |
| 88 | + /> |
| 89 | + </div> |
| 90 | + )} |
| 91 | + |
| 92 | + {isObject && isExpanded && ( |
| 93 | + <div className="grid grid-cols-1 gap-4 @md:grid-cols-2 rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm ml-4"> |
| 94 | + {renderSchema(propSchema, fieldPath, level + 1)} |
| 95 | + </div> |
| 96 | + )} |
| 97 | + </fieldset> |
| 98 | + ); |
| 99 | + }); |
| 100 | + }; |
| 101 | + |
| 102 | + return ( |
| 103 | + <div className="grid grid-cols-1 gap-4 @md:grid-cols-2 rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm"> |
| 104 | + {renderSchema(info.schema)} |
| 105 | + </div> |
| 106 | + ); |
| 107 | +} |
| 108 | + |
0 commit comments