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.stories.tsx b/layouts/DocsPage/Navigation.stories.tsx
new file mode 100644
index 0000000000..1172bfe49b
--- /dev/null
+++ b/layouts/DocsPage/Navigation.stories.tsx
@@ -0,0 +1,71 @@
+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 { TabItem } from "./TabItem";
+import { Tabs } from "./Tabs";
+
+export const NavigationFourLevels = () => {
+ const data = [
+ {
+ icon: "wrench",
+ 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 (
+ {
+ return "/enroll-resources/machine-id/deployment/aws/";
+ }}
+ >
+ );
+};
+
+const meta: Meta = {
+ title: "layouts/DocNavigation",
+ component: NavigationFourLevels,
+};
+export default meta;
+type Story = StoryObj;
+
+// export const InitialStateTab: Story = {
+// play: async ({ canvasElement, step }) => {
+// const canvas = within(canvasElement);
+// await step("Should be the text for the selected option", async () => {
+// expect(
+// await canvas.findByText(
+// "Instructions for installing release using shell commands."
+// )
+// ).toBeTruthy();
+// });
+// await step(
+// "Selected option should be disabled for re-selection",
+// async () => {
+// expect(canvas.getByText("Shell")).toBeDisabled();
+// }
+// );
+// },
+// };
diff --git a/layouts/DocsPage/Navigation.tsx b/layouts/DocsPage/Navigation.tsx
index 07ad63b558..6d7a9f835d 100644
--- a/layouts/DocsPage/Navigation.tsx
+++ b/layouts/DocsPage/Navigation.tsx
@@ -24,22 +24,27 @@ const SCOPE_DICTIONARY: Record = {
interface DocsNavigationItemsProps {
entries: NavigationItem[];
onClick: () => void;
+ currentPath: string;
}
const DocsNavigationItems = ({
entries,
onClick,
+ currentPath,
}: DocsNavigationItemsProps) => {
- const docPath = useCurrentHref().split(SCOPELESS_HREF_REGEX)[0];
+ const docPath = currentPath.split(SCOPELESS_HREF_REGEX)[0];
const { getVersionAgnosticRoute } = useVersionAgnosticPages();
+ console.log("entries:", entries);
+
return (
<>
{!!entries.length &&
entries.map((entry) => {
const selected = entry.slug === docPath;
const active =
- selected || entry.entries?.some((entry) => entry.slug === docPath);
+ selected ||
+ entry.entries?.some((entry) => docPath.includes(entry.slug));
return (
@@ -62,6 +67,7 @@ const DocsNavigationItems = ({
)}
@@ -77,6 +83,7 @@ interface DocNavigationCategoryProps extends NavigationCategory {
opened: boolean;
onToggleOpened: (value: number) => void;
onClick: () => void;
+ currentPath: string;
}
const DocNavigationCategory = ({
@@ -87,6 +94,7 @@ const DocNavigationCategory = ({
icon,
title,
entries,
+ currentPath,
}: DocNavigationCategoryProps) => {
const toggleOpened = useCallback(
() => onToggleOpened(opened ? null : id),
@@ -105,7 +113,11 @@ const DocNavigationCategory = ({
{opened && (
)}
>
@@ -134,14 +146,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 +188,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/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();