Skip to content

Commit

Permalink
docs(core): add title and collapsible menu to nx.dev docs viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Apr 27, 2021
1 parent 85ceb3c commit 3011017
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 55 deletions.
46 changes: 26 additions & 20 deletions nx-dev/data-access-documents/src/lib/documents.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import matter from 'gray-matter';
import * as marked from 'marked';
import { archiveRootPath, previewRootPath } from './documents.utils';
import {
archiveRootPath,
extractTitle,
previewRootPath,
} from './documents.utils';
import {
VersionMetadata,
DocumentData,
Expand All @@ -16,7 +20,14 @@ export function getDocument(
): DocumentData {
const segments = Array.isArray(_segments) ? [..._segments] : [_segments];
const docPath = getFilePath(version, segments);
const file = matter(readFileSync(docPath, 'utf8'));
const originalContent = readFileSync(docPath, 'utf8');
const file = matter(originalContent);

// Set default title if not provided in front-matter section.
if (!file.data.title) {
file.data.title =
extractTitle(originalContent) ?? segments[segments.length - 1];
}

return {
filePath: docPath,
Expand All @@ -35,16 +46,12 @@ export function getFilePath(version: string, segments: string[]): string {
items = found.itemList;
} else {
throw new Error(
`Cannot find document matching segments: ${segments.join(',')}`
`Cannot find document matching segments: ${JSON.stringify(segments)}`
);
}
}
if (!found.file) {
throw new Error(
`Cannot find document matching segments: ${segments.join(',')}`
);
}
return join(getDocumentsRoot(version), `${found.file}.md`);
const file = found.file ?? segments.join('/');
return join(getDocumentsRoot(version), `${file}.md`);
}

const documentsCache = new Map<string, DocumentMetadata[]>();
Expand All @@ -65,25 +72,24 @@ export function getStaticDocumentPaths(version: string) {
const paths = [];

function recur(curr, acc) {
if (curr.file) {
paths.push({
params: {
version,
flavor: acc[0],
segments: acc.slice(2).concat(curr.id),
},
});
return;
}
if (curr.itemList) {
curr.itemList.forEach((ii) => {
recur(ii, [...acc, curr.id]);
});
return;
}

paths.push({
params: {
version,
flavor: acc[0],
segments: acc.slice(1).concat(curr.id),
},
});
}

getDocuments(version).forEach((item) => {
recur(item, [item.id]);
recur(item, []);
});

return paths;
Expand Down
27 changes: 27 additions & 0 deletions nx-dev/data-access-documents/src/lib/documents.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { extractTitle } from './documents.utils';

describe('extractTitle', () => {
it('should return header if it exists', () => {
const content = `
# This is the title
Hello, this is just a test document...
`;

const result = extractTitle(content);

expect(result).toEqual('This is the title');
});

it('should return null if title cannot be found', () => {
const content = `
## Secondary header
Hello, this is just a test document...
`;

const result = extractTitle(content);

expect(result).toEqual(null);
});
});
6 changes: 6 additions & 0 deletions nx-dev/data-access-documents/src/lib/documents.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ import { appRootPath } from '@nrwl/workspace/src/utilities/app-root';

export const archiveRootPath = join(appRootPath, 'nx-dev/archive');
export const previewRootPath = join(appRootPath, 'docs');

export function extractTitle(markdownContent: string): string | null {
return (
/^\s*#\s+(?<title>.+)[\n.]+/.exec(markdownContent)?.groups.title ?? null
);
}
4 changes: 3 additions & 1 deletion nx-dev/feature-doc-viewer/src/lib/doc-viewer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ describe('DocViewer', () => {
it('should render successfully', () => {
const { baseElement } = render(
<DocViewer
content=""
version="1.0.0"
flavor="react"
document={{ content: '', data: {}, excerpt: '', filePath: '' }}
menu={{ version: 'preview', flavor: 'react', sections: [] }}
toc=""
/>
Expand Down
45 changes: 29 additions & 16 deletions nx-dev/feature-doc-viewer/src/lib/doc-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,51 @@
import React from 'react';
import Content from './content';
import Head from 'next/head';
import { Menu } from '@nrwl/nx-dev/data-access-menu';
import { DocumentData } from '@nrwl/nx-dev/data-access-documents';

import Sidebar from './sidebar';
import Toc from './toc';
import { Menu } from '@nrwl/nx-dev/data-access-menu';

export interface DocumentationFeatureDocViewerProps {
version: string;
flavor: string;
menu: Menu;
content: any;
document: DocumentData;
toc: any;
}

export function DocViewer({
content,
document,
menu,
flavor,
}: DocumentationFeatureDocViewerProps) {
return (
<div className="w-full max-w-screen-xl max-w-8xl mx-auto">
<div className="lg:flex">
<Sidebar menu={menu} />
<div
id="content-wrapper"
className="min-w-0 w-full flex-auto lg:static lg:max-h-full lg:overflow-visible"
>
<div className="w-full flex">
<Content data={content} />
<div className="hidden xl:text-sm xl:block flex-none w-64 pl-8 mr-8">
<div className="flex flex-col justify-between overflow-y-auto sticky max-h-(screen-18) pt-10 pb-6 top-18">
<Toc />
<>
<Head>
<title>
{document.data.title} | Nx {flavor} documentation
</title>
</Head>
<div className="w-full max-w-screen-xl max-w-8xl mx-auto">
<div className="lg:flex">
<Sidebar menu={menu} />
<div
id="content-wrapper"
className="min-w-0 w-full flex-auto lg:static lg:max-h-full lg:overflow-visible"
>
<div className="w-full flex">
<Content data={document.content} />
<div className="hidden xl:text-sm xl:block flex-none w-64 pl-8 mr-8">
<div className="flex flex-col justify-between overflow-y-auto sticky max-h-(screen-18) pt-10 pb-6 top-18">
<Toc />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}

Expand Down
50 changes: 46 additions & 4 deletions nx-dev/feature-doc-viewer/src/lib/sidebar.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
import React from 'react';
import { screen } from '@testing-library/dom';
import { render } from '@testing-library/react';

import Sidebar from './sidebar';

describe('Sidebar', () => {
it('should render successfully', () => {
const { baseElement } = render(
<Sidebar menu={{ version: 'preview', flavor: 'react', sections: [] }} />
it('should render sections', () => {
render(
<Sidebar
menu={{
version: 'preview',
flavor: 'react',
sections: [
{
id: 'basic',
name: 'Basic',
hideSectionHeader: true,
itemList: [
{
id: 'getting-started',
name: 'getting started',
itemList: [
{ id: 'a', name: 'A', path: '/a' },
{ id: 'b', name: 'B', path: '/b' },
{ id: 'c', name: 'C', path: '/c' },
],
},
],
},
{
id: 'api',
name: 'API',
itemList: [
{
id: 'overview',
name: 'overview',
itemList: [
{ id: 'd', name: 'D', path: '/d' },
{ id: 'e', name: 'E', path: '/e' },
],
},
],
},
],
}}
/>
);
expect(baseElement).toBeTruthy();

expect(() => screen.getByTestId('section-h4:basic')).toThrow(
/Unable to find/
);
expect(screen.getByTestId('section-h4:api')).toBeTruthy();
});
});
62 changes: 51 additions & 11 deletions nx-dev/feature-doc-viewer/src/lib/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useState } from 'react';
import cx from 'classnames';
import Link from 'next/link';
import { Menu, MenuItem, MenuSection } from '@nrwl/nx-dev/data-access-menu';
Expand Down Expand Up @@ -35,9 +35,14 @@ export function Sidebar({ menu }: SidebarProps) {
function SidebarSection({ section }: { section: MenuSection }) {
return (
<>
<h4 className="mt-6 mb-4 pb-2 border-b border-gray-50 border-solid">
{section.name}
</h4>
{section.hideSectionHeader ? null : (
<h4
data-testid={`section-h4:${section.id}`}
className="mt-6 mb-4 pb-2 text-m border-b border-gray-50 border-solid"
>
{section.name}
</h4>
)}
<ul>
<li className="mt-8">
{section.itemList.map((item) => (
Expand All @@ -51,29 +56,42 @@ function SidebarSection({ section }: { section: MenuSection }) {

function SidebarSectionItems({ item }: { item: MenuItem }) {
const router = useRouter();
const [collapsed, setCollapsed] = useState(!item.disableCollapsible);

const handleCollapseToggle = useCallback(() => {
if (!item.disableCollapsible) {
setCollapsed(!collapsed);
}
}, [collapsed, setCollapsed, item]);

return (
<>
<h5
data-testid={`section-h5:${item.id}`}
className={cx(
'mt-3 mb-3 lg:mb-3',
'uppercase tracking-wide font-semibold text-sm lg:text-xs text-gray-900'
'flex my-2 py-1',
'uppercase tracking-wide font-semibold text-sm lg:text-xs text-gray-900',
item.disableCollapsible ? 'cursor-text' : 'cursor-pointer'
)}
onClick={handleCollapseToggle}
>
{item.name}
{item.disableCollapsible ? null : (
<CollapsibleIcon isCollapsed={collapsed} />
)}
</h5>
<ul>
<ul className={collapsed ? 'hidden' : ''}>
{item.itemList.map((item) => {
const isActiveLink = item.path === router.asPath;
const isActiveLink = item.path === router?.asPath;
return (
<li key={item.id}>
<li key={item.id} data-testid={`section-li:${item.id}`}>
<Link href={item.path}>
<a
className={cx(
'py-2 transition-colors duration-200 relative block',
'p-2 transition-colors duration-200 relative block',
isActiveLink
? 'hover:text-blue-900 text-blue-500'
: 'hover:text-gray-900 text-gray-700'
: 'hover:text-gray-900 text-gray-500'
)}
>
{isActiveLink ? (
Expand All @@ -90,4 +108,26 @@ function SidebarSectionItems({ item }: { item: MenuItem }) {
);
}

function CollapsibleIcon({ isCollapsed }: { isCollapsed: boolean }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={cx(
'transition-all h-3.5 w-3.5 text-gray-500',
!isCollapsed && 'transform rotate-90'
)}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d={'M9 5l7 7-7 7'}
/>
</svg>
);
}

export default Sidebar;
Loading

0 comments on commit 3011017

Please sign in to comment.