Files
Dominic R 37f7cc710b website/docs: Fix release notes cards (#22554)
Render release note version labels without Docusaurus' leading digit icon split and improve the generated release index description.

Agent-thread: https://sdko.org/internal/threads/019e4d1f-3a81-7191-acba-2f1740acab52

Co-authored-by: Agent <agent@svc.sdko.net>
2026-05-22 02:19:27 +00:00

138 lines
4.7 KiB
TypeScript

// Shared utilities and types
import { type GlossaryItem, isGlossaryItem, isGlossaryPath } from "../utils/glossaryUtils";
import ErrorBoundary from "./ErrorBoundary";
import GlossaryDocCardList from "./GlossaryDocCardList";
import styles from "./styles.module.css";
// Docusaurus core imports
import type { PropSidebarItem } from "@docusaurus/plugin-content-docs";
import {
filterDocCardListItems,
useCurrentSidebarSiblings,
useDocById,
} from "@docusaurus/plugin-content-docs/client";
import { useLocation } from "@docusaurus/router";
import DocCard from "@theme/DocCard";
import Layout from "@theme/DocCard/Layout";
import type { Props } from "@theme/DocCardList";
import clsx from "clsx";
import React, { ReactNode, useMemo } from "react";
// Constant empty array to avoid creating new array on each render
const EMPTY_SIDEBAR_ITEMS: PropSidebarItem[] = [];
// Type aliases for clarity
type SidebarDocLike = Extract<PropSidebarItem, { type: "link" }>;
function isReleaseDocLink(item: PropSidebarItem): item is SidebarDocLike {
return item.type === "link" && !!item.docId?.startsWith("releases/");
}
/**
* Type-safe property existence checker with proper typing
*/
function hasOwnProperty<T extends object, K extends PropertyKey>(
value: T,
key: K,
): value is T & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(value, key);
}
/**
* Generates stable React keys from sidebar items, with fallback to index
*/
function getStableKey(item: GlossaryItem | PropSidebarItem, idx: number): string | number {
if (typeof item === "object" && item !== null) {
const match = ["docId", "id", "href", "label"].find(
(name) => hasOwnProperty(item, name) && typeof item[name] === "string",
);
return match ? ((item as Record<string, unknown>)[match] as string) : idx;
}
return idx;
}
/**
* Release documentation card item
*/
function ReleaseDocCard({ item }: { item: SidebarDocLike }) {
const doc = useDocById(item.docId ?? undefined);
// Docusaurus treats leading digits as emoji-like graphemes and splits the
// "2" out into the card icon slot, making version labels look broken.
return (
<Layout
item={item}
className={item.className}
href={item.href}
icon={null}
title={item.label}
description={item.description ?? doc?.description}
/>
);
}
/**
* Standard documentation card item for non-glossary content
*/
function DocCardListItem({ item }: { item: React.ComponentProps<typeof DocCard>["item"] }) {
return (
<article className={clsx(styles.docCardListItem, "col col--6")}>
{isReleaseDocLink(item) ? <ReleaseDocCard item={item} /> : <DocCard item={item} />}
</article>
);
}
/**
* Enhanced DocCardList component that delegates to specialized components based on content type.
* Provides both standard documentation card rendering and specialized glossary functionality.
*/
export default function DocCardList(props: Props): ReactNode {
const { items, className } = props;
const pathname = useLocation()?.pathname ?? "";
const isGlossary = isGlossaryPath(pathname);
const sidebarSiblings = useCurrentSidebarSiblings();
const siblings = sidebarSiblings ?? EMPTY_SIDEBAR_ITEMS;
// Extract glossary terms from sidebar structure (always computed, but only used for glossary pages)
const glossaryPool = useMemo<SidebarDocLike[]>(() => {
const terms: SidebarDocLike[] = [];
// Recursively process sidebar items to find glossary terms
const processItem = (item: PropSidebarItem) => {
if (isGlossaryItem(item) && item.type === "link") {
terms.push(item as SidebarDocLike);
} else if (item.type === "category" && item.items) {
item.items.forEach(processItem);
}
};
siblings.forEach(processItem);
return terms;
}, [siblings]);
// Standard documentation card items (always computed, but only used for non-glossary pages)
const baseItems = useMemo(() => filterDocCardListItems(items ?? siblings), [items, siblings]);
// For glossary pages, delegate to specialized GlossaryDocCardList component
if (isGlossary) {
return (
<ErrorBoundary>
<GlossaryDocCardList glossaryPool={glossaryPool} className={className} />
</ErrorBoundary>
);
}
// Standard documentation card rendering for non-glossary pages
return (
<section className={clsx("row", className)}>
{baseItems.map((item, idx) => (
<DocCardListItem key={getStableKey(item, idx)} item={item} />
))}
</section>
);
}
export { DocCardList };