Browse Source

SONAR-11955 Allow more levels in embedded documentation, and make it DRYer by re-using logic from sonar-docs

tags/7.8
Wouter Admiraal 5 years ago
parent
commit
9ee9132d8b
17 changed files with 503 additions and 189 deletions
  1. 3
    1
      server/sonar-web/package.json
  2. 17
    1
      server/sonar-web/src/main/js/app/styles/components/list-groups.css
  3. 3
    3
      server/sonar-web/src/main/js/apps/documentation/components/App.tsx
  4. 41
    49
      server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx
  5. 61
    16
      server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx
  6. 6
    5
      server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx
  7. 4
    2
      server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx
  8. 3
    2
      server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx
  9. 40
    4
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx
  10. 11
    9
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx
  11. 59
    46
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx
  12. 39
    0
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuItem-test.tsx
  13. 40
    2
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap
  14. 70
    7
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap
  15. 62
    9
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap
  16. 43
    0
      server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap
  17. 1
    33
      server/sonar-web/src/main/js/apps/documentation/utils.ts

+ 3
- 1
server/sonar-web/package.json View File

@@ -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",

+ 17
- 1
server/sonar-web/src/main/js/app/styles/components/list-groups.css View File

@@ -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 {

+ 3
- 3
server/sonar-web/src/main/js/apps/documentation/components/App.tsx View File

@@ -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');

+ 41
- 49
server/sonar-web/src/main/js/apps/documentation/components/Menu.tsx View File

@@ -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}
/>
);
})}
</>
);
}
}

+ 61
- 16
server/sonar-web/src/main/js/apps/documentation/components/MenuBlock.tsx View File

@@ -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)}
</>
);
}

+ 6
- 5
server/sonar-web/src/main/js/apps/documentation/components/MenuItem.tsx View File

@@ -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>

+ 4
- 2
server/sonar-web/src/main/js/apps/documentation/components/SearchResults.tsx View File

@@ -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;

+ 3
- 2
server/sonar-web/src/main/js/apps/documentation/components/Sidebar.tsx View File

@@ -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;
}

+ 40
- 4
server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx View File

@@ -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();

+ 11
- 9
server/sonar-web/src/main/js/apps/documentation/components/__tests__/Menu-test.tsx View File

@@ -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();
});

+ 59
- 46
server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuBlock-test.tsx View File

@@ -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}
/>
);
}

+ 39
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/MenuItem-test.tsx View File

@@ -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} />);
}

+ 40
- 2
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -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={

+ 70
- 7
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/Menu-test.tsx.snap View File

@@ -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>
`;

+ 62
- 9
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuBlock-test.tsx.snap View File

@@ -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>
`;

+ 43
- 0
server/sonar-web/src/main/js/apps/documentation/components/__tests__/__snapshots__/MenuItem-test.tsx.snap View File

@@ -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>
`;

+ 1
- 33
server/sonar-web/src/main/js/apps/documentation/utils.ts View File

@@ -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);
}

Loading…
Cancel
Save