@@ -178,7 +178,9 @@ | |||
], | |||
"moduleNameMapper": { | |||
"^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/config/jest/FileStub.js", | |||
"^.+\\.css$": "<rootDir>/config/jest/CSSStub.js" | |||
"^.+\\.css$": "<rootDir>/config/jest/CSSStub.js", | |||
"^Docs/@types/types$": "<rootDir>/../sonar-docs/src/@types/types.d.ts", | |||
"^Docs/(.*)": "<rootDir>/../sonar-docs/src/$1" | |||
}, | |||
"setupFiles": [ | |||
"<rootDir>/config/polyfills.js", |
@@ -22,13 +22,29 @@ | |||
padding-left: 0; | |||
} | |||
.list-group-item { | |||
.list-group-item, | |||
button.list-group-item { | |||
position: relative; | |||
z-index: var(--normalZIndex); | |||
display: block; | |||
margin-bottom: -1px; | |||
padding: 5px 10px; | |||
border: 1px solid transparent; | |||
width: 100%; | |||
box-sizing: border-box; | |||
text-align: left; | |||
} | |||
.list-group-item.depth-1 { | |||
padding-left: 31px; | |||
} | |||
.list-group-item.depth-2 { | |||
padding-left: 51px; | |||
} | |||
.list-group-item.depth-3 { | |||
padding-left: 71px; | |||
} | |||
.list-group-item:last-child { |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { Link } from 'react-router'; | |||
import { DocNavigationItem } from 'Docs/@types/types'; | |||
import * as navigationTreeSonarQube from 'Docs/../static/SonarQubeNavigationTree.json'; | |||
import * as navigationTreeSonarCloud from 'Docs/../static/SonarCloudNavigationTree.json'; | |||
import Sidebar from './Sidebar'; | |||
@@ -31,7 +32,6 @@ import DocMarkdownBlock from '../../../components/docs/DocMarkdownBlock'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; | |||
import { DocsNavigationItem } from '../utils'; | |||
import '../styles.css'; | |||
interface Props { | |||
@@ -52,8 +52,8 @@ export default class App extends React.PureComponent<Props> { | |||
render() { | |||
const tree = isSonarCloud() | |||
? ((navigationTreeSonarCloud as any).default as DocsNavigationItem[]) | |||
: ((navigationTreeSonarQube as any).default as DocsNavigationItem[]); | |||
? ((navigationTreeSonarCloud as any).default as DocNavigationItem[]) | |||
: ((navigationTreeSonarQube as any).default as DocNavigationItem[]); | |||
const { splat = '' } = this.props.params; | |||
const page = this.pages.find(p => p.url === '/' + splat); | |||
const mainTitle = translate('documentation.page_title'); |
@@ -18,79 +18,71 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import MenuBlock from './MenuBlock'; | |||
import { MenuItem } from './MenuItem'; | |||
import { MenuExternalLink } from './MenuExternalLink'; | |||
import { DocNavigationItem } from 'Docs/@types/types'; | |||
import { | |||
DocumentationEntry, | |||
DocsNavigationBlock, | |||
getNodeFromUrl, | |||
isDocsNavigationBlock, | |||
isDocsNavigationExternalLink, | |||
DocsNavigationItem | |||
} from '../utils'; | |||
getOpenChainFromPath | |||
} from 'Docs/components/navTreeUtils'; | |||
import MenuBlock from './MenuBlock'; | |||
import { MenuItem } from './MenuItem'; | |||
import { MenuExternalLink } from './MenuExternalLink'; | |||
import { DocumentationEntry, getNodeFromUrl } from '../utils'; | |||
interface Props { | |||
navigation: DocsNavigationItem[]; | |||
navigation: DocNavigationItem[]; | |||
pages: DocumentationEntry[]; | |||
splat: string; | |||
} | |||
interface State { | |||
openBlockTitle: string; | |||
openChain: DocNavigationItem[]; | |||
} | |||
export default class Menu extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
openBlockTitle: this.getOpenBlockFromLocation(this.props.splat) | |||
openChain: getOpenChainFromPath(this.props.splat, this.props.navigation) | |||
}; | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (this.props.splat !== nextProps.splat) { | |||
this.setState({ openBlockTitle: this.getOpenBlockFromLocation(nextProps.splat) }); | |||
this.setState({ openChain: getOpenChainFromPath(nextProps.splat, nextProps.navigation) }); | |||
} | |||
} | |||
getOpenBlockFromLocation(splat: string) { | |||
const element = this.props.navigation.find( | |||
item => isDocsNavigationBlock(item) && item.children.some(child => '/' + splat === child) | |||
); | |||
return element ? (element as DocsNavigationBlock).title : ''; | |||
} | |||
toggleBlock = (title: string) => { | |||
this.setState(state => ({ openBlockTitle: state.openBlockTitle === title ? '' : title })); | |||
}; | |||
render() { | |||
return this.props.navigation.map(item => { | |||
if (isDocsNavigationBlock(item)) { | |||
return ( | |||
<MenuBlock | |||
block={item} | |||
key={item.title} | |||
onToggle={this.toggleBlock} | |||
open={this.state.openBlockTitle === item.title} | |||
pages={this.props.pages} | |||
splat={this.props.splat} | |||
title={item.title} | |||
/> | |||
); | |||
} | |||
if (isDocsNavigationExternalLink(item)) { | |||
return <MenuExternalLink key={item.title} title={item.title} url={item.url} />; | |||
} | |||
return ( | |||
<MenuItem | |||
indent={false} | |||
key={item} | |||
node={getNodeFromUrl(this.props.pages, item)} | |||
splat={this.props.splat} | |||
/> | |||
); | |||
}); | |||
const { openChain } = this.state; | |||
return ( | |||
<> | |||
{this.props.navigation.map(item => { | |||
if (isDocsNavigationBlock(item)) { | |||
return ( | |||
<MenuBlock | |||
block={item} | |||
key={item.title} | |||
openByDefault={openChain.includes(item)} | |||
openChain={openChain} | |||
pages={this.props.pages} | |||
splat={this.props.splat} | |||
title={item.title} | |||
/> | |||
); | |||
} | |||
if (isDocsNavigationExternalLink(item)) { | |||
return <MenuExternalLink key={item.title} title={item.title} url={item.url} />; | |||
} | |||
return ( | |||
<MenuItem | |||
key={item} | |||
node={getNodeFromUrl(this.props.pages, item)} | |||
splat={this.props.splat} | |||
/> | |||
); | |||
})} | |||
</> | |||
); | |||
} | |||
} |
@@ -18,40 +18,85 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { DocsNavigationBlock, DocNavigationItem } from 'Docs/@types/types'; | |||
import { isDocsNavigationBlock } from 'Docs/components/navTreeUtils'; | |||
import { MenuItem } from './MenuItem'; | |||
import { DocumentationEntry, DocsNavigationBlock, getNodeFromUrl } from '../utils'; | |||
import OpenCloseIcon from '../../../components/icons-components/OpenCloseIcon'; | |||
import { ButtonLink } from '../../../components/ui/buttons'; | |||
import { DocumentationEntry, getNodeFromUrl } from '../utils'; | |||
interface Props { | |||
block: DocsNavigationBlock; | |||
onToggle: (title: string) => void; | |||
open: boolean; | |||
depth?: number; | |||
openByDefault: boolean; | |||
openChain: DocNavigationItem[]; | |||
pages: DocumentationEntry[]; | |||
splat: string; | |||
title: string; | |||
} | |||
export default class MenuBlock extends React.PureComponent<Props> { | |||
handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
event.stopPropagation(); | |||
event.preventDefault(); | |||
this.props.onToggle(this.props.title); | |||
interface State { | |||
open: boolean; | |||
} | |||
export default class MenuBlock extends React.PureComponent<Props, State> { | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
open: props.openByDefault !== undefined ? props.openByDefault : false | |||
}; | |||
} | |||
handleClick = () => { | |||
this.setState(prevState => ({ | |||
open: !prevState.open | |||
})); | |||
}; | |||
renderMenuItems = (block: DocsNavigationBlock): React.ReactNode => { | |||
const { depth = 0, openChain, pages, splat } = this.props; | |||
return block.children.map(item => { | |||
if (typeof item === 'string') { | |||
return ( | |||
<MenuItem depth={depth + 1} key={item} node={getNodeFromUrl(pages, item)} splat={splat} /> | |||
); | |||
} else if (isDocsNavigationBlock(item)) { | |||
return ( | |||
<MenuBlock | |||
block={item} | |||
depth={depth + 1} | |||
key={item.title} | |||
openByDefault={openChain.includes(item)} | |||
openChain={openChain} | |||
pages={pages} | |||
splat={splat} | |||
title={item.title} | |||
/> | |||
); | |||
} else { | |||
return null; | |||
} | |||
}); | |||
}; | |||
render() { | |||
const { open, block, pages, title, splat } = this.props; | |||
const { block, depth = 0, title } = this.props; | |||
const { open } = this.state; | |||
const maxDepth = Math.min(depth, 3); | |||
return ( | |||
<> | |||
<a className="list-group-item" href="#" onClick={this.handleClick}> | |||
<ButtonLink | |||
className={classNames('list-group-item', { [`depth-${maxDepth}`]: depth > 0 })} | |||
onClick={this.handleClick}> | |||
<h3 className="list-group-item-heading"> | |||
<OpenCloseIcon className="little-spacer-right" open={this.props.open} /> | |||
<OpenCloseIcon className="little-spacer-right" open={open} /> | |||
{title} | |||
</h3> | |||
</a> | |||
{open && | |||
block.children.map(item => ( | |||
<MenuItem indent={true} key={item} node={getNodeFromUrl(pages, item)} splat={splat} /> | |||
))} | |||
</ButtonLink> | |||
{open && this.renderMenuItems(block)} | |||
</> | |||
); | |||
} |
@@ -20,25 +20,26 @@ | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { Link } from 'react-router'; | |||
import { testPathAgainstUrl } from 'Docs/components/navTreeUtils'; | |||
import { DocumentationEntry } from '../utils'; | |||
interface Props { | |||
indent: boolean; | |||
depth?: number; | |||
node: DocumentationEntry | undefined; | |||
splat: string; | |||
} | |||
export function MenuItem({ indent, node, splat }: Props) { | |||
export function MenuItem({ depth = 0, node, splat }: Props) { | |||
if (!node) { | |||
return null; | |||
} | |||
const active = node.url === '/' + splat; | |||
const active = testPathAgainstUrl(node.url, splat); | |||
const maxDepth = Math.min(depth, 3); | |||
return ( | |||
<Link | |||
className={classNames('list-group-item', { active })} | |||
className={classNames('list-group-item', { active, [`depth-${maxDepth}`]: depth > 0 })} | |||
key={node.url} | |||
style={{ paddingLeft: indent ? 31 : 10 }} | |||
to={'/documentation' + node.url}> | |||
<h3 className="list-group-item-heading">{node.navTitle || node.title}</h3> | |||
</Link> |
@@ -20,11 +20,13 @@ | |||
import * as React from 'react'; | |||
import lunr, { LunrBuilder, LunrIndex, LunrToken } from 'lunr'; | |||
import { sortBy } from 'lodash'; | |||
import { getUrlsList } from 'Docs/components/navTreeUtils'; | |||
import { DocNavigationItem } from 'Docs/@types/types'; | |||
import SearchResultEntry, { SearchResult } from './SearchResultEntry'; | |||
import { DocumentationEntry, getUrlsList, DocsNavigationItem } from '../utils'; | |||
import { DocumentationEntry } from '../utils'; | |||
interface Props { | |||
navigation: DocsNavigationItem[]; | |||
navigation: DocNavigationItem[]; | |||
pages: DocumentationEntry[]; | |||
query: string; | |||
splat: string; |
@@ -18,13 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { DocNavigationItem } from 'Docs/@types/types'; | |||
import Menu from './Menu'; | |||
import SearchResults from './SearchResults'; | |||
import { DocumentationEntry, DocsNavigationItem } from '../utils'; | |||
import { DocumentationEntry } from '../utils'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
interface Props { | |||
navigation: DocsNavigationItem[]; | |||
navigation: DocNavigationItem[]; | |||
pages: DocumentationEntry[]; | |||
splat: string; | |||
} |
@@ -19,8 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { addSideBarClass, removeSideBarClass } from '../../../../helpers/pages'; | |||
import App from '../App'; | |||
import { addSideBarClass, removeSideBarClass } from '../../../../helpers/pages'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
jest.mock('../../../../components/common/ScreenPositionHelper', () => ({ | |||
default: class ScreenPositionHelper extends React.Component<{ | |||
@@ -33,12 +34,29 @@ jest.mock('../../../../components/common/ScreenPositionHelper', () => ({ | |||
} | |||
})); | |||
jest.mock('../../../../helpers/system', () => ({ | |||
isSonarCloud: jest.fn().mockReturnValue(false) | |||
})); | |||
jest.mock( | |||
'Docs/../static/SonarQubeNavigationTree.json', | |||
() => [ | |||
{ | |||
title: 'SonarQube', | |||
children: ['/lorem/ipsum/'] | |||
children: [ | |||
'/lorem/ipsum/', | |||
{ | |||
title: 'Child category', | |||
children: [ | |||
'/lorem/ipsum/dolor', | |||
{ | |||
title: 'Grandchild category', | |||
children: ['/lorem/ipsum/sit'] | |||
}, | |||
'/lorem/ipsum/amet' | |||
] | |||
} | |||
] | |||
} | |||
], | |||
{ virtual: true } | |||
@@ -49,7 +67,20 @@ jest.mock( | |||
() => [ | |||
{ | |||
title: 'SonarCloud', | |||
children: ['/lorem/ipsum/'] | |||
children: [ | |||
'/lorem/ipsum/', | |||
{ | |||
title: 'Child category', | |||
children: [ | |||
'/lorem/ipsum/dolor', | |||
{ | |||
title: 'Grandchild category', | |||
children: ['/lorem/ipsum/sit'] | |||
}, | |||
'/lorem/ipsum/amet' | |||
] | |||
} | |||
] | |||
} | |||
], | |||
{ virtual: true } | |||
@@ -67,7 +98,7 @@ jest.mock('../../pages', () => { | |||
}; | |||
}); | |||
it('should render correctly', () => { | |||
it('should render correctly for SonarQube', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -79,6 +110,11 @@ it('should render correctly', () => { | |||
expect(removeSideBarClass).toBeCalled(); | |||
}); | |||
it('should render correctly for SonarCloud', () => { | |||
(isSonarCloud as jest.Mock).mockReturnValue(true); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it("should show a 404 if the page doesn't exist", () => { | |||
const wrapper = shallowRender({ params: { splat: 'unknown' } }); | |||
expect(wrapper).toMatchSnapshot(); |
@@ -44,13 +44,15 @@ const pages = [ | |||
]; | |||
it('should render hierarchical menu', () => { | |||
expect( | |||
shallow( | |||
<Menu | |||
navigation={[{ title: 'Block', children: ['/lorem/index', '/lorem/origin'] }, 'foobar']} | |||
pages={pages} | |||
splat="lorem/origin" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
const wrapper = shallow( | |||
<Menu | |||
navigation={[{ title: 'Block', children: ['/lorem/index', '/lorem/origin'] }, 'foobar']} | |||
pages={pages} | |||
splat="lorem/origin" | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setProps({ splat: 'baz/bar' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -20,57 +20,70 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import MenuBlock from '../MenuBlock'; | |||
const block = { | |||
title: 'Foo', | |||
children: ['/bar/', '/baz/'] | |||
}; | |||
const pages = [ | |||
{ | |||
content: 'bar', | |||
relativeName: '/bar/', | |||
text: 'bar', | |||
title: 'Bar', | |||
navTitle: undefined, | |||
url: '/bar/' | |||
}, | |||
{ | |||
content: 'baz', | |||
relativeName: '/baz/', | |||
text: 'baz', | |||
title: 'baz', | |||
navTitle: 'baznav', | |||
url: '/baz/' | |||
} | |||
]; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render a closed menu block', () => { | |||
expect( | |||
shallow( | |||
<MenuBlock | |||
block={block} | |||
onToggle={jest.fn()} | |||
open={false} | |||
pages={pages} | |||
splat="/foobar/" | |||
title="Foobarbaz" | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render an opened menu block', () => { | |||
expect(shallowRender({ openByDefault: true })).toMatchSnapshot(); | |||
}); | |||
it('should not render a high depth differently than a depth of 3', () => { | |||
expect( | |||
shallow( | |||
<MenuBlock | |||
block={block} | |||
onToggle={jest.fn()} | |||
open={true} | |||
pages={pages} | |||
splat="/foo/" | |||
title="Foo" | |||
/> | |||
) | |||
shallowRender({ block: { title: 'Foo', children: ['/foo'] }, depth: 6 }) | |||
).toMatchSnapshot(); | |||
}); | |||
it('can be opened and closed', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper.state('open')).toBe(false); | |||
click(wrapper.find('ButtonLink')); | |||
expect(wrapper.state('open')).toBe(true); | |||
}); | |||
function shallowRender(props: Partial<MenuBlock['props']> = {}) { | |||
return shallow( | |||
<MenuBlock | |||
block={{ | |||
title: 'Foo', | |||
children: [ | |||
'/bar/', | |||
'/baz/', | |||
{ | |||
title: 'Baz', | |||
children: ['/baz/foo'] | |||
}, | |||
{ | |||
title: 'Bar', | |||
url: 'http://example.com' | |||
} | |||
] | |||
}} | |||
openByDefault={false} | |||
openChain={[]} | |||
pages={[ | |||
{ | |||
content: 'bar', | |||
relativeName: '/bar/', | |||
text: 'bar', | |||
title: 'Bar', | |||
navTitle: undefined, | |||
url: '/bar/' | |||
}, | |||
{ | |||
content: 'baz', | |||
relativeName: '/baz/', | |||
text: 'baz', | |||
title: 'baz', | |||
navTitle: 'baznav', | |||
url: '/baz/' | |||
} | |||
]} | |||
splat="/foo/" | |||
title="Foo" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { MenuItem } from '../MenuItem'; | |||
import { DocumentationEntry } from '../../utils'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render correctly if the current node matches the splat', () => { | |||
expect(shallowRender({ splat: 'bar' })).toMatchSnapshot(); | |||
}); | |||
it('should not render a high depth differently than a depth of 3', () => { | |||
expect(shallowRender({ depth: 6 })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props = {}) { | |||
return shallow(<MenuItem node={{ url: '/bar' } as DocumentationEntry} splat="foo" {...props} />); | |||
} |
@@ -1,6 +1,44 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
exports[`should render correctly for SonarCloud 1`] = ` | |||
<div | |||
className="layout-page" | |||
> | |||
<HelmetWrapper | |||
defer={true} | |||
encodeSpecialCharacters={true} | |||
title="Lorem | documentation.page_title" | |||
/> | |||
<ScreenPositionHelper | |||
className="layout-page-side-outer" | |||
> | |||
<Component /> | |||
</ScreenPositionHelper> | |||
<div | |||
className="layout-page-main" | |||
> | |||
<div | |||
className="layout-page-main-inner" | |||
> | |||
<div | |||
className="boxed-group" | |||
> | |||
<A11ySkipTarget | |||
anchor="documentation_main" | |||
/> | |||
<DocMarkdownBlock | |||
className="documentation-content cut-margins boxed-group-inner" | |||
content="Lorem ipsum dolor sit amet fredum" | |||
displayH1={true} | |||
stickyToc={true} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly for SonarQube 1`] = ` | |||
<div | |||
className="layout-page" | |||
> | |||
@@ -43,7 +81,7 @@ exports[`should render correctly 1`] = ` | |||
</div> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
exports[`should render correctly for SonarQube 2`] = ` | |||
<div | |||
className="layout-page-side" | |||
style={ |
@@ -1,7 +1,7 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render hierarchical menu 1`] = ` | |||
Array [ | |||
<Fragment> | |||
<MenuBlock | |||
block={ | |||
Object { | |||
@@ -13,8 +13,19 @@ Array [ | |||
} | |||
} | |||
key="Block" | |||
onToggle={[Function]} | |||
open={true} | |||
openByDefault={true} | |||
openChain={ | |||
Array [ | |||
Object { | |||
"children": Array [ | |||
"/lorem/index", | |||
"/lorem/origin", | |||
], | |||
"title": "Block", | |||
}, | |||
"/lorem/origin", | |||
] | |||
} | |||
pages={ | |||
Array [ | |||
Object { | |||
@@ -45,11 +56,63 @@ Array [ | |||
} | |||
splat="lorem/origin" | |||
title="Block" | |||
/>, | |||
/> | |||
<MenuItem | |||
indent={false} | |||
key="foobar" | |||
splat="lorem/origin" | |||
/>, | |||
] | |||
/> | |||
</Fragment> | |||
`; | |||
exports[`should render hierarchical menu 2`] = ` | |||
<Fragment> | |||
<MenuBlock | |||
block={ | |||
Object { | |||
"children": Array [ | |||
"/lorem/index", | |||
"/lorem/origin", | |||
], | |||
"title": "Block", | |||
} | |||
} | |||
key="Block" | |||
openByDefault={false} | |||
openChain={Array []} | |||
pages={ | |||
Array [ | |||
Object { | |||
"content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", | |||
"navTitle": undefined, | |||
"relativeName": "lorem/index", | |||
"text": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", | |||
"title": "Lorem Ipsum", | |||
"url": "/lorem/index", | |||
}, | |||
Object { | |||
"content": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", | |||
"navTitle": undefined, | |||
"relativeName": "lorem/origin", | |||
"text": "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words.", | |||
"title": "Where does it come from?", | |||
"url": "/lorem/origin", | |||
}, | |||
Object { | |||
"content": "Foobar is a universal variable understood to represent whatever is being discussed.", | |||
"navTitle": undefined, | |||
"relativeName": "foobar", | |||
"text": "Foobar is a universal variable understood to represent whatever is being discussed.", | |||
"title": "Where does Foobar come from?", | |||
"url": "/foobar", | |||
}, | |||
] | |||
} | |||
splat="baz/bar" | |||
title="Block" | |||
/> | |||
<MenuItem | |||
key="foobar" | |||
splat="baz/bar" | |||
/> | |||
</Fragment> | |||
`; |
@@ -1,10 +1,28 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should not render a high depth differently than a depth of 3 1`] = ` | |||
<Fragment> | |||
<ButtonLink | |||
className="list-group-item depth-3" | |||
onClick={[Function]} | |||
> | |||
<h3 | |||
className="list-group-item-heading" | |||
> | |||
<OpenCloseIcon | |||
className="little-spacer-right" | |||
open={false} | |||
/> | |||
Foo | |||
</h3> | |||
</ButtonLink> | |||
</Fragment> | |||
`; | |||
exports[`should render a closed menu block 1`] = ` | |||
<Fragment> | |||
<a | |||
<ButtonLink | |||
className="list-group-item" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<h3 | |||
@@ -14,17 +32,16 @@ exports[`should render a closed menu block 1`] = ` | |||
className="little-spacer-right" | |||
open={false} | |||
/> | |||
Foobarbaz | |||
Foo | |||
</h3> | |||
</a> | |||
</ButtonLink> | |||
</Fragment> | |||
`; | |||
exports[`should render an opened menu block 1`] = ` | |||
<Fragment> | |||
<a | |||
<ButtonLink | |||
className="list-group-item" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<h3 | |||
@@ -36,9 +53,9 @@ exports[`should render an opened menu block 1`] = ` | |||
/> | |||
Foo | |||
</h3> | |||
</a> | |||
</ButtonLink> | |||
<MenuItem | |||
indent={true} | |||
depth={1} | |||
key="/bar/" | |||
node={ | |||
Object { | |||
@@ -53,7 +70,7 @@ exports[`should render an opened menu block 1`] = ` | |||
splat="/foo/" | |||
/> | |||
<MenuItem | |||
indent={true} | |||
depth={1} | |||
key="/baz/" | |||
node={ | |||
Object { | |||
@@ -67,5 +84,41 @@ exports[`should render an opened menu block 1`] = ` | |||
} | |||
splat="/foo/" | |||
/> | |||
<MenuBlock | |||
block={ | |||
Object { | |||
"children": Array [ | |||
"/baz/foo", | |||
], | |||
"title": "Baz", | |||
} | |||
} | |||
depth={1} | |||
key="Baz" | |||
openByDefault={false} | |||
openChain={Array []} | |||
pages={ | |||
Array [ | |||
Object { | |||
"content": "bar", | |||
"navTitle": undefined, | |||
"relativeName": "/bar/", | |||
"text": "bar", | |||
"title": "Bar", | |||
"url": "/bar/", | |||
}, | |||
Object { | |||
"content": "baz", | |||
"navTitle": "baznav", | |||
"relativeName": "/baz/", | |||
"text": "baz", | |||
"title": "baz", | |||
"url": "/baz/", | |||
}, | |||
] | |||
} | |||
splat="/foo/" | |||
title="Baz" | |||
/> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,43 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should not render a high depth differently than a depth of 3 1`] = ` | |||
<Link | |||
className="list-group-item depth-3" | |||
key="/bar" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/bar" | |||
> | |||
<h3 | |||
className="list-group-item-heading" | |||
/> | |||
</Link> | |||
`; | |||
exports[`should render correctly 1`] = ` | |||
<Link | |||
className="list-group-item" | |||
key="/bar" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/bar" | |||
> | |||
<h3 | |||
className="list-group-item-heading" | |||
/> | |||
</Link> | |||
`; | |||
exports[`should render correctly if the current node matches the splat 1`] = ` | |||
<Link | |||
className="list-group-item active" | |||
key="/bar" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation/bar" | |||
> | |||
<h3 | |||
className="list-group-item-heading" | |||
/> | |||
</Link> | |||
`; |
@@ -17,20 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { sortBy, flatten } from 'lodash'; | |||
import { sortBy } from 'lodash'; | |||
export type DocumentationEntryScope = 'sonarqube' | 'sonarcloud' | 'static'; | |||
export interface DocsNavigationBlock { | |||
title: string; | |||
children: string[]; | |||
} | |||
export interface DocsNavigationExternalLink { | |||
title: string; | |||
url: string; | |||
} | |||
export interface DocumentationEntry { | |||
content: string; | |||
relativeName: string; | |||
@@ -40,28 +30,6 @@ export interface DocumentationEntry { | |||
url: string; | |||
} | |||
export type DocsNavigationItem = string | DocsNavigationBlock | DocsNavigationExternalLink; | |||
export function isDocsNavigationBlock(item: DocsNavigationItem): item is DocsNavigationBlock { | |||
return typeof item === 'object' && !(item as any).url; | |||
} | |||
export function isDocsNavigationExternalLink( | |||
item: DocsNavigationItem | |||
): item is DocsNavigationExternalLink { | |||
return typeof item === 'object' && (item as any).url; | |||
} | |||
export function getUrlsList(navigation: DocsNavigationItem[]): string[] { | |||
return flatten( | |||
navigation | |||
.filter(item => !isDocsNavigationExternalLink(item)) | |||
.map((item: string | DocsNavigationBlock) => | |||
isDocsNavigationBlock(item) ? item.children : [item] | |||
) | |||
); | |||
} | |||
export function getNodeFromUrl(pages: DocumentationEntry[], url: string) { | |||
return pages.find(p => p.url === url); | |||
} |