From 45a2922ac927d549018d5fc832d6ef04de4c434d Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Sat, 28 Feb 2026 21:24:42 +0200 Subject: [PATCH] fix(plugin): generate unique sidebar keys for tagGroup items Signed-off-by: Emmanuel Ferdman --- .../src/sidebars/index.test.ts | 94 +++++++++++++++++++ .../src/sidebars/index.ts | 33 +++++-- 2 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 packages/docusaurus-plugin-openapi-docs/src/sidebars/index.test.ts diff --git a/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.test.ts b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.test.ts new file mode 100644 index 000000000..06a9d16ad --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.test.ts @@ -0,0 +1,94 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import type { TagGroupObject, TagObject } from "../openapi/types"; +import type { ApiMetadata } from "../types"; +import generateSidebarSlice from "./index"; + +describe("generateSidebarSlice", () => { + describe("tagGroup with overlapping tags", () => { + const mockApiItems: ApiMetadata[] = [ + { + type: "api", + id: "get-books", + unversionedId: "get-books", + title: "Get Books", + description: "", + source: "", + sourceDirName: "", + permalink: "/get-books", + frontMatter: {}, + api: { + method: "get", + path: "/books", + tags: ["Books", "Deprecated"], + jsonRequestBodyExample: "", + info: { title: "Test API", version: "1.0.0" }, + }, + } as ApiMetadata, + ]; + + const mockTags: TagObject[][] = [ + [ + { name: "Books", description: "Book operations" }, + { name: "Deprecated", description: "Deprecated endpoints" }, + ], + ]; + + const mockTagGroups: TagGroupObject[] = [ + { name: "Library", tags: ["Books"] }, + { name: "Deprecation", tags: ["Deprecated"] }, + ]; + + function collectKeys(obj: unknown): string[] { + const keys: string[] = []; + JSON.stringify(obj, (k, v) => { + if (k === "key" && typeof v === "string") { + keys.push(v); + } + return v; + }); + return keys; + } + + it("should generate unique keys for items appearing in multiple tagGroups", () => { + const result = generateSidebarSlice( + { groupPathsBy: "tagGroup" }, + { outputDir: "docs/test", specPath: "" }, + mockApiItems, + mockTags, + "", + mockTagGroups + ); + + const keys = collectKeys(result); + + expect(keys.length).toBeGreaterThan(0); + expect(new Set(keys).size).toBe(keys.length); + }); + + it("should include tagGroup name in keys to differentiate same items", () => { + const result = generateSidebarSlice( + { groupPathsBy: "tagGroup" }, + { outputDir: "docs/test", specPath: "" }, + mockApiItems, + mockTags, + "", + mockTagGroups + ); + + const keys = collectKeys(result); + + expect(keys.filter((k) => k.includes("library")).length).toBeGreaterThan( + 0 + ); + expect( + keys.filter((k) => k.includes("deprecation")).length + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts index 1f7376f81..84af908b4 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/sidebars/index.ts @@ -77,7 +77,8 @@ function groupByTags( sidebarOptions: SidebarOptions, options: APIOptions, tags: TagObject[][], - docPath: string + docPath: string, + tagGroupKey?: string ): ProcessedSidebar { let { outputDir, label, showSchemas } = options; @@ -152,9 +153,11 @@ function groupByTags( if (infoItems.length === 1) { const infoItem = infoItems[0]; const id = infoItem.id; + const docId = basePath === "" || undefined ? `${id}` : `${basePath}/${id}`; rootIntroDoc = { type: "doc" as const, - id: basePath === "" || undefined ? `${id}` : `${basePath}/${id}`, + id: docId, + ...(tagGroupKey && { key: kebabCase(`${tagGroupKey}-${docId}`) }), }; } @@ -219,15 +222,28 @@ function groupByTags( (item) => !!item.schema["x-tags"]?.includes(tag) ); + const categoryLabel = tagObject?.["x-displayName"] ?? tag; + const categoryKey = tagGroupKey + ? kebabCase(`${tagGroupKey}-${categoryLabel}`) + : undefined; + return { type: "category" as const, - label: tagObject?.["x-displayName"] ?? tag, + label: categoryLabel, + ...(categoryKey && { key: categoryKey }), link: linkConfig, collapsible: sidebarCollapsible, collapsed: sidebarCollapsed, - items: [...taggedSchemaItems, ...taggedApiItems].map((item) => - createDocItemFn(item, createDocItemFnContext) - ), + items: [...taggedSchemaItems, ...taggedApiItems].map((item) => { + const docItem = createDocItemFn(item, createDocItemFnContext); + if (tagGroupKey && docItem.type === "doc") { + return { + ...docItem, + key: kebabCase(`${tagGroupKey}-${tag}-${docItem.id}`), + }; + } + return docItem; + }), }; }) .filter((item) => item.items.length > 0); // Filter out any categories with no items. @@ -296,6 +312,8 @@ export default function generateSidebarSlice( } }); + const tagGroupKey = kebabCase(tagGroup.name); + const groupCategory = { type: "category" as const, label: tagGroup.name, @@ -306,7 +324,8 @@ export default function generateSidebarSlice( sidebarOptions, options, [filteredTags], - docPath + docPath, + tagGroupKey ), };