From e2998091b5cdfe75649702b0e55e1596e9976165 Mon Sep 17 00:00:00 2001 From: winkerVSbecks Date: Sat, 31 Aug 2024 15:17:51 -0400 Subject: [PATCH] generate breadcrumbs in the nav groups transform and use the transformed data for SearchMeta --- .../Navigation/CollapsibleGroup.tsx | 4 +- src/components/Navigation/Navigation.astro | 17 +- src/components/Navigation/nav-groups.ts | 91 ++++ .../Navigation/transform-nav-groups.test.ts | 458 ++++++++++++++++-- .../Navigation/transform-nav-groups.ts | 50 +- src/components/Navigation/types.ts | 1 + src/components/SearchMeta.astro | 53 +- src/layouts/BaseLayout.astro | 99 +--- 8 files changed, 582 insertions(+), 191 deletions(-) create mode 100644 src/components/Navigation/nav-groups.ts diff --git a/src/components/Navigation/CollapsibleGroup.tsx b/src/components/Navigation/CollapsibleGroup.tsx index cd018b5b..0ac1d04e 100644 --- a/src/components/Navigation/CollapsibleGroup.tsx +++ b/src/components/Navigation/CollapsibleGroup.tsx @@ -7,7 +7,9 @@ import { type TransformedNavGroup, } from "./types"; -const Trigger = styled(Collapsible.Trigger)<{ nested?: boolean }>` +const Trigger = styled(Collapsible.Trigger, { + shouldForwardProp: (prop) => prop !== "nested", +})<{ nested?: boolean }>` all: unset; display: flex; align-items: center; diff --git a/src/components/Navigation/Navigation.astro b/src/components/Navigation/Navigation.astro index 41076ded..a370735f 100644 --- a/src/components/Navigation/Navigation.astro +++ b/src/components/Navigation/Navigation.astro @@ -4,26 +4,19 @@ import { Support } from "../Support"; import { SideNav } from "./SideNav"; import { Container } from "./styles"; import { DropdownNav } from "./DropdownNav"; -import { transformNavGroups } from "./transform-nav-groups"; -import type { NavGroup } from "./types"; +import type { TransformedNavGroup } from "./types"; interface Props { url: string; - navGroups: NavGroup[]; + navGroups: TransformedNavGroup[]; } -const { navGroups: navItems, url } = Astro.props; - -const transformedNavGroups = navItems ? transformNavGroups(navItems) : []; +const { navGroups, url } = Astro.props; --- - - + + diff --git a/src/components/Navigation/nav-groups.ts b/src/components/Navigation/nav-groups.ts new file mode 100644 index 00000000..63ce9342 --- /dev/null +++ b/src/components/Navigation/nav-groups.ts @@ -0,0 +1,91 @@ +import { getCollection } from "astro:content"; +import { transformNavGroups, flattenGroups } from "./transform-nav-groups"; + +const overview = await getCollection("overview"); +const storybook = await getCollection("storybook"); +const playwright = await getCollection("playwright"); +const cypress = await getCollection("cypress"); +const configuration = await getCollection("configuration"); +const modes = await getCollection("modes"); +const snapshot = await getCollection("snapshot"); +const snapshotOptions = await getCollection("snapshotOptions"); +const turbosnap = await getCollection("turbosnap"); +const collaborate = await getCollection("collaborate"); +const plugins = await getCollection("plugins"); +const ci = await getCollection("ci"); +const account = await getCollection("account"); +const guides = await getCollection("guides"); +const troubleshooting = await getCollection("troubleshooting"); + +const rawNavGroups = [ + { + title: "Overview", + items: overview, + defaultOpen: true, + timeline: true, + }, + { + title: "Storybook", + items: [ + ...storybook, + { + title: "Modes", + items: modes, + }, + { + title: "TurboSnap", + items: turbosnap, + }, + ], + defaultOpen: false, + }, + { + title: "Playwright", + items: playwright, + timeline: true, + }, + { + title: "Cypress", + items: cypress, + timeline: true, + }, + { + title: "Guides", + items: guides, + }, + { + title: "Configuration", + items: configuration, + }, + { + title: "Snapshot", + items: snapshot, + }, + { + title: "Snapshot options", + items: snapshotOptions, + }, + { + title: "Collaborate", + items: collaborate, + }, + { + title: "CI", + items: ci, + }, + { + title: "Plugins", + items: plugins, + }, + { + title: "Account", + items: account, + }, + { + title: "Troubleshooting", + items: troubleshooting, + }, +]; + +export const navGroups = transformNavGroups(rawNavGroups); +export const flattenedNavItems = flattenGroups(navGroups); diff --git a/src/components/Navigation/transform-nav-groups.test.ts b/src/components/Navigation/transform-nav-groups.test.ts index 3bd0c2b3..e147b346 100644 --- a/src/components/Navigation/transform-nav-groups.test.ts +++ b/src/components/Navigation/transform-nav-groups.test.ts @@ -1,6 +1,6 @@ import { expect, test, describe } from "vitest"; import type { NavGroup } from "./types"; -import { transformNavGroups } from "./transform-nav-groups"; +import { transformNavGroups, flattenGroups } from "./transform-nav-groups"; const mockGroups: NavGroup[] = [ { @@ -204,7 +204,7 @@ const mockGroups: NavGroup[] = [ }, ]; -describe("transformNavGroups", () => { +describe("transformNavGroups > Defaults", () => { test("Uses sidebar label when defined", () => { expect( transformNavGroups([ @@ -237,6 +237,7 @@ describe("transformNavGroups", () => { order: 2, slug: "test", isHome: false, + breadcrumb: "Overview", }, ], }, @@ -275,6 +276,7 @@ describe("transformNavGroups", () => { order: 2, slug: "test", isHome: false, + breadcrumb: "Overview", }, ], }, @@ -314,6 +316,7 @@ describe("transformNavGroups", () => { order: 1, slug: "", isHome: true, + breadcrumb: "Overview", }, ], }, @@ -351,12 +354,53 @@ describe("transformNavGroups", () => { order: 999, slug: "test", isHome: false, + breadcrumb: "Overview", }, ], }, ]); }); + test("Sets hide to false when not specified", () => { + expect( + transformNavGroups([ + { + title: "Overview", + items: [ + { + id: "test.mdx", + slug: "test", + collection: "overview", + data: { + title: "Test", + sidebar: { + label: "UI Tests", + order: 2, + }, + }, + }, + ], + }, + ]), + ).toEqual([ + { + title: "Overview", + items: [ + { + hide: false, + label: "UI Tests", + order: 2, + slug: "test", + isHome: false, + breadcrumb: "Overview", + }, + ], + }, + ]); + }); +}); + +describe("transformNavGroups > Nested defaults", () => { test("Sets nested group's order to 999 when not specified", () => { expect( transformNavGroups([ @@ -398,6 +442,7 @@ describe("transformNavGroups", () => { order: 999, slug: "modes", isHome: false, + breadcrumb: "Storybook » Modes", }, ], }, @@ -406,43 +451,6 @@ describe("transformNavGroups", () => { ]); }); - test("Sets hide to false when not specified", () => { - expect( - transformNavGroups([ - { - title: "Overview", - items: [ - { - id: "test.mdx", - slug: "test", - collection: "overview", - data: { - title: "Test", - sidebar: { - label: "UI Tests", - order: 2, - }, - }, - }, - ], - }, - ]), - ).toEqual([ - { - title: "Overview", - items: [ - { - hide: false, - label: "UI Tests", - order: 2, - slug: "test", - isHome: false, - }, - ], - }, - ]); - }); - test("Sets nested group's hide to false when not specified", () => { expect( transformNavGroups([ @@ -483,6 +491,7 @@ describe("transformNavGroups", () => { order: 999, slug: "modes", isHome: false, + breadcrumb: "Storybook » Modes", }, ], }, @@ -490,7 +499,9 @@ describe("transformNavGroups", () => { }, ]); }); +}); +describe("transformNavGroups > Sorting & filtering", () => { test("transforms and sorts single level groups", () => { expect(transformNavGroups([mockGroups[0]])).toEqual([ { @@ -502,6 +513,7 @@ describe("transformNavGroups", () => { order: 1, slug: "", isHome: true, + breadcrumb: "Overview", }, { hide: false, @@ -509,6 +521,7 @@ describe("transformNavGroups", () => { order: 2, slug: "test", isHome: false, + breadcrumb: "Overview", }, { hide: false, @@ -516,6 +529,7 @@ describe("transformNavGroups", () => { order: 3, slug: "review", isHome: false, + breadcrumb: "Overview", }, { hide: false, @@ -523,6 +537,7 @@ describe("transformNavGroups", () => { order: 4, slug: "ci", isHome: false, + breadcrumb: "Overview", }, { hide: false, @@ -530,6 +545,7 @@ describe("transformNavGroups", () => { order: 5, slug: "diff-inspector", isHome: false, + breadcrumb: "Overview", }, ], }, @@ -595,6 +611,7 @@ describe("transformNavGroups", () => { order: 1, slug: "", isHome: true, + breadcrumb: "Overview", }, { hide: false, @@ -602,6 +619,7 @@ describe("transformNavGroups", () => { order: 5, slug: "diff-inspector", isHome: false, + breadcrumb: "Overview", }, ], }, @@ -619,6 +637,7 @@ describe("transformNavGroups", () => { order: 1, slug: "storybook", isHome: false, + breadcrumb: "Storybook", }, { hide: false, @@ -626,6 +645,7 @@ describe("transformNavGroups", () => { order: 2, slug: "interactions", isHome: false, + breadcrumb: "Storybook", }, { hide: false, @@ -633,6 +653,7 @@ describe("transformNavGroups", () => { order: 3, slug: "storybook/publish", isHome: false, + breadcrumb: "Storybook", }, { hide: false, @@ -640,6 +661,7 @@ describe("transformNavGroups", () => { order: 4, slug: "composition", isHome: false, + breadcrumb: "Storybook", }, { hide: false, @@ -650,6 +672,7 @@ describe("transformNavGroups", () => { order: 1, slug: "modes", isHome: false, + breadcrumb: "Storybook » Modes", }, { hide: false, @@ -657,6 +680,7 @@ describe("transformNavGroups", () => { order: 3, slug: "themes", isHome: false, + breadcrumb: "Storybook » Modes", }, { hide: false, @@ -664,6 +688,7 @@ describe("transformNavGroups", () => { order: 4, slug: "custom-decorators", isHome: false, + breadcrumb: "Storybook » Modes", }, { hide: false, @@ -671,6 +696,7 @@ describe("transformNavGroups", () => { order: 5, slug: "legacy-viewports", isHome: false, + breadcrumb: "Storybook » Modes", }, ], order: 999, @@ -681,3 +707,357 @@ describe("transformNavGroups", () => { ]); }); }); + +describe("transformNavGroups > Breadcrumbs", () => { + test("Generates breadcrumbs for single level groups", () => { + expect( + transformNavGroups([ + { + title: "Storybook", + items: [ + { + id: "setup.mdx", + slug: "storybook", + collection: "storybook", + data: { + title: "Setup", + sidebar: { + label: "Setup", + order: 1, + hide: false, + }, + }, + }, + { + id: "interactions.md", + slug: "interactions", + collection: "storybook", + data: { + title: "Interaction tests", + sidebar: { + label: "Interaction tests", + order: 2, + hide: false, + }, + }, + }, + ], + }, + ]), + ).toEqual([ + { + title: "Storybook", + items: [ + { + hide: false, + label: "Setup", + order: 1, + slug: "storybook", + isHome: false, + breadcrumb: "Storybook", + }, + { + hide: false, + label: "Interaction tests", + order: 2, + slug: "interactions", + isHome: false, + breadcrumb: "Storybook", + }, + ], + }, + ]); + }); + + test("Generates breadcrumbs for nested groups", () => { + expect( + transformNavGroups([ + { + title: "Storybook", + items: [ + { + id: "composition.md", + slug: "composition", + collection: "storybook", + data: { + title: "Composition", + sidebar: { + label: "Composition", + order: 4, + hide: false, + }, + }, + }, + + { + title: "Modes", + items: [ + { + id: "modes.mdx", + slug: "modes", + collection: "modes", + data: { + title: "Story Modes", + sidebar: { + label: "Story Modes", + order: 1, + hide: false, + }, + }, + }, + ], + }, + ], + }, + ]), + ).toEqual([ + { + title: "Storybook", + items: [ + { + hide: false, + label: "Composition", + order: 4, + slug: "composition", + isHome: false, + breadcrumb: "Storybook", + }, + { + hide: false, + items: [ + { + hide: false, + label: "Story Modes", + order: 1, + slug: "modes", + isHome: false, + breadcrumb: "Storybook » Modes", + }, + ], + order: 999, + title: "Modes", + }, + ], + }, + ]); + }); + + test("Generates breadcrumbs for deeply nested groups", () => { + expect( + transformNavGroups([ + { + title: "Storybook", + items: [ + { + id: "setup.mdx", + slug: "storybook", + collection: "storybook", + data: { + title: "Setup", + sidebar: { + label: "Setup", + order: 1, + hide: false, + }, + }, + }, + { + title: "Modes", + items: [ + { + id: "modes.mdx", + slug: "modes", + collection: "modes", + data: { + title: "Story Modes", + sidebar: { + label: "Story Modes", + order: 1, + hide: false, + }, + }, + }, + { + title: "Something", + items: [ + { + id: "interactions.md", + slug: "interactions", + collection: "storybook", + data: { + title: "Interaction tests", + sidebar: { + label: "Interaction tests", + order: 2, + hide: false, + }, + }, + }, + { + id: "publish.md", + slug: "storybook/publish", + collection: "storybook", + data: { + title: "Publish", + sidebar: { + label: "Publish", + order: 3, + hide: false, + }, + }, + }, + ], + }, + ], + }, + ], + }, + ]), + ).toEqual([ + { + title: "Storybook", + items: [ + { + hide: false, + label: "Setup", + order: 1, + slug: "storybook", + isHome: false, + breadcrumb: "Storybook", + }, + { + hide: false, + items: [ + { + hide: false, + label: "Story Modes", + order: 1, + slug: "modes", + isHome: false, + breadcrumb: "Storybook » Modes", + }, + { + hide: false, + items: [ + { + hide: false, + isHome: false, + label: "Interaction tests", + order: 2, + slug: "interactions", + breadcrumb: "Storybook » Modes » Something", + }, + { + hide: false, + isHome: false, + label: "Publish", + order: 3, + slug: "storybook/publish", + breadcrumb: "Storybook » Modes » Something", + }, + ], + order: 999, + title: "Something", + }, + ], + order: 999, + title: "Modes", + }, + ], + }, + ]); + }); +}); + +describe("flattenNavGroups", () => { + test("Flattens nested groups", () => { + expect( + flattenGroups([ + { + title: "Storybook", + items: [ + { + hide: false, + label: "Setup", + order: 1, + slug: "storybook", + isHome: false, + breadcrumb: "Storybook", + }, + { + hide: false, + items: [ + { + hide: false, + label: "Story Modes", + order: 1, + slug: "modes", + isHome: false, + breadcrumb: "Storybook » Modes", + }, + { + hide: false, + items: [ + { + hide: false, + isHome: false, + label: "Interaction tests", + order: 2, + slug: "interactions", + breadcrumb: "Storybook » Modes » Something", + }, + { + hide: false, + isHome: false, + label: "Publish", + order: 3, + slug: "storybook/publish", + breadcrumb: "Storybook » Modes » Something", + }, + ], + order: 999, + title: "Something", + }, + ], + order: 999, + title: "Modes", + }, + ], + }, + ]), + ).toEqual([ + { + hide: false, + label: "Setup", + order: 1, + slug: "storybook", + isHome: false, + breadcrumb: "Storybook", + }, + { + hide: false, + label: "Story Modes", + order: 1, + slug: "modes", + isHome: false, + breadcrumb: "Storybook » Modes", + }, + { + hide: false, + isHome: false, + label: "Interaction tests", + order: 2, + slug: "interactions", + breadcrumb: "Storybook » Modes » Something", + }, + { + hide: false, + isHome: false, + label: "Publish", + order: 3, + slug: "storybook/publish", + breadcrumb: "Storybook » Modes » Something", + }, + ]); + }); +}); diff --git a/src/components/Navigation/transform-nav-groups.ts b/src/components/Navigation/transform-nav-groups.ts index 3fed479d..02c5ccc3 100644 --- a/src/components/Navigation/transform-nav-groups.ts +++ b/src/components/Navigation/transform-nav-groups.ts @@ -2,16 +2,26 @@ import { isNestedGroup, type NavGroup, type NavGroupItem, + type NestedTransformedGroup, + type TransformedItem, + type TransformedNavGroup, type TransformedNavGroupItem, } from "./types"; -export function transformNavItem(item: NavGroupItem): TransformedNavGroupItem { +function generateBreadcrumb(path: string[], slug: string): string { + return path.join(" » "); +} + +function transformNavItem( + item: NavGroupItem, + path: string[], +): TransformedNavGroupItem | NestedTransformedGroup { if (isNestedGroup(item)) { return { ...item, order: item.order || 999, hide: item.hide || false, - items: transformSortAndFilterNavItems(item.items), + items: transformSortAndFilterNavItems(item.items, [...path, item.title]), }; } @@ -21,14 +31,16 @@ export function transformNavItem(item: NavGroupItem): TransformedNavGroupItem { order: item.data?.sidebar?.order || 999, hide: item.data?.sidebar?.hide || false, isHome: item.data?.isHome || false, + breadcrumb: generateBreadcrumb(path, item.slug), }; } -export function transformSortAndFilterNavItems( +function transformSortAndFilterNavItems( items: NavGroupItem[], + path: string[] = [], ): TransformedNavGroupItem[] { return items - .map(transformNavItem) + .map((item) => transformNavItem(item, path)) .filter((item) => !item.hide) .sort((p1, p2) => (p1.order > p2.order ? 1 : p1.order < p2.order ? -1 : 0)); } @@ -36,6 +48,34 @@ export function transformSortAndFilterNavItems( export function transformNavGroups(groups: NavGroup[]) { return groups.map((group) => ({ ...group, - items: transformSortAndFilterNavItems(group.items), + items: transformSortAndFilterNavItems(group.items, [group.title]), })); } + +export function isTransformedItem( + item: TransformedNavGroupItem | TransformedNavGroup, +): item is TransformedItem { + return ( + (item as NestedTransformedGroup | TransformedNavGroup).items === undefined + ); +} + +// Flatten the navGroups into a single array of items +export function flattenGroups( + groups: TransformedNavGroup[] | TransformedNavGroupItem[], +): TransformedItem[] { + let flattenedItems: TransformedItem[] = []; + + for (const item of groups) { + if (!isTransformedItem(item)) { + const items = flattenGroups( + (item as NestedTransformedGroup | TransformedNavGroup).items, + ); + flattenedItems.push(...items); + } else { + flattenedItems.push(item as TransformedItem); + } + } + + return flattenedItems; +} diff --git a/src/components/Navigation/types.ts b/src/components/Navigation/types.ts index 0f926e73..d08c5eac 100644 --- a/src/components/Navigation/types.ts +++ b/src/components/Navigation/types.ts @@ -38,6 +38,7 @@ export type TransformedItem = { order: number; hide: boolean; isHome?: boolean; + breadcrumb: string; }; export type TransformedNavGroupItem = TransformedItem | NestedTransformedGroup; diff --git a/src/components/SearchMeta.astro b/src/components/SearchMeta.astro index c3075993..dcb072c9 100644 --- a/src/components/SearchMeta.astro +++ b/src/components/SearchMeta.astro @@ -1,51 +1,22 @@ --- -import { Debug } from "astro:components"; -import { - type NavGroup, - type NavGroupItem, - isNestedGroup, -} from "./Navigation/types"; +import { type TransformedItem } from "./Navigation/types"; -const { navItems, slug, sidebarOrder } = Astro.props; +const { flattenedNavItems, slug } = Astro.props; -// interface NavItem { -// title: string; -// items: NavGroupItem[]; -// defaultOpen?: boolean; -// timeline?: boolean; -// } +interface Props { + slug: string; + flattenedNavItems: TransformedItem[]; +} -const findItem = ( - items: NavGroup[] | NavGroupItem[], - slug: string, -): NavGroupItem | undefined => - items.find((item) => - isNestedGroup(item) ? findItem(item.items, slug) : item.slug === slug, - ); - -const groupIndex = navItems.findIndex(({ items }: NavGroup) => - items.find( - (item) => - isNestedGroup(item) - ? item.items.some((nestedItem) => nestedItem.slug === slug) - : item.slug === slug, - // item.items ? items.find((item) => item.slug === slug) : item.slug === slug, - ), -); -const group = navItems[groupIndex]; -const groupTitle = slug === "faq" ? "FAQs" : group ? group.title : "Docs"; - -const globalOrder = -groupIndex * 10; - -// Use global and section order to determine the ranking of the page -const order = - globalOrder + (sidebarOrder !== undefined ? sidebarOrder * -1 : -100); +const itemIndex = flattenedNavItems.findIndex((i) => i.slug === slug); +const item = flattenedNavItems[itemIndex]; +const breadcrumb = item?.breadcrumb || "Docs"; +// Order is reversed so that the first item is the most relevant +const order = -(itemIndex + 1); --- - - -{groupTitle} + diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index e1f002a3..9d4e5c78 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,7 +1,6 @@ --- import type { MarkdownHeading } from "astro"; import { FullWidthContainer } from "@chromatic-com/tetra"; -import { getCollection } from "astro:content"; import "@docsearch/css"; import SEO from "../components/SEO.astro"; import Scripts from "../components/Scripts.astro"; @@ -15,6 +14,10 @@ import { projectRoot } from "../util"; import OnThisPage from "../components/OnThisPage.astro"; import SearchMeta from "../components/SearchMeta.astro"; import { globalStyles } from "../styles/global"; +import { + navGroups, + flattenedNavItems, +} from "../components/Navigation/nav-groups"; type Props = { title: string; @@ -23,96 +26,10 @@ type Props = { file?: string; headings: MarkdownHeading[]; slug: string; - sidebarOrder?: number; }; -const { title, description, slug, sidebarOrder } = Astro.props; +const { title, description, slug } = Astro.props; const { pathname } = Astro.url; -const overview = await getCollection("overview"); -const storybook = await getCollection("storybook"); -const playwright = await getCollection("playwright"); -const cypress = await getCollection("cypress"); -const configuration = await getCollection("configuration"); -const modes = await getCollection("modes"); -const snapshot = await getCollection("snapshot"); -const snapshotOptions = await getCollection("snapshotOptions"); -const turbosnap = await getCollection("turbosnap"); -const collaborate = await getCollection("collaborate"); -const plugins = await getCollection("plugins"); -const ci = await getCollection("ci"); -const account = await getCollection("account"); -const guides = await getCollection("guides"); -const troubleshooting = await getCollection("troubleshooting"); - -const navGroups = [ - { - title: "Overview", - items: overview, - defaultOpen: true, - timeline: true, - }, - { - title: "Storybook", - items: [ - ...storybook, - { - title: "Modes", - items: modes, - }, - { - title: "TurboSnap", - items: turbosnap, - }, - ], - defaultOpen: false, - }, - { - title: "Playwright", - items: playwright, - timeline: true, - }, - { - title: "Cypress", - items: cypress, - timeline: true, - }, - { - title: "Guides", - items: guides, - }, - { - title: "Configuration", - items: configuration, - }, - { - title: "Snapshot", - items: snapshot, - }, - { - title: "Snapshot options", - items: snapshotOptions, - }, - { - title: "Collaborate", - items: collaborate, - }, - { - title: "CI", - items: ci, - }, - { - title: "Plugins", - items: plugins, - }, - { - title: "Account", - items: account, - }, - { - title: "Troubleshooting", - items: troubleshooting, - }, -]; const editUrl = Astro.props.file?.replace(projectRoot, "").replace("src/", ""); --- @@ -130,11 +47,7 @@ const editUrl = Astro.props.file?.replace(projectRoot, "").replace("src/", ""); 0}> - +