diff --git a/.storybook/main.ts b/.storybook/main.ts index 290946945a..472f128627 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,10 @@ import type { StorybookConfig } from "@storybook/nextjs"; const config: StorybookConfig = { - stories: ["../components/**/*.stories.@(js|jsx|ts|tsx)"], + stories: [ + "../components/**/*.stories.@(js|jsx|ts|tsx)", + "../layouts/**/*.stories.@(js|jsx|ts|tsx)", + ], addons: ["@storybook/addon-interactions", "@storybook/addon-viewport"], framework: { name: "@storybook/nextjs", diff --git a/layouts/DocsPage/Navigation.module.css b/layouts/DocsPage/Navigation.module.css index b3318fe14b..3d79d93ae6 100644 --- a/layouts/DocsPage/Navigation.module.css +++ b/layouts/DocsPage/Navigation.module.css @@ -107,12 +107,23 @@ display: block; } - & .link { - padding-left: var(--m-4); + & .link{ font-size: var(--fs-text-sm); line-height: var(--lh-md); } + & .link-1 { + padding-left: var(--m-4); + } + + & .link-2 { + padding-left: var(--m-5); + } + + & .link-3 { + padding-left: var(--m-6); + } + .link.active + & { display: block; } diff --git a/layouts/DocsPage/Navigation.stories.tsx b/layouts/DocsPage/Navigation.stories.tsx new file mode 100644 index 0000000000..cd3e40fc91 --- /dev/null +++ b/layouts/DocsPage/Navigation.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { userEvent, within } from "@storybook/testing-library"; +import { expect } from "@storybook/jest"; +import { default as DocNavigation } from "layouts/DocsPage/Navigation"; +import { NavigationCategory } from "./types"; + +export const NavigationFourLevels = () => { + const data = [ + { + icon: "cloud", + title: "Enroll Resources", + entries: [ + { + title: "Machine ID", + slug: "/enroll-resources/machine-id/", + entries: [ + { + title: "Deploy Machine ID", + slug: "/enroll-resources/machine-id/deployment/", + entries: [ + { + title: "Deploy Machine ID on AWS", + slug: "/enroll-resources/machine-id/deployment/aws/", + }, + ], + }, + ], + }, + ], + }, + ]; + + return ( + } + section={true} + currentVersion="16.x" + currentPathGetter={() => { + return "/enroll-resources/machine-id/deployment/aws/"; + }} + > + ); +}; + +const meta: Meta = { + title: "layouts/DocNavigation", + component: NavigationFourLevels, +}; +export default meta; diff --git a/layouts/DocsPage/Navigation.tsx b/layouts/DocsPage/Navigation.tsx index 07ad63b558..4de84d9678 100644 --- a/layouts/DocsPage/Navigation.tsx +++ b/layouts/DocsPage/Navigation.tsx @@ -24,14 +24,22 @@ const SCOPE_DICTIONARY: Record = { interface DocsNavigationItemsProps { entries: NavigationItem[]; onClick: () => void; + currentPath: string; + level?: number; } const DocsNavigationItems = ({ entries, onClick, + currentPath, + level, }: DocsNavigationItemsProps) => { - const docPath = useCurrentHref().split(SCOPELESS_HREF_REGEX)[0]; + const docPath = currentPath.split(SCOPELESS_HREF_REGEX)[0]; const { getVersionAgnosticRoute } = useVersionAgnosticPages(); + const maxLevel = 3; + if (!level) { + level = 1; + } return ( <> @@ -39,13 +47,15 @@ const DocsNavigationItems = ({ entries.map((entry) => { const selected = entry.slug === docPath; const active = - selected || entry.entries?.some((entry) => entry.slug === docPath); + selected || + entry.entries?.some((entry) => docPath.startsWith(entry.slug)); return (
  • )} - {!!entry.entries?.length && ( + {!!entry.entries?.length && level <= maxLevel && (
    )} @@ -77,6 +89,7 @@ interface DocNavigationCategoryProps extends NavigationCategory { opened: boolean; onToggleOpened: (value: number) => void; onClick: () => void; + currentPath: string; } const DocNavigationCategory = ({ @@ -87,6 +100,7 @@ const DocNavigationCategory = ({ icon, title, entries, + currentPath, }: DocNavigationCategoryProps) => { const toggleOpened = useCallback( () => onToggleOpened(opened ? null : id), @@ -105,7 +119,11 @@ const DocNavigationCategory = ({ {opened && (
      - +
    )} @@ -134,14 +152,19 @@ interface DocNavigationProps { section?: boolean; currentVersion?: string; data: NavigationCategory[]; + currentPathGetter?: () => string; } const DocNavigation = ({ data, section, currentVersion, + currentPathGetter, }: DocNavigationProps) => { - const route = useCurrentHref(); + if (!currentPathGetter) { + currentPathGetter = useCurrentHref; + } + const route = currentPathGetter(); const [openedId, setOpenedId] = useState( getCurrentCategoryIndex(data, route) @@ -171,6 +194,7 @@ const DocNavigation = ({ opened={index === openedId} onToggleOpened={setOpenedId} onClick={toggleMenu} + currentPath={route} {...props} />
  • diff --git a/server/pages-helpers.ts b/server/pages-helpers.ts index 4dfcb43e20..88f68f6f6e 100644 --- a/server/pages-helpers.ts +++ b/server/pages-helpers.ts @@ -62,6 +62,8 @@ export const getPageInfo = ( return result; }; +// getEntryForPath returns a navigation item for the file at filePath in the +// given filesystem. const getEntryForPath = (fs, filePath) => { const txt = fs.readFileSync(filePath, "utf8"); const { data } = matter(txt); @@ -108,11 +110,13 @@ const categoryPagePathForDir = (fs, dirPath) => { ); }; -export const generateNavPaths = (fs, dirPath) => { +export const navEntriesForDir = (fs, dirPath) => { const firstLvl = fs.readdirSync(dirPath, "utf8"); let result = []; let firstLvlFiles = new Set(); let firstLvlDirs = new Set(); + + // Sort the contents of dirPath into files and directoreis. firstLvl.forEach((p) => { const fullPath = join(dirPath, p); const info = fs.statSync(fullPath); @@ -120,11 +124,22 @@ export const generateNavPaths = (fs, dirPath) => { firstLvlDirs.add(fullPath); return; } + const fileName = parse(fullPath).name; + const dirName = parse(dirPath).name; + + // This is a category page for the containing directory. We would have + // already handled this in the previous iteration. The first iteration + // does not require a category page. + if (fileName == dirName) { + return; + } + firstLvlFiles.add(fullPath); }); // Map category pages to the directories they introduce so we can can add a - // sidebar entry for the category page, then traverse the directory. + // sidebar entry for each category page, then traverse each directory to add + // further sidebar pages. let sectionIntros = new Map(); firstLvlDirs.forEach((d: string) => { sectionIntros.set(categoryPagePathForDir(fs, d), d); @@ -145,6 +160,9 @@ export const generateNavPaths = (fs, dirPath) => { result.push(getEntryForPath(fs, f)); }); + // Add a category page for each section intro, then traverse the contents of + // the directory that the category page introduces, adding the contents to + // entries. sectionIntros.forEach((dirPath, categoryPagePath) => { const { slug, title } = getEntryForPath(fs, categoryPagePath); const section = { @@ -152,49 +170,14 @@ export const generateNavPaths = (fs, dirPath) => { slug: slug, entries: [], }; - const secondLvl = new Set(fs.readdirSync(dirPath, "utf8")); - - // Find all second-level category pages first so we don't - // repeat them in the sidebar. - secondLvl.forEach((f2: string) => { - let fullPath2 = join(dirPath, f2); - const stat = fs.statSync(fullPath2); - - // List category pages on the second level, but not their contents. - if (!stat.isDirectory()) { - return; - } - const catPath = categoryPagePathForDir(fs, fullPath2); - fullPath2 = catPath; - secondLvl.delete(f2); - - // Delete the category page from the set so we don't add it again - // when we add individual files. - secondLvl.delete(parse(catPath).base); - section.entries.push(getEntryForPath(fs, fullPath2)); - }); - - secondLvl.forEach((f2: string) => { - // Only add entries for MDX files here - if (!f2.endsWith(".mdx")) { - return; - } - - let fullPath2 = join(dirPath, f2); - - // This is a first-level category page that happens to exist on the second - // level. - if (sectionIntros.has(fullPath2)) { - return; - } - - const stat = fs.statSync(fullPath2); - section.entries.push(getEntryForPath(fs, fullPath2)); - }); - - section.entries.sort(sortByTitle); + + section.entries = navEntriesForDir(fs, dirPath); result.push(section); }); result.sort(sortByTitle); return result; }; + +export const generateNavPaths = (fs, dirPath) => { + return navEntriesForDir(fs, dirPath); +}; diff --git a/server/remark-toc.ts b/server/remark-toc.ts index 15bd2c88bd..d66e13936d 100644 --- a/server/remark-toc.ts +++ b/server/remark-toc.ts @@ -25,7 +25,7 @@ const relativePathToFile = (root: string, filepath: string) => { // properties: // - result: a string containing the resulting list of links. // - error: an error message encountered during processing -export const getTOC = (filePath: string, fs = nodeFS) => { +export const getTOC = (filePath: string, fs: any = nodeFS) => { const dirPath = path.dirname(filePath); if (!fs.existsSync(dirPath)) { return { diff --git a/uvu-tests/config-docs.test.ts b/uvu-tests/config-docs.test.ts index bdef7dc9a4..b7272875d6 100644 --- a/uvu-tests/config-docs.test.ts +++ b/uvu-tests/config-docs.test.ts @@ -289,54 +289,57 @@ title: MySQL Guide } ); -Suite( - "generateNavPaths shows third-level category pages on the sidebar", - () => { - const files = { - "/docs/pages/database-access/guides/guides.mdx": `--- +Suite("generateNavPaths shows third-level pages on the sidebar", () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- title: Database Access Guides ---`, - "/docs/pages/database-access/guides/postgres.mdx": `--- + "/docs/pages/database-access/guides/postgres.mdx": `--- title: Postgres Guide ---`, - "/docs/pages/database-access/guides/mysql.mdx": `--- + "/docs/pages/database-access/guides/mysql.mdx": `--- title: MySQL Guide ---`, - "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- + "/docs/pages/database-access/guides/rbac/rbac.mdx": `--- title: Database Access RBAC ---`, - "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- + "/docs/pages/database-access/guides/rbac/get-started.mdx": `--- title: Get Started with DB RBAC ---`, - }; + }; - const expected = [ - { - title: "Database Access Guides", - slug: "/database-access/guides/guides/", - entries: [ - { - title: "Database Access RBAC", - slug: "/database-access/guides/rbac/rbac/", - }, - { - title: "MySQL Guide", - slug: "/database-access/guides/mysql/", - }, - { - title: "Postgres Guide", - slug: "/database-access/guides/postgres/", - }, - ], - }, - ]; + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access RBAC", + slug: "/database-access/guides/rbac/rbac/", + entries: [ + { + title: "Get Started with DB RBAC", + slug: "/database-access/guides/rbac/get-started/", + }, + ], + }, + { + title: "MySQL Guide", + slug: "/database-access/guides/mysql/", + }, + { + title: "Postgres Guide", + slug: "/database-access/guides/postgres/", + }, + ], + }, + ]; - const vol = Volume.fromJSON(files); - const fs = createFsFromVolume(vol); - const actual = generateNavPaths(fs, "/docs/pages/database-access"); - assert.equal(actual, expected); - } -); + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + const actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); +}); Suite( "allows category pages in the same directory as the associated subdirectory", @@ -367,6 +370,12 @@ title: Get Started with DB RBAC { title: "Database Access RBAC", slug: "/database-access/guides/rbac/", + entries: [ + { + title: "Get Started with DB RBAC", + slug: "/database-access/guides/rbac/get-started/", + }, + ], }, { title: "MySQL Guide", @@ -387,4 +396,42 @@ title: Get Started with DB RBAC } ); +Suite("generates four levels of the sidebar", () => { + const files = { + "/docs/pages/database-access/guides/guides.mdx": `--- +title: Database Access Guides +---`, + "/docs/pages/database-access/guides/deployment/kubernetes.mdx": `--- +title: Database Access Kubernetes Deployment +---`, + "/docs/pages/database-access/guides/deployment/deployment.mdx": `--- +title: Database Access Deployment Guides +---`, + }; + + const expected = [ + { + title: "Database Access Guides", + slug: "/database-access/guides/guides/", + entries: [ + { + title: "Database Access Deployment Guides", + slug: "/database-access/guides/deployment/deployment/", + entries: [ + { + title: "Database Access Kubernetes Deployment", + slug: "/database-access/guides/deployment/kubernetes/", + }, + ], + }, + ], + }, + ]; + + const vol = Volume.fromJSON(files); + const fs = createFsFromVolume(vol); + let actual = generateNavPaths(fs, "/docs/pages/database-access"); + assert.equal(actual, expected); +}); + Suite.run();