aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-04-15 11:19:06 +0200
committerSonarTech <sonartech@sonarsource.com>2019-04-25 20:21:05 +0200
commit15b6c5780c0a3d27a00e944d9770cf5197b115d2 (patch)
tree33a2086079eea4facfd86cc122487ac8e4348aa1
parent159d8427bd256d7bcc27d8329bcb4baa15131e4b (diff)
downloadsonarqube-15b6c5780c0a3d27a00e944d9770cf5197b115d2.tar.gz
sonarqube-15b6c5780c0a3d27a00e944d9770cf5197b115d2.zip
SONAR-11955 Allow more levels in documentation navigation
-rw-r--r--server/sonar-docs/config/jest/SetupJest.ts12
-rw-r--r--server/sonar-docs/package.json4
-rw-r--r--server/sonar-docs/src/@types/types.d.ts2
-rw-r--r--server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js13
-rw-r--r--server/sonar-docs/src/components/CategoryBlockLink.tsx53
-rw-r--r--server/sonar-docs/src/components/Sidebar.tsx113
-rw-r--r--server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx5
-rw-r--r--server/sonar-docs/src/components/__tests__/Sidebar-test.tsx106
-rw-r--r--server/sonar-docs/src/components/__tests__/__snapshots__/Sidebar-test.tsx.snap248
-rw-r--r--server/sonar-docs/src/components/__tests__/navTreeUtils-test.ts87
-rw-r--r--server/sonar-docs/src/components/navTreeUtils.ts64
-rw-r--r--server/sonar-docs/src/layouts/layout.css5
-rw-r--r--server/sonar-docs/static/SonarQubeNavigationTree.json3
-rw-r--r--server/sonar-docs/yarn.lock21
14 files changed, 646 insertions, 90 deletions
diff --git a/server/sonar-docs/config/jest/SetupJest.ts b/server/sonar-docs/config/jest/SetupJest.ts
new file mode 100644
index 00000000000..0a0edeb4be2
--- /dev/null
+++ b/server/sonar-docs/config/jest/SetupJest.ts
@@ -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;
diff --git a/server/sonar-docs/package.json b/server/sonar-docs/package.json
index db49904f1a0..ad2b1e4e63d 100644
--- a/server/sonar-docs/package.json
+++ b/server/sonar-docs/package.json
@@ -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"
diff --git a/server/sonar-docs/src/@types/types.d.ts b/server/sonar-docs/src/@types/types.d.ts
index 024ad6ad26a..67db6d2ad17 100644
--- a/server/sonar-docs/src/@types/types.d.ts
+++ b/server/sonar-docs/src/@types/types.d.ts
@@ -27,7 +27,7 @@ export type DocNavigationItem = string | DocsNavigationBlock | DocsNavigationExt
export interface DocsNavigationBlock {
title: string;
- children: string[];
+ children: (DocNavigationItem | string)[];
}
export interface DocsNavigationExternalLink {
diff --git a/server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js b/server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js
index 4640ad1cd27..2cf6bceed66 100644
--- a/server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js
+++ b/server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js
@@ -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();
});
diff --git a/server/sonar-docs/src/components/CategoryBlockLink.tsx b/server/sonar-docs/src/components/CategoryBlockLink.tsx
index 4276f3c430b..ef062e2cb17 100644
--- a/server/sonar-docs/src/components/CategoryBlockLink.tsx
+++ b/server/sonar-docs/src/components/CategoryBlockLink.tsx
@@ -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>
diff --git a/server/sonar-docs/src/components/Sidebar.tsx b/server/sonar-docs/src/components/Sidebar.tsx
index 662b1533264..e49e89b8b4e 100644
--- a/server/sonar-docs/src/components/Sidebar.tsx
+++ b/server/sonar-docs/src/components/Sidebar.tsx
@@ -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>
diff --git a/server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx b/server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx
index 47721da0bba..64a068bd3a7 100644
--- a/server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx
+++ b/server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx
@@ -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[]}
diff --git a/server/sonar-docs/src/components/__tests__/Sidebar-test.tsx b/server/sonar-docs/src/components/__tests__/Sidebar-test.tsx
new file mode 100644
index 00000000000..a7d64ea8ba2
--- /dev/null
+++ b/server/sonar-docs/src/components/__tests__/Sidebar-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-docs/src/components/__tests__/__snapshots__/Sidebar-test.tsx.snap b/server/sonar-docs/src/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
new file mode 100644
index 00000000000..3380f584f82
--- /dev/null
+++ b/server/sonar-docs/src/components/__tests__/__snapshots__/Sidebar-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-docs/src/components/__tests__/navTreeUtils-test.ts b/server/sonar-docs/src/components/__tests__/navTreeUtils-test.ts
new file mode 100644
index 00000000000..c7d188f1d91
--- /dev/null
+++ b/server/sonar-docs/src/components/__tests__/navTreeUtils-test.ts
@@ -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);
+ });
+});
diff --git a/server/sonar-docs/src/components/navTreeUtils.ts b/server/sonar-docs/src/components/navTreeUtils.ts
index a6e681b5e64..10203272179 100644
--- a/server/sonar-docs/src/components/navTreeUtils.ts
+++ b/server/sonar-docs/src/components/navTreeUtils.ts
@@ -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, '')
+ );
}
diff --git a/server/sonar-docs/src/layouts/layout.css b/server/sonar-docs/src/layouts/layout.css
index f8b7d8d86c5..d58309fbff8 100644
--- a/server/sonar-docs/src/layouts/layout.css
+++ b/server/sonar-docs/src/layouts/layout.css
@@ -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);
diff --git a/server/sonar-docs/static/SonarQubeNavigationTree.json b/server/sonar-docs/static/SonarQubeNavigationTree.json
index c5a7d2c9a64..1c1b66ea4d4 100644
--- a/server/sonar-docs/static/SonarQubeNavigationTree.json
+++ b/server/sonar-docs/static/SonarQubeNavigationTree.json
@@ -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/"
-
]
diff --git a/server/sonar-docs/yarn.lock b/server/sonar-docs/yarn.lock
index 2046238e3a4..3719f7e3de6 100644
--- a/server/sonar-docs/yarn.lock
+++ b/server/sonar-docs/yarn.lock
@@ -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"