mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
OpenAPI: Improve schema UI array rendering
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"fumadocs-openapi": patch
|
||||
---
|
||||
|
||||
Improve schema UI array rendering
|
||||
Vendored
+1
-1
@@ -14,5 +14,5 @@
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"oxc.linter.enabled": false
|
||||
"oxc.fmt.configPath": ".oxfmtrc.json"
|
||||
}
|
||||
|
||||
@@ -758,6 +758,7 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: ['test', 'hello']
|
||||
examples:
|
||||
- ['solar-system', 'rocky', 'explored']
|
||||
lastUpdated:
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'fumadocs-ui/components/tabs';
|
||||
import type { SchemaData, SchemaUIGeneratedData } from '@/ui/schema';
|
||||
import type { SchemaUIGeneratedData } from '@/ui/schema';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -26,16 +26,8 @@ import { cva } from 'class-variance-authority';
|
||||
|
||||
type DataContextType = SchemaUIGeneratedData;
|
||||
|
||||
interface PropertyContextType {
|
||||
renderRef: (options: RenderRefOptions) => ReactNode;
|
||||
}
|
||||
|
||||
interface RenderRefOptions {
|
||||
text: ReactNode;
|
||||
pathName: ReactNode;
|
||||
$ref: string;
|
||||
|
||||
inlineUnion?: boolean;
|
||||
interface PopoverContextType {
|
||||
renderTrigger: (props: { pathName: ReactNode; $ref: string; children: ReactNode }) => ReactNode;
|
||||
}
|
||||
|
||||
const typeVariants = cva('text-sm text-start text-fd-muted-foreground font-mono', {
|
||||
@@ -47,8 +39,8 @@ const typeVariants = cva('text-sm text-start text-fd-muted-foreground font-mono'
|
||||
},
|
||||
});
|
||||
|
||||
const PropertyContext = createContext<PropertyContextType>({
|
||||
renderRef: (props) => <RootRef {...props} />,
|
||||
const PopoverContext = createContext<PopoverContextType>({
|
||||
renderTrigger: (props) => <RootPopoverTrigger {...props} />,
|
||||
});
|
||||
|
||||
const DataContext = createContext<DataContextType | null>(null);
|
||||
@@ -57,8 +49,8 @@ function useData() {
|
||||
return use(DataContext)!;
|
||||
}
|
||||
|
||||
function useProperty() {
|
||||
return use(PropertyContext);
|
||||
function usePopover() {
|
||||
return use(PopoverContext);
|
||||
}
|
||||
|
||||
export interface SchemaUIProps {
|
||||
@@ -70,36 +62,43 @@ export interface SchemaUIProps {
|
||||
}
|
||||
|
||||
export function SchemaUI({ name, required = false, as = 'property', generated }: SchemaUIProps) {
|
||||
const schema = generated.refs[generated.$root];
|
||||
const context: DataContextType = useMemo(() => generated, [generated]);
|
||||
const isProperty = as === 'property' || !isExpandable(schema);
|
||||
|
||||
return (
|
||||
<DataContext value={context}>
|
||||
{isProperty ? (
|
||||
<SchemaUIProperty
|
||||
name={name}
|
||||
$type={generated.$root}
|
||||
overrides={{
|
||||
required,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SchemaUIContent $type={generated.$root} />
|
||||
)}
|
||||
<DataContext value={generated}>
|
||||
<SchemaUIProperty
|
||||
name={name}
|
||||
$type={generated.$root}
|
||||
overrides={{
|
||||
required,
|
||||
}}
|
||||
variant={
|
||||
as === 'property' || generated.refs[generated.$root].type === 'primitive'
|
||||
? 'default'
|
||||
: 'expand'
|
||||
}
|
||||
/>
|
||||
</DataContext>
|
||||
);
|
||||
}
|
||||
|
||||
function SchemaUIContent({ $type }: { $type: string }) {
|
||||
function SchemaUIProperty({
|
||||
name,
|
||||
$type,
|
||||
variant = 'default',
|
||||
overrides,
|
||||
}: {
|
||||
name: ReactNode;
|
||||
$type: string;
|
||||
variant?: 'default' | 'expand';
|
||||
overrides?: Partial<PropertyProps>;
|
||||
}) {
|
||||
const { refs } = useData();
|
||||
const schema = refs[$type];
|
||||
let child: ReactNode = null;
|
||||
const renderRef = useRenderRef();
|
||||
let type: ReactNode = schema.typeName;
|
||||
|
||||
if (schema.type === 'or' && schema.items.length > 0) {
|
||||
child = (
|
||||
<>
|
||||
{child}
|
||||
if ((schema.type === 'or' || schema.type === 'and') && schema.items.length > 0) {
|
||||
if (variant === 'expand')
|
||||
return (
|
||||
<Tabs defaultValue={schema.items[0].$type}>
|
||||
<TabsList>
|
||||
{schema.items.map((item) => (
|
||||
@@ -115,34 +114,32 @@ function SchemaUIContent({ $type }: { $type: string }) {
|
||||
forceMount={undefined}
|
||||
className="py-0"
|
||||
>
|
||||
<SchemaUIContent {...item} />
|
||||
<SchemaUIProperty {...item} variant="expand" />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'object' && schema.props.length > 0) {
|
||||
child = (
|
||||
<>
|
||||
{child}
|
||||
{schema.props.map((prop) => (
|
||||
<SchemaUIProperty
|
||||
key={prop.name}
|
||||
name={prop.name}
|
||||
$type={prop.$type}
|
||||
overrides={{ required: prop.required }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
child = (
|
||||
<>
|
||||
{child}
|
||||
);
|
||||
type = renderRef({
|
||||
pathName: name,
|
||||
$ref: $type,
|
||||
});
|
||||
} else if (schema.type === 'object' && schema.props.length > 0) {
|
||||
if (variant === 'expand')
|
||||
return schema.props.map((prop) => (
|
||||
<SchemaUIProperty
|
||||
key={prop.name}
|
||||
name={prop.name}
|
||||
$type={prop.$type}
|
||||
overrides={{ required: prop.required }}
|
||||
/>
|
||||
));
|
||||
type = renderRef({
|
||||
pathName: name,
|
||||
$ref: $type,
|
||||
});
|
||||
} else if (schema.type === 'array') {
|
||||
if (variant === 'expand')
|
||||
return (
|
||||
<Collapsible className="my-2">
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
@@ -154,57 +151,19 @@ function SchemaUIContent({ $type }: { $type: string }) {
|
||||
<ChevronDown className="size-4 text-fd-muted-foreground group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="-mt-px bg-fd-card px-3 rounded-lg rounded-tl-none border shadow-sm">
|
||||
<SchemaUIContent $type={schema.item.$type} />
|
||||
<SchemaUIProperty name="" $type={schema.item.$type} variant="expand" />
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
function SchemaUIProperty({
|
||||
name,
|
||||
$type,
|
||||
overrides,
|
||||
}: {
|
||||
name: ReactNode;
|
||||
$type: string;
|
||||
overrides?: Partial<PropertyProps>;
|
||||
}) {
|
||||
const { renderRef } = useProperty();
|
||||
const { refs } = useData();
|
||||
const schema = refs[$type];
|
||||
|
||||
let type: ReactNode = schema.typeName;
|
||||
if ((schema.type === 'or' || schema.type === 'and') && schema.items.length > 0) {
|
||||
type = renderRef({
|
||||
text: schema.aliasName,
|
||||
pathName: name,
|
||||
$ref: $type,
|
||||
inlineUnion: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.type === 'object' && schema.props.length > 0) {
|
||||
type = renderRef({
|
||||
text: schema.aliasName,
|
||||
pathName: name,
|
||||
$ref: $type,
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
type = renderRef({
|
||||
text: schema.aliasName,
|
||||
pathName: name,
|
||||
$ref: schema.item.$type,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Property name={name} type={type} deprecated={schema.deprecated} {...overrides}>
|
||||
const child = (
|
||||
<>
|
||||
{schema.description}
|
||||
{schema.infoTags && schema.infoTags.length > 0 && (
|
||||
<div className="flex flex-row gap-2 flex-wrap my-2 not-prose empty:hidden">
|
||||
@@ -213,6 +172,12 @@ function SchemaUIProperty({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (variant === 'expand') return child;
|
||||
return (
|
||||
<Property name={name} type={type} deprecated={schema.deprecated} {...overrides}>
|
||||
{child}
|
||||
</Property>
|
||||
);
|
||||
}
|
||||
@@ -230,15 +195,15 @@ function SchemaUIPopover({ initialPath }: { initialPath: { name: ReactNode; $ref
|
||||
element.parentElement.scrollTop = 0;
|
||||
}, [last?.$ref]);
|
||||
|
||||
const context: PropertyContextType = useMemo(
|
||||
const context: PopoverContextType = useMemo(
|
||||
() => ({
|
||||
renderRef: (props) => (
|
||||
<LinkRef
|
||||
{...props}
|
||||
onInsert={(name, $ref) => {
|
||||
setPath((path) => [...path, { name, $ref }]);
|
||||
}}
|
||||
/>
|
||||
renderTrigger: ({ $ref, pathName, children }) => (
|
||||
<button
|
||||
className={cn(typeVariants({ variant: 'trigger' }))}
|
||||
onClick={() => setPath((path) => [...path, { name: pathName, $ref }])}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}),
|
||||
[],
|
||||
@@ -255,7 +220,6 @@ function SchemaUIPopover({ initialPath }: { initialPath: { name: ReactNode; $ref
|
||||
isDuplicated && 'text-orange-400',
|
||||
item.$ref && 'hover:underline hover:text-fd-accent-foreground',
|
||||
);
|
||||
|
||||
const node = item.$ref ? (
|
||||
<button onClick={() => setPath((path) => path.slice(0, i + 1))} className={className}>
|
||||
{item.name}
|
||||
@@ -272,56 +236,82 @@ function SchemaUIPopover({ initialPath }: { initialPath: { name: ReactNode; $ref
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<PropertyContext value={context}>
|
||||
<PopoverContext value={context}>
|
||||
<div ref={ref} className="px-2">
|
||||
<SchemaUIContent $type={last.$ref!} />
|
||||
<SchemaUIProperty name="" $type={last.$ref!} variant="expand" />
|
||||
</div>
|
||||
</PropertyContext>
|
||||
</PopoverContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RootRef({ text, $ref, pathName, inlineUnion }: RenderRefOptions) {
|
||||
function useRenderRef() {
|
||||
const { refs } = useData();
|
||||
const { renderTrigger } = usePopover();
|
||||
return function renderRef({
|
||||
pathName,
|
||||
$ref,
|
||||
text,
|
||||
}: {
|
||||
pathName: ReactNode;
|
||||
$ref: string;
|
||||
text?: ReactNode;
|
||||
}) {
|
||||
const schema = refs[$ref];
|
||||
|
||||
if (schema.type === 'and' || schema.type === 'or') {
|
||||
const sep = schema.type === 'and' ? '&' : '|';
|
||||
return (
|
||||
<span className={cn(typeVariants(), 'flex flex-row gap-2 items-center flex-wrap')}>
|
||||
{schema.items.map((item, i) => (
|
||||
<Fragment key={item.$type}>
|
||||
{i > 0 && <span>{sep}</span>}
|
||||
{renderRef({ pathName, text: item.name, $ref: item.$type })}
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
return (
|
||||
<span className={cn(typeVariants(), 'flex flex-row items-center flex-wrap')}>
|
||||
{'array<'}
|
||||
{renderRef({ pathName: <>{pathName}[]</>, $ref: schema.item.$type })}
|
||||
{'>'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return renderTrigger({ $ref, pathName, children: text ?? schema.aliasName });
|
||||
};
|
||||
}
|
||||
|
||||
function RootPopoverTrigger({
|
||||
$ref,
|
||||
pathName,
|
||||
children,
|
||||
}: {
|
||||
pathName: ReactNode;
|
||||
$ref: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const ref = useCallback((element: HTMLDivElement | null) => {
|
||||
if (!element || element.style.getPropertyValue('--initial-height')) return;
|
||||
|
||||
element.style.setProperty('--initial-height', `${element.clientHeight}px`);
|
||||
}, []);
|
||||
|
||||
const schema = refs[$ref];
|
||||
|
||||
if (inlineUnion && (schema.type === 'and' || schema.type === 'or')) {
|
||||
const sep = schema.type === 'and' ? '&' : '|';
|
||||
return (
|
||||
<span className={cn(typeVariants(), 'flex flex-row gap-2 items-center flex-wrap')}>
|
||||
{schema.items.map((item, i) => (
|
||||
<Fragment key={item.$type}>
|
||||
{i > 0 && <span>{sep}</span>}
|
||||
<RootRef pathName={pathName} text={item.name} $ref={item.$type} />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExpandable(refs[$ref])) {
|
||||
return <span className={cn(typeVariants())}>{text}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger className={cn(typeVariants({ variant: 'trigger' }))}>{text}</PopoverTrigger>
|
||||
<PopoverTrigger className={cn(typeVariants({ variant: 'trigger' }))}>
|
||||
{children}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent ref={ref} className="w-[600px] min-h-(--initial-height,0) max-h-[460px] p-0">
|
||||
<SchemaUIPopover
|
||||
initialPath={[
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
{pathName}
|
||||
{schema.type === 'array' && '[]'}
|
||||
</>
|
||||
),
|
||||
name: pathName,
|
||||
$ref: $ref,
|
||||
},
|
||||
]}
|
||||
@@ -331,31 +321,6 @@ function RootRef({ text, $ref, pathName, inlineUnion }: RenderRefOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
function LinkRef({
|
||||
$ref,
|
||||
pathName,
|
||||
onInsert,
|
||||
text,
|
||||
}: RenderRefOptions & {
|
||||
onInsert: (name: ReactNode, $ref: string) => void;
|
||||
}) {
|
||||
const { refs } = useData();
|
||||
if (!isExpandable(refs[$ref])) {
|
||||
return <span className={cn(typeVariants())}>{text}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(typeVariants({ variant: 'trigger' }))}
|
||||
onClick={() => {
|
||||
onInsert(pathName, $ref);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface PropertyProps {
|
||||
name: ReactNode;
|
||||
type: ReactNode;
|
||||
@@ -410,7 +375,3 @@ function Property({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isExpandable(schema: SchemaData) {
|
||||
return schema.type !== 'primitive';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user