@@ -0,0 +1,12 @@ | |||
/* | |||
* Copyright (C) 2017-2019 SonarSource SA | |||
* All rights reserved | |||
* mailto:info AT sonarsource DOT com | |||
*/ | |||
import { GlobalWithFetchMock } from 'jest-fetch-mock'; | |||
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock; | |||
customGlobal.fetch = require('jest-fetch-mock'); | |||
customGlobal.fetchMock = customGlobal.fetch; |
@@ -47,6 +47,7 @@ | |||
"glob-promise": "3.4.0", | |||
"graphql-code-generator": "0.5.2", | |||
"jest": "24.5.0", | |||
"jest-fetch-mock": "2.1.2", | |||
"prettier": "1.16.4", | |||
"react-test-renderer": "16.8.5", | |||
"remark": "10.0.1", | |||
@@ -92,7 +93,8 @@ | |||
"^.+\\.css$": "<rootDir>/config/jest/CSSStub.js" | |||
}, | |||
"setupFiles": [ | |||
"<rootDir>/config/jest/SetupEnzyme.js" | |||
"<rootDir>/config/jest/SetupEnzyme.js", | |||
"<rootDir>/config/jest/SetupJest.ts" | |||
], | |||
"snapshotSerializers": [ | |||
"enzyme-to-json/serializer" |
@@ -27,7 +27,7 @@ export type DocNavigationItem = string | DocsNavigationBlock | DocsNavigationExt | |||
export interface DocsNavigationBlock { | |||
title: string; | |||
children: string[]; | |||
children: (DocNavigationItem | string)[]; | |||
} | |||
export interface DocsNavigationExternalLink { |
@@ -53,23 +53,18 @@ it('should have valid links in trees files', () => { | |||
let hasErrors = false; | |||
trees.forEach(file => { | |||
const tree = JSON.parse(fs.readFileSync(path.join(rootPath, '..', 'static', file), 'utf8')); | |||
tree.forEach(leaf => { | |||
const walk = leaf => { | |||
if (typeof leaf === 'object') { | |||
if (leaf.children) { | |||
leaf.children.forEach(child => { | |||
// Check children markdown file path validity | |||
if (!urlExists(parsedFiles, child)) { | |||
console.log(`[${child}] is not a valid link, in ${file}`); | |||
hasErrors = true; | |||
} | |||
}); | |||
leaf.children.forEach(walk); | |||
} | |||
} else if (!urlExists(parsedFiles, leaf)) { | |||
// Check markdown file path validity | |||
console.log(`[${leaf}] is not a valid link, in ${file}`); | |||
hasErrors = true; | |||
} | |||
}); | |||
}; | |||
tree.forEach(walk); | |||
}); | |||
expect(hasErrors).toBeFalsy(); | |||
}); |
@@ -25,22 +25,46 @@ import ChevronUpIcon from './icons/ChevronUpIcon'; | |||
import { MarkdownRemark } from '../@types/graphql-types'; | |||
interface Props { | |||
children: MarkdownRemark[]; | |||
children: (MarkdownRemark | JSX.Element)[]; | |||
location: Location; | |||
onToggle: (title: string) => void; | |||
open: boolean; | |||
openByDefault: boolean; | |||
title: string; | |||
} | |||
export default class CategoryLink extends React.PureComponent<Props> { | |||
interface State { | |||
open: boolean; | |||
} | |||
export default class CategoryLink extends React.PureComponent<Props, State> { | |||
state: State; | |||
static defaultProps = { | |||
openByDefault: false | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
open: props.openByDefault | |||
}; | |||
} | |||
handleToggle = (event: React.MouseEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onToggle(this.props.title); | |||
this.setState(prevState => ({ | |||
open: !prevState.open | |||
})); | |||
}; | |||
isMarkdownRemark = (child: any): child is MarkdownRemark => { | |||
return child.id !== undefined; | |||
}; | |||
render() { | |||
const { children, location, title, open } = this.props; | |||
const { children, location, title } = this.props; | |||
const { open } = this.state; | |||
return ( | |||
<div> | |||
<a | |||
@@ -52,9 +76,20 @@ export default class CategoryLink extends React.PureComponent<Props> { | |||
</a> | |||
{children && open && ( | |||
<div className="sub-menu"> | |||
{children.map(page => ( | |||
<PageLink className="sub-menu-link" key={page.id} location={location} node={page} /> | |||
))} | |||
{children.map((child, i) => { | |||
if (this.isMarkdownRemark(child)) { | |||
return ( | |||
<PageLink | |||
className="sub-menu-link" | |||
key={child.id} | |||
location={location} | |||
node={child} | |||
/> | |||
); | |||
} else { | |||
return <React.Fragment key={`child-${i}`}>{child}</React.Fragment>; | |||
} | |||
})} | |||
</div> | |||
)} | |||
</div> |
@@ -26,7 +26,13 @@ import Search from './Search'; | |||
import SearchEntryResult from './SearchEntryResult'; | |||
import VersionSelect from './VersionSelect'; | |||
import DownloadIcon from './icons/DownloadIcon'; | |||
import { getNavTree, isDocsNavigationBlock, isDocsNavigationExternalLink } from './navTreeUtils'; | |||
import { | |||
getNavTree, | |||
isDocsNavigationBlock, | |||
isDocsNavigationExternalLink, | |||
getOpenChainFromPath, | |||
testPathAgainstUrl | |||
} from './navTreeUtils'; | |||
import { MarkdownRemark } from '../@types/graphql-types'; | |||
import { SearchResult, DocVersion, DocNavigationItem } from '../@types/types'; | |||
@@ -39,7 +45,7 @@ interface Props { | |||
interface State { | |||
loaded: boolean; | |||
navTree: DocNavigationItem[]; | |||
openBlockTitle: string; | |||
openChain?: DocNavigationItem[]; | |||
query: string; | |||
results: SearchResult[]; | |||
versions: DocVersion[]; | |||
@@ -52,7 +58,7 @@ export default class Sidebar extends React.PureComponent<Props, State> { | |||
this.state = { | |||
loaded: false, | |||
navTree, | |||
openBlockTitle: this.getOpenBlockFromLocation(this.props.location, navTree), | |||
openChain: getOpenChainFromPath(this.props.location.pathname, navTree), | |||
query: '', | |||
results: [], | |||
versions: [] | |||
@@ -66,20 +72,11 @@ export default class Sidebar extends React.PureComponent<Props, State> { | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.location.pathname !== prevProps.location.pathname) { | |||
this.setState(({ navTree }) => ({ | |||
openBlockTitle: this.getOpenBlockFromLocation(this.props.location, navTree) | |||
openChain: getOpenChainFromPath(this.props.location.pathname, navTree) | |||
})); | |||
} | |||
} | |||
// A block is opened if the current page is set to one of his children | |||
getOpenBlockFromLocation({ pathname }: Location, navTree: DocNavigationItem[]) { | |||
const element = navTree.find( | |||
leave => | |||
isDocsNavigationBlock(leave) && leave.children.some(child => pathname.endsWith(child)) | |||
); | |||
return isDocsNavigationBlock(element) ? element.title : ''; | |||
} | |||
loadVersions() { | |||
fetch('/DocsVersions.json') | |||
.then(response => response.json()) | |||
@@ -90,59 +87,69 @@ export default class Sidebar extends React.PureComponent<Props, State> { | |||
} | |||
getNodeFromUrl = (url: string) => { | |||
return this.props.pages.find(p => | |||
Boolean( | |||
(p.fields && p.fields.slug === url + '/') || (p.frontmatter && p.frontmatter.url === url) | |||
) | |||
); | |||
}; | |||
return this.props.pages.find(p => { | |||
if (p.fields && p.fields.slug) { | |||
if (testPathAgainstUrl(p.fields.slug, url)) { | |||
return true; | |||
} | |||
} | |||
if (p.frontmatter && p.frontmatter.url) { | |||
if (testPathAgainstUrl(p.frontmatter.url, url)) { | |||
return true; | |||
} | |||
} | |||
handleToggle = (title: string) => { | |||
this.setState(state => ({ openBlockTitle: state.openBlockTitle === title ? '' : title })); | |||
return false; | |||
}); | |||
}; | |||
handleSearch = (results: SearchResult[], query: string) => { | |||
this.setState({ results, query }); | |||
}; | |||
renderCategories = () => { | |||
return ( | |||
<nav> | |||
{this.state.navTree.map(leave => { | |||
if (isDocsNavigationBlock(leave)) { | |||
return ( | |||
<CategoryBlockLink | |||
key={leave.title} | |||
location={this.props.location} | |||
onToggle={this.handleToggle} | |||
open={leave.title === this.state.openBlockTitle} | |||
title={leave.title}> | |||
{ | |||
leave.children | |||
.map(child => this.getNodeFromUrl(child)) | |||
.filter(Boolean) as MarkdownRemark[] | |||
} | |||
</CategoryBlockLink> | |||
); | |||
renderCategory = (leaf: DocNavigationItem) => { | |||
if (isDocsNavigationBlock(leaf)) { | |||
let children: (MarkdownRemark | JSX.Element)[] = []; | |||
leaf.children.forEach(child => { | |||
if (typeof child === 'string') { | |||
const node = this.getNodeFromUrl(child); | |||
if (node) { | |||
children.push(node); | |||
} | |||
} else { | |||
children = children.concat(this.renderCategory(child)); | |||
} | |||
}); | |||
return ( | |||
<CategoryBlockLink | |||
key={leaf.title} | |||
location={this.props.location} | |||
openByDefault={this.state.openChain && this.state.openChain.includes(leaf)} | |||
title={leaf.title}> | |||
{children} | |||
</CategoryBlockLink> | |||
); | |||
} | |||
if (isDocsNavigationExternalLink(leave)) { | |||
return <ExternalLink external={leave.url} key={leave.title} title={leave.title} />; | |||
} | |||
if (isDocsNavigationExternalLink(leaf)) { | |||
return <ExternalLink external={leaf.url} key={leaf.title} title={leaf.title} />; | |||
} | |||
return ( | |||
<PageLink | |||
className="page-indexes-link" | |||
key={leave} | |||
location={this.props.location} | |||
node={this.getNodeFromUrl(leave)} | |||
/> | |||
); | |||
})} | |||
</nav> | |||
return ( | |||
<PageLink | |||
className="page-indexes-link" | |||
key={leaf} | |||
location={this.props.location} | |||
node={this.getNodeFromUrl(leaf)} | |||
/> | |||
); | |||
}; | |||
renderCategories = () => { | |||
return <nav>{this.state.navTree.map(this.renderCategory)}</nav>; | |||
}; | |||
renderResults = () => { | |||
return ( | |||
<div> |
@@ -27,15 +27,14 @@ it('should render correctly', () => { | |||
}); | |||
it('should render correctly when closed', () => { | |||
expect(shallowRender({ open: false })).toMatchSnapshot(); | |||
expect(shallowRender({ openByDefault: false })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<CategoryBlockLink['props']> = {}) { | |||
return shallow( | |||
<CategoryBlockLink | |||
location={{} as Location} | |||
onToggle={jest.fn()} | |||
open={true} | |||
openByDefault={true} | |||
title="My category" | |||
{...props}> | |||
{[{ id: '1' }, { id: '2' }] as MarkdownRemark[]} |
@@ -0,0 +1,106 @@ | |||
/* | |||
* 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 { FetchMock } from 'jest-fetch-mock'; | |||
import { shallow } from 'enzyme'; | |||
import Sidebar from '../Sidebar'; | |||
import { MarkdownRemark } from '../../@types/graphql-types'; | |||
jest.mock('../navTreeUtils', () => { | |||
return { | |||
...require.requireActual('../navTreeUtils'), | |||
getNavTree: jest.fn().mockReturnValue([ | |||
'/foo/', | |||
{ | |||
title: 'Foo subs', | |||
children: [ | |||
'/foo/bar/', | |||
'/foo/baz/', | |||
{ | |||
title: 'Foo Baz subs', | |||
children: [ | |||
'/foo/baz/bar/', | |||
'/foo/baz/foo/', | |||
{ | |||
title: 'Foo Baz Foo subs', | |||
children: ['/foo/baz/foo/bar/', '/foo/baz/foo/baz'] | |||
} | |||
] | |||
} | |||
] | |||
}, | |||
'/bar/', | |||
{ | |||
title: 'Bar subs', | |||
children: [{ title: 'External link 1', url: 'http://example.com/1' }, '/bar/foo/'] | |||
}, | |||
{ title: 'External link 2', url: 'http://example.com/2' } | |||
]) | |||
}; | |||
}); | |||
beforeEach(() => { | |||
(fetch as FetchMock).resetMocks(); | |||
(fetch as FetchMock).mockResponse(`[ | |||
{ "value": "2.0", "current": true }, | |||
{ "value": "1.0", "current": false } | |||
]`); | |||
}); | |||
it('should render correctly', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<Sidebar['props']> = {}) { | |||
return shallow( | |||
<Sidebar | |||
location={{ pathname: '/foo/baz/foo/bar' } as Location} | |||
pages={[ | |||
{ | |||
fields: { | |||
slug: '/foo/' | |||
}, | |||
frontmatter: { | |||
title: 'Foo' | |||
} | |||
} as MarkdownRemark, | |||
{ | |||
fields: { | |||
slug: '/foo/baz/bar' | |||
}, | |||
frontmatter: { | |||
title: 'Foo Baz Bar' | |||
} | |||
} as MarkdownRemark, | |||
{ | |||
fields: { | |||
slug: '/bar/' | |||
}, | |||
frontmatter: { | |||
title: 'Bar' | |||
} | |||
} as MarkdownRemark | |||
]} | |||
version="2.0" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,248 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="page-sidebar" | |||
> | |||
<div | |||
className="sidebar-header" | |||
> | |||
<ForwardRef | |||
to="/" | |||
> | |||
<img | |||
alt="Continuous Code Quality" | |||
className="sidebar-logo" | |||
src="/2.0/images/SonarQubeIcon.svg" | |||
title="Continuous Code Quality" | |||
width="160" | |||
/> | |||
</ForwardRef> | |||
<VersionSelect | |||
isOnCurrentVersion={true} | |||
selectedVersionValue="2.0" | |||
versions={Array []} | |||
/> | |||
</div> | |||
<div | |||
className="page-indexes" | |||
> | |||
<Search | |||
navigation={ | |||
Array [ | |||
"/foo/", | |||
Object { | |||
"children": Array [ | |||
"/foo/bar/", | |||
"/foo/baz/", | |||
Object { | |||
"children": Array [ | |||
"/foo/baz/bar/", | |||
"/foo/baz/foo/", | |||
Object { | |||
"children": Array [ | |||
"/foo/baz/foo/bar/", | |||
"/foo/baz/foo/baz", | |||
], | |||
"title": "Foo Baz Foo subs", | |||
}, | |||
], | |||
"title": "Foo Baz subs", | |||
}, | |||
], | |||
"title": "Foo subs", | |||
}, | |||
"/bar/", | |||
Object { | |||
"children": Array [ | |||
Object { | |||
"title": "External link 1", | |||
"url": "http://example.com/1", | |||
}, | |||
"/bar/foo/", | |||
], | |||
"title": "Bar subs", | |||
}, | |||
Object { | |||
"title": "External link 2", | |||
"url": "http://example.com/2", | |||
}, | |||
] | |||
} | |||
onResultsChange={[Function]} | |||
pages={ | |||
Array [ | |||
Object { | |||
"fields": Object { | |||
"slug": "/foo/", | |||
}, | |||
"frontmatter": Object { | |||
"title": "Foo", | |||
}, | |||
}, | |||
Object { | |||
"fields": Object { | |||
"slug": "/foo/baz/bar", | |||
}, | |||
"frontmatter": Object { | |||
"title": "Foo Baz Bar", | |||
}, | |||
}, | |||
Object { | |||
"fields": Object { | |||
"slug": "/bar/", | |||
}, | |||
"frontmatter": Object { | |||
"title": "Bar", | |||
}, | |||
}, | |||
] | |||
} | |||
/> | |||
<nav> | |||
<PageLink | |||
className="page-indexes-link" | |||
key="/foo/" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
node={ | |||
Object { | |||
"fields": Object { | |||
"slug": "/foo/", | |||
}, | |||
"frontmatter": Object { | |||
"title": "Foo", | |||
}, | |||
} | |||
} | |||
/> | |||
<CategoryLink | |||
key="Foo subs" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
openByDefault={true} | |||
title="Foo subs" | |||
> | |||
<CategoryLink | |||
key="Foo Baz subs" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
openByDefault={true} | |||
title="Foo Baz subs" | |||
> | |||
<Component /> | |||
<CategoryLink | |||
key="Foo Baz Foo subs" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
openByDefault={true} | |||
title="Foo Baz Foo subs" | |||
/> | |||
</CategoryLink> | |||
</CategoryLink> | |||
<PageLink | |||
className="page-indexes-link" | |||
key="/bar/" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
node={ | |||
Object { | |||
"fields": Object { | |||
"slug": "/bar/", | |||
}, | |||
"frontmatter": Object { | |||
"title": "Bar", | |||
}, | |||
} | |||
} | |||
/> | |||
<CategoryLink | |||
key="Bar subs" | |||
location={ | |||
Object { | |||
"pathname": "/foo/baz/foo/bar", | |||
} | |||
} | |||
openByDefault={false} | |||
title="Bar subs" | |||
> | |||
<ExternalLink | |||
external="http://example.com/1" | |||
key="External link 1" | |||
title="External link 1" | |||
/> | |||
</CategoryLink> | |||
<ExternalLink | |||
external="http://example.com/2" | |||
key="External link 2" | |||
title="External link 2" | |||
/> | |||
</nav> | |||
</div> | |||
<div | |||
className="sidebar-footer" | |||
> | |||
<a | |||
href="https://www.sonarqube.org/" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<DownloadIcon /> | |||
SonarQube | |||
</a> | |||
<a | |||
href="https://community.sonarsource.com/" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<img | |||
alt="Community" | |||
src="/2.0/images/community.svg" | |||
/> | |||
Community | |||
</a> | |||
<a | |||
className="icon-only" | |||
href="https://twitter.com/SonarQube" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<img | |||
alt="Twitter" | |||
src="/2.0/images/twitter.svg" | |||
/> | |||
</a> | |||
<a | |||
className="icon-only" | |||
href="https://www.sonarsource.com/resources/product-news/" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<img | |||
alt="Product News" | |||
src="/2.0/images/newspaper.svg" | |||
/> | |||
<span | |||
className="tooltip" | |||
> | |||
Product News | |||
</span> | |||
</a> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,87 @@ | |||
/* | |||
* 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 { getUrlsList, getOpenChainFromPath, testPathAgainstUrl } from '../navTreeUtils'; | |||
const navTree = [ | |||
'path/value', | |||
{ | |||
title: 'My paths', | |||
children: [ | |||
'child/path/1', | |||
{ | |||
title: 'Child paths', | |||
children: [ | |||
'sub/child/path/1', | |||
{ | |||
title: 'External link 2', | |||
url: 'http://example.com/2' | |||
}, | |||
{ | |||
title: 'Last ones, promised', | |||
children: ['sub/sub/child/path/1'] | |||
}, | |||
'sub/child/path/3' | |||
] | |||
}, | |||
'child/path/2' | |||
] | |||
}, | |||
{ | |||
title: 'External link', | |||
url: 'http://example.com' | |||
} | |||
]; | |||
describe('getUrlsList', () => { | |||
it('should return the correct values for a list of paths', () => { | |||
expect(getUrlsList(navTree)).toEqual([ | |||
'path/value', | |||
'child/path/1', | |||
'sub/child/path/1', | |||
'http://example.com/2', | |||
'sub/sub/child/path/1', | |||
'sub/child/path/3', | |||
'child/path/2', | |||
'http://example.com' | |||
]); | |||
}); | |||
}); | |||
describe('getOpenChainFromPath', () => { | |||
it('should correctly fetch the chain of open elements for a given path', () => { | |||
expect(getOpenChainFromPath('path/value/', navTree)).toEqual([navTree[0]]); | |||
expect(getOpenChainFromPath('sub/child/path/3', navTree)).toEqual([ | |||
navTree[1], | |||
(navTree as any)[1].children[1], | |||
(navTree as any)[1].children[1].children[3] | |||
]); | |||
}); | |||
}); | |||
describe('testPathAgainstUrl', () => { | |||
it('should handle paths with trailing and/or leading slashes', () => { | |||
expect(testPathAgainstUrl('path/foo/', 'path/bar')).toBe(false); | |||
expect(testPathAgainstUrl('/path/foo/', '/path/bar/')).toBe(false); | |||
expect(testPathAgainstUrl('path/foo/', 'path/foo')).toBe(true); | |||
expect(testPathAgainstUrl('path/foo', 'path/foo/')).toBe(true); | |||
expect(testPathAgainstUrl('/path/foo/', 'path/foo')).toBe(true); | |||
expect(testPathAgainstUrl('path/foo', '/path/foo/')).toBe(true); | |||
}); | |||
}); |
@@ -29,26 +29,66 @@ export function getNavTree() { | |||
return NavigationTree as DocNavigationItem[]; | |||
} | |||
export function getUrlsList(navTree: DocNavigationItem[]) { | |||
export function getUrlsList(navTree: DocNavigationItem[]): string[] { | |||
return flatten( | |||
navTree.map(leave => { | |||
if (isDocsNavigationBlock(leave)) { | |||
return leave.children; | |||
navTree.map(leaf => { | |||
if (isDocsNavigationBlock(leaf)) { | |||
return getUrlsList(leaf.children); | |||
} | |||
if (isDocsNavigationExternalLink(leave)) { | |||
return leave.url; | |||
if (isDocsNavigationExternalLink(leaf)) { | |||
return [leaf.url]; | |||
} | |||
return [leave]; | |||
return [leaf]; | |||
}) | |||
); | |||
} | |||
export function isDocsNavigationBlock(leave?: DocNavigationItem): leave is DocsNavigationBlock { | |||
return typeof leave === 'object' && (leave as DocsNavigationBlock).children !== undefined; | |||
export function getOpenChainFromPath(pathname: string, navTree: DocNavigationItem[]) { | |||
let chain: DocNavigationItem[] = []; | |||
let found = false; | |||
const walk = (leaf: DocNavigationItem, parents: DocNavigationItem[] = []) => { | |||
if (found) { | |||
return; | |||
} | |||
parents = parents.concat(leaf); | |||
if (isDocsNavigationBlock(leaf)) { | |||
leaf.children.forEach(child => { | |||
if (typeof child === 'string' && testPathAgainstUrl(child, pathname)) { | |||
chain = parents.concat(child); | |||
found = true; | |||
} else { | |||
walk(child, parents); | |||
} | |||
}); | |||
} else if (typeof leaf === 'string' && testPathAgainstUrl(leaf, pathname)) { | |||
chain = parents; | |||
found = true; | |||
} | |||
}; | |||
navTree.forEach(leaf => walk(leaf)); | |||
return chain; | |||
} | |||
export function isDocsNavigationBlock(leaf?: DocNavigationItem): leaf is DocsNavigationBlock { | |||
return typeof leaf === 'object' && (leaf as DocsNavigationBlock).children !== undefined; | |||
} | |||
export function isDocsNavigationExternalLink( | |||
leave?: DocNavigationItem | |||
): leave is DocsNavigationExternalLink { | |||
return typeof leave === 'object' && (leave as DocsNavigationExternalLink).url !== undefined; | |||
leaf?: DocNavigationItem | |||
): leaf is DocsNavigationExternalLink { | |||
return typeof leaf === 'object' && (leaf as DocsNavigationExternalLink).url !== undefined; | |||
} | |||
export function testPathAgainstUrl(path: string, url: string) { | |||
const leadingRegEx = /^\//; | |||
const trailingRegEx = /\/$/; | |||
return ( | |||
path.replace(leadingRegEx, '').replace(trailingRegEx, '') === | |||
url.replace(leadingRegEx, '').replace(trailingRegEx, '') | |||
); | |||
} |
@@ -283,6 +283,11 @@ a.search-result .note { | |||
display: block; | |||
} | |||
.sub-menu .page-indexes-link, | |||
.sub-menu .sub-menu { | |||
margin-left: -10px; | |||
} | |||
.page-indexes-link svg { | |||
float: right; | |||
transform: translateY(9px); |
@@ -40,7 +40,7 @@ | |||
"/user-guide/security-hotspots/", | |||
"/user-guide/rules/", | |||
"/user-guide/security-rules/", | |||
"/user-guide/built-in-rule-tags/", | |||
"/user-guide/built-in-rule-tags/", | |||
"/user-guide/quality-gates/", | |||
"/user-guide/metric-definitions/", | |||
"/user-guide/concepts/", | |||
@@ -100,5 +100,4 @@ | |||
] | |||
}, | |||
"/faq/" | |||
] |
@@ -3007,6 +3007,14 @@ cross-fetch@2.2.2: | |||
node-fetch "2.1.2" | |||
whatwg-fetch "2.0.4" | |||
cross-fetch@^2.2.2: | |||
version "2.2.3" | |||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.3.tgz#e8a0b3c54598136e037f8650f8e823ccdfac198e" | |||
integrity sha512-PrWWNH3yL2NYIb/7WF/5vFG3DCQiXDOVf8k3ijatbrtnwNuhMWLC7YF7uqf53tbTFDzHIUD8oITw4Bxt8ST3Nw== | |||
dependencies: | |||
node-fetch "2.1.2" | |||
whatwg-fetch "2.0.4" | |||
cross-spawn@5.1.0, cross-spawn@^5.0.1: | |||
version "5.1.0" | |||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" | |||
@@ -6584,6 +6592,14 @@ jest-environment-node@^24.5.0: | |||
jest-mock "^24.5.0" | |||
jest-util "^24.5.0" | |||
jest-fetch-mock@2.1.2: | |||
version "2.1.2" | |||
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-2.1.2.tgz#1260b347918e3931c4ec743ceaf60433da661bd0" | |||
integrity sha512-tcSR4Lh2bWLe1+0w/IwvNxeDocMI/6yIA2bijZ0fyWxC4kQ18lckQ1n7Yd40NKuisGmcGBRFPandRXrW/ti/Bw== | |||
dependencies: | |||
cross-fetch "^2.2.2" | |||
promise-polyfill "^7.1.1" | |||
jest-get-type@^24.3.0: | |||
version "24.3.0" | |||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.3.0.tgz#582cfd1a4f91b5cdad1d43d2932f816d543c65da" | |||
@@ -9118,6 +9134,11 @@ promise-inflight@^1.0.1: | |||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" | |||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= | |||
promise-polyfill@^7.1.1: | |||
version "7.1.2" | |||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.2.tgz#ab05301d8c28536301622d69227632269a70ca3b" | |||
integrity sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ== | |||
promise@^7.1.1: | |||
version "7.3.1" | |||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" |