mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: render deprecated badge for deprecated operations (#3281)
* OpenAPI: render deprecated badge for deprecated operations Show a yellow "Deprecated" badge for any OpenAPI operation marked `deprecated: true`. The badge appears in two places: inline with the operation heading when `showTitle` is true, and as a standalone header above the playground card when `showTitle` is false (which is the default for individual operation pages generated by `openapiSource`). This restores the behaviour originally added in #2417, which was lost during the v9→v10 move of the operation renderer from `packages/openapi/src/render/operation/index.tsx` to `packages/openapi/src/ui/operation/index.tsx`. The parameter-level, schema-property-level and auth-scheme-level deprecated badges all survived the refactor; the operation-level one was silently dropped. The example spec's `deletePlanet` operation already claimed in its description to be deprecated but never set the flag (inherited from the upstream Scalar Galaxy fixture). Setting it both fixes the inconsistency and provides a visible demo for the new badge. * OpenAPI: improve operation deprecation styles --------- Co-authored-by: Fuma Nama <[email protected]>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
OpenAPI: display a "Deprecated" badge for operations marked as `deprecated: true` in the spec, next to the operation heading and the method/path bar.
|
||||
@@ -351,6 +351,7 @@ paths:
|
||||
- Planets
|
||||
summary: Delete a planet
|
||||
operationId: deletePlanet
|
||||
deprecated: true
|
||||
description: This endpoint was used to delete planets. Unfortunately, that
|
||||
caused a lot of trouble for planets with life. So, this endpoint is now
|
||||
deprecated and should not be used anymore.
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface PlaygroundClientProps extends ComponentProps<'form'>, SchemaSco
|
||||
/** the OpenAPI document (not dereferenced) */
|
||||
doc: Document;
|
||||
proxyUrl?: string;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
export type { ResultDisplayProps };
|
||||
@@ -131,6 +132,7 @@ export default function PlaygroundClient({
|
||||
proxyUrl,
|
||||
writeOnly,
|
||||
readOnly,
|
||||
deprecated,
|
||||
...rest
|
||||
}: PlaygroundClientProps) {
|
||||
const t = useTranslations();
|
||||
@@ -279,7 +281,7 @@ export default function PlaygroundClient({
|
||||
<ServerSelect className="border-b" />
|
||||
<div className="flex flex-row items-center gap-2 text-sm p-3 not-last:pb-0">
|
||||
<MethodLabel>{method}</MethodLabel>
|
||||
<Route route={route} className="flex-1" />
|
||||
<Route route={route} className={cn('flex-1', deprecated && 'line-through')} />
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(buttonVariants({ color: 'primary', size: 'sm' }), 'w-14 py-1.5')}
|
||||
|
||||
@@ -36,6 +36,7 @@ declare module 'fumadocs-core/source' {
|
||||
export interface InternalOpenAPIMeta {
|
||||
method?: string;
|
||||
webhook?: boolean;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,6 +55,10 @@ export function openapiPlugin(): LoaderPlugin {
|
||||
const openApiData = file.data._openapi;
|
||||
if (!openApiData || typeof openApiData !== 'object') return node;
|
||||
|
||||
if (openApiData.deprecated) {
|
||||
node.name = <span className="fd-page-tree-item-name line-through">{node.name}</span>;
|
||||
}
|
||||
|
||||
if (openApiData.webhook) {
|
||||
node.name = (
|
||||
<>
|
||||
@@ -151,6 +156,7 @@ export async function openapiSource(
|
||||
? entry.item.method
|
||||
: undefined,
|
||||
webhook: entry.type === 'webhook',
|
||||
deprecated: entry.info.deprecated,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -262,6 +262,7 @@ export function createAPIPage(
|
||||
proxyUrl={ctx.proxyUrl}
|
||||
writeOnly
|
||||
readOnly={false}
|
||||
deprecated={method.deprecated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export function createClientAPIPage({
|
||||
proxyUrl={ctx.proxyUrl}
|
||||
writeOnly
|
||||
readOnly={false}
|
||||
deprecated={method.deprecated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { RequestTabs } from './request-tabs';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { getExampleRequests } from './get-example-requests';
|
||||
import { SelectTabs, SelectTabTrigger, SelectTab } from '../components/select-tab';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
const paramTypeKeys = ['path', 'query', 'header', 'cookie'] as const;
|
||||
|
||||
@@ -63,8 +64,21 @@ export function Operation({
|
||||
if (showTitle) {
|
||||
const title = method.summary || (method.operationId ? idToTitle(method.operationId) : path);
|
||||
|
||||
headNode = ctx.renderHeading(headingLevel, title);
|
||||
headNode = (
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
{ctx.renderHeading(headingLevel, title, {
|
||||
className: 'my-0!',
|
||||
})}
|
||||
{method.deprecated && (
|
||||
<Badge color="yellow" className="text-xs not-prose">
|
||||
<I18nLabel label="deprecated" />
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
headingLevel++;
|
||||
} else if (method.deprecated) {
|
||||
headNode = <Callout type="warn" title={<I18nLabel label="deprecated" />} className="mt-0!" />;
|
||||
}
|
||||
|
||||
const contentTypes = body?.content ? Object.entries(body.content) : null;
|
||||
@@ -280,7 +294,12 @@ export function Operation({
|
||||
) : (
|
||||
<div className="flex flex-row items-center gap-2.5 p-3 rounded-xl border bg-fd-card text-fd-card-foreground not-prose">
|
||||
<MethodLabel className="text-xs">{method.method}</MethodLabel>
|
||||
<code className="flex-1 overflow-auto text-nowrap text-[0.8125rem] text-fd-muted-foreground">
|
||||
<code
|
||||
className={cn(
|
||||
'flex-1 overflow-auto text-nowrap text-[0.8125rem] text-fd-muted-foreground',
|
||||
method.deprecated && 'line-through',
|
||||
)}
|
||||
>
|
||||
{path}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -64,6 +64,7 @@ export function UsageTab({
|
||||
} = useOperationContext();
|
||||
const { server } = useServerContext();
|
||||
const codegen = codeUsages.get(id);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [data, setData] = useState(
|
||||
() => examples.find((example) => example.id === selectedExampleId)?.encoded,
|
||||
);
|
||||
@@ -72,6 +73,7 @@ export function UsageTab({
|
||||
const listener: ExampleUpdateListener = (_, encoded) => setData(encoded);
|
||||
|
||||
addListener(listener);
|
||||
setMounted(true);
|
||||
return () => {
|
||||
removeListener(listener);
|
||||
};
|
||||
@@ -80,7 +82,7 @@ export function UsageTab({
|
||||
const code = useMemo(() => {
|
||||
if (!data) return;
|
||||
const url = joinURL(
|
||||
server && typeof window !== 'undefined'
|
||||
server && mounted
|
||||
? withBase(resolveServerUrl(server.url, server.variables), window.location.origin)
|
||||
: 'https://example.com',
|
||||
resolveRequestData(route, data),
|
||||
@@ -100,7 +102,7 @@ export function UsageTab({
|
||||
mediaAdapters,
|
||||
server: null,
|
||||
});
|
||||
}, [data, server, route, _client, codegen, mediaAdapters]);
|
||||
}, [data, server, route, mounted, _client, codegen, mediaAdapters]);
|
||||
|
||||
if (!code) return null;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface BaseEntry {
|
||||
info: {
|
||||
title: string;
|
||||
description?: string;
|
||||
deprecated?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -317,6 +317,7 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
||||
info: {
|
||||
title: displayName,
|
||||
description: operation.description ?? pathItem.description,
|
||||
deprecated: operation.deprecated,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -330,6 +331,7 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
||||
info: {
|
||||
title: displayName,
|
||||
description: operation.description ?? pathItem.description,
|
||||
deprecated: operation.deprecated,
|
||||
},
|
||||
item: webhook,
|
||||
});
|
||||
|
||||
@@ -61,13 +61,8 @@ export function toText(
|
||||
{
|
||||
operations: [entry.item],
|
||||
},
|
||||
{
|
||||
...options,
|
||||
...entry.info,
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
},
|
||||
options,
|
||||
entry,
|
||||
);
|
||||
case 'page':
|
||||
return generatePage(
|
||||
@@ -78,18 +73,8 @@ export function toText(
|
||||
webhooks: entry.webhooks,
|
||||
showTitle: true,
|
||||
},
|
||||
{
|
||||
...options,
|
||||
...entry.info,
|
||||
},
|
||||
entry.tag
|
||||
? {
|
||||
type: 'tag',
|
||||
tag: entry.tag,
|
||||
}
|
||||
: {
|
||||
type: 'file',
|
||||
},
|
||||
options,
|
||||
entry,
|
||||
);
|
||||
case 'webhook':
|
||||
return generatePage(
|
||||
@@ -98,13 +83,8 @@ export function toText(
|
||||
{
|
||||
webhooks: [entry.item],
|
||||
},
|
||||
{
|
||||
...options,
|
||||
...entry.info,
|
||||
},
|
||||
{
|
||||
type: 'operation',
|
||||
},
|
||||
options,
|
||||
entry,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +126,7 @@ export function generateDocument(
|
||||
export type DocumentContext =
|
||||
| {
|
||||
type: 'tag';
|
||||
tag: TagObject | undefined;
|
||||
tag: TagObject;
|
||||
}
|
||||
| {
|
||||
type: 'operation';
|
||||
@@ -159,14 +139,19 @@ function generatePage(
|
||||
schemaId: string,
|
||||
processed: DereferencedDocument,
|
||||
pageProps: Omit<ApiPageProps, 'document'>,
|
||||
options: PagesToTextOptions & {
|
||||
title: string;
|
||||
description?: string;
|
||||
},
|
||||
context: DocumentContext,
|
||||
options: PagesToTextOptions,
|
||||
entry: PageOutput | OperationOutput | WebhookOutput,
|
||||
): string {
|
||||
const { frontmatter, includeDescription = false } = options;
|
||||
const extend = frontmatter?.(options.title, options.description, context);
|
||||
const extend = frontmatter?.(
|
||||
entry.info.title,
|
||||
entry.info.description,
|
||||
entry.type === 'page'
|
||||
? entry.tag
|
||||
? { type: 'tag', tag: entry.tag }
|
||||
: { type: 'file' }
|
||||
: { type: 'operation' },
|
||||
);
|
||||
const page: ApiPageProps = {
|
||||
...pageProps,
|
||||
document: schemaId,
|
||||
@@ -178,6 +163,7 @@ function generatePage(
|
||||
|
||||
meta = {
|
||||
method: operation.method.toUpperCase(),
|
||||
deprecated: entry.info.deprecated,
|
||||
};
|
||||
} else if (page.webhooks?.length === 1) {
|
||||
const webhook = page.webhooks[0];
|
||||
@@ -185,19 +171,20 @@ function generatePage(
|
||||
meta = {
|
||||
method: webhook.method.toUpperCase(),
|
||||
webhook: true,
|
||||
deprecated: entry.info.deprecated,
|
||||
};
|
||||
}
|
||||
|
||||
const data = toStaticData(page, processed.dereferenced);
|
||||
const content: string[] = [];
|
||||
|
||||
if (options.description && includeDescription) content.push(options.description);
|
||||
if (entry.info.description && includeDescription) content.push(entry.info.description);
|
||||
content.push(pageContent(page));
|
||||
|
||||
return generateDocument(
|
||||
{
|
||||
title: options.title,
|
||||
description: !includeDescription ? options.description : undefined,
|
||||
title: entry.info.title,
|
||||
description: !includeDescription ? entry.info.description : undefined,
|
||||
full: true,
|
||||
...extend,
|
||||
_openapi: {
|
||||
|
||||
Reference in New Issue
Block a user