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

Markdown-driven static pages: building the framework #5601

Merged
merged 22 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 21 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
9 changes: 9 additions & 0 deletions frontend-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@
"react-helmet": "^6.1.0",
"react-idle-timer": "^4.6.4",
"react-loader-spinner": "^5.1.3",
"react-markdown": "^8.0.3",
"react-router-dom": "^5.2.0",
"react-scroll-sync": "^0.9.0",
"react-toastify": "^8.2.0",
"rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.1",
"remark-toc": "^8.0.1",
"rest-hooks": "^6.1.7",
"rimraf": "^3.0.2",
"uswds": "^2.13.0",
Expand Down Expand Up @@ -64,6 +68,11 @@
"yarn:show-outdated-packages": "yarn outdated",
"run-build-dir": "yarn build:localdev:csp && yarn global add serve && serve -s build"
},
"jest": {
"transformIgnorePatterns": [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big ugly line of config to get jest and react-markdown to play nice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ouch! wonder if there's any way to point to this thread for further explanation remarkjs/react-markdown#635 (comment)

"[/\\\\]node_modules[/\\\\](?!(react-markdown|vfile|vfile-message|markdown-table|unist-.*|unified|bail|is-plain-obj|trough|remark-.*|rehype-.*|html-void-elements|hast-util-.*|zwitch|hast-to-hyperscript|hastscript|web-namespaces|mdast-util-.*|escape-string-regexp|micromark.*|decode-named-character-reference|character-entities|property-information|hast-util-whitespace|space-separated-tokens|comma-separated-tokens|pretty-bytes|ccount|mdast-util-gfm|gemoji)).+\\.(js|jsx|mjs|cjs|ts|tsx)$"
]
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"extends": [
Expand Down
13 changes: 12 additions & 1 deletion frontend-react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { SessionStorageContext } from "./contexts/SessionStorageContext";
import { AdminOrgNew } from "./pages/admin/AdminOrgNew";
import { DAPHeader } from "./components/header/DAPHeader";
import ValueSetsIndex from "./pages/admin/value-set-editor/ValueSetsIndex";
import BuiltForYouIndex from "./pages/built-for-you/BuiltForYouIndex";
import InternalUserGuides from "./pages/admin/InternalUserGuides";

const OKTA_AUTH = new OktaAuth(oktaAuthConfig);

Expand Down Expand Up @@ -132,6 +134,10 @@ const App = () => {
path="/getting-started/testing-facilities"
component={GettingStartedTestingFacilities}
/>
<Route
path="/built-for-you"
component={BuiltForYouIndex}
/>
<AuthorizedRoute
path="/daily-data"
authorize={PERMISSIONS.RECEIVER}
Expand Down Expand Up @@ -185,12 +191,17 @@ const App = () => {
authorize={PERMISSIONS.PRIME_ADMIN}
component={NewSetting}
/>
<AuthorizedRoute
path="/admin/guides"
authorize={PERMISSIONS.PRIME_ADMIN}
component={InternalUserGuides}
/>
<SecureRoute
path="/report-details"
component={Details}
/>
<SecureRoute
path="/features"
path="/admin/features"
component={FeatureFlagUIComponent}
/>
<SecureRoute
Expand Down
12 changes: 12 additions & 0 deletions frontend-react/src/components/Markdown/DirectoryAsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import testMd from "../../content/markdown-test.md";
import { renderWithRouter } from "../../utils/CustomRenderUtils";

import { MarkdownDirectory } from "./MarkdownDirectory";
import DirectoryAsPage from "./DirectoryAsPage";

describe("DirectoryAsPage", () => {
const testDir = new MarkdownDirectory("Test Dir", "test-dir", [testMd]);
test("Renders without error", () => {
renderWithRouter(<DirectoryAsPage directory={testDir} />);
});
});
17 changes: 17 additions & 0 deletions frontend-react/src/components/Markdown/DirectoryAsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* Renders all files on a page for a directory */
import { MarkdownDirectory } from "./MarkdownDirectory";
import { MarkdownContent } from "./MarkdownContent";

const DirectoryAsPage = ({ directory }: { directory: MarkdownDirectory }) => {
return (
<div>
{directory.files.map((file, idx) => (
/* Because file != typeof string but this is what our spike showed us works. */
// @ts-ignore
<MarkdownContent key={idx} markdownUrl={file} />
))}
</div>
);
};

export default DirectoryAsPage;
22 changes: 22 additions & 0 deletions frontend-react/src/components/Markdown/GeneratedSideNav.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { screen } from "@testing-library/react";

import { renderWithRouter } from "../../utils/CustomRenderUtils";

import GeneratedSideNav from "./GeneratedSideNav";
import { MarkdownDirectory } from "./MarkdownDirectory";

const TEST_DIRS = [
new MarkdownDirectory("Test Dir", "/test-dir", []),
new MarkdownDirectory("Another Dir", "/another-dir", []),
];

test("GeneratedSideNav", () => {
renderWithRouter(<GeneratedSideNav directories={TEST_DIRS} />);
expect(screen.getByText("Test Dir")).toBeInTheDocument();
expect(screen.getByText("Test Dir")).toHaveAttribute("href", "/test-dir");
expect(screen.getByText("Another Dir")).toBeInTheDocument();
expect(screen.getByText("Another Dir")).toHaveAttribute(
"href",
"/another-dir"
);
});
23 changes: 23 additions & 0 deletions frontend-react/src/components/Markdown/GeneratedSideNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NavLink } from "react-router-dom";
import { SideNav } from "@trussworks/react-uswds";

import { MarkdownDirectory } from "./MarkdownDirectory";

export const GeneratedSideNav = ({
directories,
}: {
directories: MarkdownDirectory[];
}) => {
const navItems = directories.map((dir) => (
<NavLink
to={dir.slug}
activeClassName="usa-current"
className="usa-nav__link"
>
{dir.title}
</NavLink>
));
return <SideNav items={navItems} />;
};

export default GeneratedSideNav;
43 changes: 43 additions & 0 deletions frontend-react/src/components/Markdown/MarkdownContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown, { Options } from "react-markdown";
import rehypeSlug from "rehype-slug";
import remarkGfm from "remark-gfm";
import remarkToc from "remark-toc";

const baseOptions: Partial<Options> = {
remarkPlugins: [
// Use GitHub-flavored markdown
remarkGfm,
// Generate a table of contents
[remarkToc, { tight: true }],
],
rehypePlugins: [
// Add ids to headings so the table of contents can link to each section
rehypeSlug,
],
};

type MarkdownContentProps = {
// Relative URL of the webpack-bundled markdown file. This value can be determined by importing
// the file as long as webpack is not configured to load the contents of the file, which is the
// state that our version of Create React App is in.
markdownUrl: string;
};

export const MarkdownContent: React.FC<MarkdownContentProps> = ({
markdownUrl,
}) => {
const [markdownContent, setMarkdownContent] = useState("");

// Fetch the contents of the markdown file.
// See: https://stackoverflow.com/questions/65395125/how-to-load-an-md-file-on-build-when-using-create-react-app-and-typescript
useEffect(() => {
fetch(markdownUrl)
.then((response) => response.text())
.then((text) => {
setMarkdownContent(text);
});
}, [markdownUrl]);

return <ReactMarkdown {...baseOptions} children={markdownContent} />;
};
11 changes: 11 additions & 0 deletions frontend-react/src/components/Markdown/MarkdownDirectory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MarkdownDirectory } from "./MarkdownDirectory";

describe("MarkdownDirectory", () => {
const testDir = new MarkdownDirectory("Test Dir", "/test-dir", []);

test("renders with params", () => {
expect(testDir.title).toEqual("Test Dir");
expect(testDir.slug).toEqual("/test-dir");
expect(testDir.files).toEqual([]);
});
});
22 changes: 22 additions & 0 deletions frontend-react/src/components/Markdown/MarkdownDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Used to instantiate a set of static pages, like BuiltForYouIndex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks more like a utility class than a component. Maybe it belongs in a utils file somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on a mission to refactor out the utils files. They tend to become a junk drawer for relatively similar functions a lot of times. That said, I do think it should be either wrapped into a component file or put elsewhere. I think this could easily be merged into the StaticPageFromDirectories code since these are the directories it uses to generate the content. Makes sense to me as a module.

* or HowItWorks */
import * as module from "module";

export interface MarkdownPageProps {
directories: MarkdownDirectory[];
}

/* Used to create objects that hold pointers to markdown directories and the
* info needed to query them. This is because we cannot access the filesystem
* at runtime */
export class MarkdownDirectory {
title: string;
slug: string;
files: module[];

constructor(title: string, slug: string, files: module[]) {
this.title = title;
this.slug = slug;
this.files = files;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { match } from "react-router-dom";
import { screen } from "@testing-library/react";

import testMd from "../../content/markdown-test.md";
import { renderWithRouter } from "../../utils/CustomRenderUtils";

import { MarkdownDirectory } from "./MarkdownDirectory";
import StaticPageFromDirectories from "./StaticPageFromDirectories";

const testDirectories = [
new MarkdownDirectory("Test Dir", "test-dir", [testMd]),
];

jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useRouteMatch: () => ({ path: "/test" } as match<{ path: string }>),
}));

describe("StaticPageFromDirectories", () => {
test("Renders without error", () => {
renderWithRouter(
<StaticPageFromDirectories directories={testDirectories} />
);
});
test("Renders without side-nav", () => {
renderWithRouter(
<StaticPageFromDirectories directories={testDirectories} />
);
const nav = screen.getByText("Test Dir");
expect(nav).toBeInTheDocument();
expect(nav).toHaveAttribute("href", "/test-dir");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Route, Switch, useRouteMatch } from "react-router-dom";

import { CODES, ErrorPage } from "../../pages/error/ErrorPage";

import { MarkdownDirectory } from "./MarkdownDirectory";
import GeneratedSideNav from "./GeneratedSideNav";
import DirectoryAsPage from "./DirectoryAsPage";

const StaticPageFromDirectories = ({
directories,
}: {
directories: MarkdownDirectory[];
}) => {
const { path } = useRouteMatch();

return (
<section className="grid-container tablet:margin-top-6 margin-bottom-5">
<div className="grid-row grid-gap">
<section className="tablet:grid-col-4 margin-bottom-6">
<GeneratedSideNav directories={directories} />
</section>
<section className="tablet:grid-col-8 usa-prose rs-documentation">
<Switch>
{/* SubRouter for /built-for-you */}
{directories.map((dir) => (
<Route
key={`${dir.slug}-route`}
path={`${path}/${dir.slug}`}
render={() => (
<DirectoryAsPage
key={`${dir.slug}-dir-as-page`}
directory={dir}
/>
)}
/>
))}
{/* Handles any undefined route */}
<Route
render={() => (
<ErrorPage code={CODES.NOT_FOUND_404} />
)}
/>
</Switch>
</section>
</div>
</section>
);
};

export default StaticPageFromDirectories;
57 changes: 0 additions & 57 deletions frontend-react/src/components/header/AdminDropdownNav.tsx

This file was deleted.

Loading