Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow four levels of sidebar entries #486

Merged
merged 1 commit into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
15 changes: 13 additions & 2 deletions layouts/DocsPage/Navigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
49 changes: 49 additions & 0 deletions layouts/DocsPage/Navigation.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DocNavigation
data={data as Array<NavigationCategory>}
section={true}
currentVersion="16.x"
currentPathGetter={() => {
return "/enroll-resources/machine-id/deployment/aws/";
}}
></DocNavigation>
);
};

const meta: Meta<typeof DocNavigation> = {
title: "layouts/DocNavigation",
component: NavigationFourLevels,
};
export default meta;
34 changes: 29 additions & 5 deletions layouts/DocsPage/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,38 @@ const SCOPE_DICTIONARY: Record<string, ScopeType> = {
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 (
<>
{!!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.startsWith(entry.slug));

return (
<li key={entry.slug}>
<Link
className={cn(
styles.link,
styles[`link-${level}`],
active && styles.active,
selected && styles.selected
)}
Expand All @@ -57,11 +67,13 @@ const DocsNavigationItems = ({
<Icon size="sm" name="ellipsis" className={styles.ellipsis} />
)}
</Link>
{!!entry.entries?.length && (
{!!entry.entries?.length && level <= maxLevel && (
<ul className={cn(styles.submenu, active && styles.opened)}>
<DocsNavigationItems
entries={entry.entries}
onClick={onClick}
currentPath={currentPath}
level={level + 1}
/>
</ul>
)}
Expand All @@ -77,6 +89,7 @@ interface DocNavigationCategoryProps extends NavigationCategory {
opened: boolean;
onToggleOpened: (value: number) => void;
onClick: () => void;
currentPath: string;
}

const DocNavigationCategory = ({
Expand All @@ -87,6 +100,7 @@ const DocNavigationCategory = ({
icon,
title,
entries,
currentPath,
}: DocNavigationCategoryProps) => {
const toggleOpened = useCallback(
() => onToggleOpened(opened ? null : id),
Expand All @@ -105,7 +119,11 @@ const DocNavigationCategory = ({
</HeadlessButton>
{opened && (
<ul className={styles["category-links"]}>
<DocsNavigationItems entries={entries} onClick={onClick} />
<DocsNavigationItems
entries={entries}
onClick={onClick}
currentPath={currentPath}
/>
</ul>
)}
</>
Expand Down Expand Up @@ -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<number>(
getCurrentCategoryIndex(data, route)
Expand Down Expand Up @@ -171,6 +194,7 @@ const DocNavigation = ({
opened={index === openedId}
onToggleOpened={setOpenedId}
onClick={toggleMenu}
currentPath={route}
{...props}
/>
</li>
Expand Down
69 changes: 26 additions & 43 deletions server/pages-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const getPageInfo = <T = MDXPageFrontmatter>(
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);
Expand Down Expand Up @@ -108,23 +110,36 @@ 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);
if (info.isDirectory()) {
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);
Expand All @@ -145,56 +160,24 @@ 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 = {
title: title,
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);
};
2 changes: 1 addition & 1 deletion server/remark-toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading