diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-04-15 11:19:06 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-04-25 20:21:05 +0200 |
commit | 15b6c5780c0a3d27a00e944d9770cf5197b115d2 (patch) | |
tree | 33a2086079eea4facfd86cc122487ac8e4348aa1 | |
parent | 159d8427bd256d7bcc27d8329bcb4baa15131e4b (diff) | |
download | sonarqube-15b6c5780c0a3d27a00e944d9770cf5197b115d2.tar.gz sonarqube-15b6c5780c0a3d27a00e944d9770cf5197b115d2.zip |
SONAR-11955 Allow more levels in documentation navigation
-rw-r--r-- | server/sonar-docs/config/jest/SetupJest.ts | 12 | ||||
-rw-r--r-- | server/sonar-docs/package.json | 4 | ||||
-rw-r--r-- | server/sonar-docs/src/@types/types.d.ts | 2 | ||||
-rw-r--r-- | server/sonar-docs/src/__tests__/BrokenLinkSafetyNet.test.js | 13 | ||||
-rw-r--r-- | server/sonar-docs/src/components/CategoryBlockLink.tsx | 53 | ||||
-rw-r--r-- | server/sonar-docs/src/components/Sidebar.tsx | 113 | ||||
-rw-r--r-- | server/sonar-docs/src/components/__tests__/CategoryBlockLink-test.tsx | 5 | ||||
-rw-r--r-- | server/sonar-docs/src/components/__tests__/Sidebar-test.tsx | 106 | ||||
-rw-r--r-- | server/sonar-docs/src/components/__tests__/__snapshots__/Sidebar-test.tsx.snap | 248 | ||||
-rw-r--r-- | server/sonar-docs/src/components/__tests__/navTreeUtils-test.ts | 87 | ||||
-rw-r--r-- | server/sonar-docs/src/components/navTreeUtils.ts | 64 | ||||
-rw-r--r-- | server/sonar-docs/src/layouts/layout.css | 5 | ||||
-rw-r--r-- | server/sonar-docs/static/SonarQubeNavigationTree.json | 3 | ||||
-rw-r--r-- | server/sonar-docs/yarn.lock | 21 |
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" |