aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-07-20 16:11:03 +0200
committerSonarTech <sonartech@sonarsource.com>2018-07-25 20:21:21 +0200
commit275eb88dc045740c01e8a5b0c6d4449731ed48f9 (patch)
tree3e12736cac326fe13e8c3026503c351ceb5c64c8 /server
parentc76da4346704fdc8a2ce5ecedb1e0461186ba222 (diff)
downloadsonarqube-275eb88dc045740c01e8a5b0c6d4449731ed48f9.tar.gz
sonarqube-275eb88dc045740c01e8a5b0c6d4449731ed48f9.zip
SONAR-11014 Create a collapsible component
Diffstat (limited to 'server')
-rw-r--r--server/sonar-docs/gatsby-config.js3
-rw-r--r--server/sonar-docs/src/images/close.svg1
-rw-r--r--server/sonar-docs/src/images/open.svg1
-rw-r--r--server/sonar-docs/src/templates/page.css40
-rw-r--r--server/sonar-docs/src/templates/page.js82
-rw-r--r--server/sonar-web/src/main/js/apps/documentation/styles.css20
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx70
-rw-r--r--server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap48
10 files changed, 286 insertions, 37 deletions
diff --git a/server/sonar-docs/gatsby-config.js b/server/sonar-docs/gatsby-config.js
index 19d540c5c43..831aa76758d 100644
--- a/server/sonar-docs/gatsby-config.js
+++ b/server/sonar-docs/gatsby-config.js
@@ -42,7 +42,8 @@ module.exports = {
danger: 'alert alert-danger',
warning: 'alert alert-warning',
info: 'alert alert-info',
- success: 'alert alert-success'
+ success: 'alert alert-success',
+ collapse: 'collapse'
}
}
}
diff --git a/server/sonar-docs/src/images/close.svg b/server/sonar-docs/src/images/close.svg
new file mode 100644
index 00000000000..10b0b0a44f9
--- /dev/null
+++ b/server/sonar-docs/src/images/close.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M11.596 8.28l-4.604 4.602a.382.382 0 0 1-.279.118.382.382 0 0 1-.279-.118l-1.03-1.03a.382.382 0 0 1-.117-.278c0-.108.04-.201.117-.28L8.7 8 5.404 4.706a.382.382 0 0 1-.117-.28c0-.108.04-.2.117-.279l1.03-1.03A.382.382 0 0 1 6.714 3c.107 0 .2.04.278.118l4.604 4.603a.382.382 0 0 1 .117.279c0 .108-.04.201-.117.28z" fill="#236a97" /></svg> \ No newline at end of file
diff --git a/server/sonar-docs/src/images/open.svg b/server/sonar-docs/src/images/open.svg
new file mode 100644
index 00000000000..b5d7e4c6d26
--- /dev/null
+++ b/server/sonar-docs/src/images/open.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M7.72 11.596L3.119 6.992A.382.382 0 0 1 3 6.713c0-.108.04-.2.118-.279l1.03-1.03a.382.382 0 0 1 .278-.117c.108 0 .201.04.28.117L8 8.7l3.294-3.295a.382.382 0 0 1 .28-.117c.108 0 .2.04.279.117l1.03 1.03a.382.382 0 0 1 .117.28c0 .107-.04.2-.118.278L8.28 11.596a.382.382 0 0 1-.279.117.382.382 0 0 1-.28-.117z" fill="#236a97" /></svg> \ No newline at end of file
diff --git a/server/sonar-docs/src/templates/page.css b/server/sonar-docs/src/templates/page.css
index cf8ff3dd405..1e6c4dd8de5 100644
--- a/server/sonar-docs/src/templates/page.css
+++ b/server/sonar-docs/src/templates/page.css
@@ -32,3 +32,43 @@
background-color: #dff0d8;
color: #3c763d;
}
+
+.collapse {
+ border: 1px solid #e6e6e6;
+ border-radius: 2px;
+ background-color: #f3f3f3;
+ padding: 8px;
+ margin: 0 -1em 1.5rem;
+}
+
+.collapse > a:first-child {
+ background: url(../images/open.svg) no-repeat 0 50%;
+ padding-left: 20px;
+ display: block;
+ color: #236a97;
+ display: block;
+ cursor: pointer;
+ margin-bottom: 0.5rem;
+ font-size: 16px;
+ text-decoration: none;
+}
+
+.collapse.close > a:first-child {
+ background: url(../images/close.svg) no-repeat 0 50%;
+}
+
+.collapse.close > * {
+ display: none;
+}
+
+.collapse.close > a:first-child {
+ margin: 0;
+}
+
+.collapse *:last-child {
+ margin-bottom: 0;
+}
+
+.collapse .alert {
+ margin: 0 0.5em 1.5rem;
+}
diff --git a/server/sonar-docs/src/templates/page.js b/server/sonar-docs/src/templates/page.js
index 45040ee4807..2c04417fe55 100644
--- a/server/sonar-docs/src/templates/page.js
+++ b/server/sonar-docs/src/templates/page.js
@@ -21,43 +21,57 @@ import React from 'react';
import Helmet from 'react-helmet';
import './page.css';
-export default ({ data }) => {
- const page = data.markdownRemark;
- let htmlWithInclusions = cutSonarCloudContent(page.html).replace(
- /\<p\>@include (.*)\<\/p\>/,
- (_, path) => {
- const chunk = data.allMarkdownRemark.edges.find(
- edge => edge.node.fields && edge.node.fields.slug === path
- );
- return chunk ? chunk.node.html : '';
+export default class Page extends React.PureComponent {
+ componentDidMount() {
+ const collaspables = document.getElementsByClassName('collapse');
+ for (let i = 0; i < collaspables.length; i++) {
+ collaspables[i].classList.add('close');
+ collaspables[i].firstChild.outerHTML = collaspables[i].firstChild.outerHTML
+ .replace(/\<h2/gi, '<a href="#"')
+ .replace(/\<\/h2\>/gi, '</a>');
+ collaspables[i].firstChild.addEventListener('click', e => {
+ e.currentTarget.parentNode.classList.toggle('close');
+ e.preventDefault();
+ });
}
- );
-
- if (
- page.headings &&
- page.headings.length > 0 &&
- page.html.match(/<h[1-9]>Table Of Contents<\/h[1-9]>/i)
- ) {
- htmlWithInclusions = generateTableOfContents(htmlWithInclusions, page.headings);
}
- return (
- <div css={{ paddingTop: 24, paddingBottom: 24 }}>
- <Helmet title={page.frontmatter.title} />
- <h1>{page.frontmatter.title}</h1>
- <div
- css={{
- '& img[src$=".svg"]': {
- position: 'relative',
- top: '-2px',
- verticalAlign: 'text-bottom'
- }
- }}
- dangerouslySetInnerHTML={{ __html: htmlWithInclusions }}
- />
- </div>
- );
-};
+ render() {
+ const page = this.props.data.markdownRemark;
+ let htmlWithInclusions = cutSonarCloudContent(page.html).replace(
+ /\<p\>@include (.*)\<\/p\>/,
+ (_, path) => {
+ const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path);
+ return chunk ? chunk.node.html : '';
+ }
+ );
+
+ if (
+ page.headings &&
+ page.headings.length > 0 &&
+ page.html.match(/<h[1-9]>Table Of Contents<\/h[1-9]>/i)
+ ) {
+ htmlWithInclusions = generateTableOfContents(htmlWithInclusions, page.headings);
+ }
+
+ return (
+ <div css={{ paddingTop: 24, paddingBottom: 24 }}>
+ <Helmet title={page.frontmatter.title} />
+ <h1>{page.frontmatter.title}</h1>
+ <div
+ css={{
+ '& img[src$=".svg"]': {
+ position: 'relative',
+ top: '-2px',
+ verticalAlign: 'text-bottom'
+ }
+ }}
+ dangerouslySetInnerHTML={{ __html: htmlWithInclusions }}
+ />
+ </div>
+ );
+ }
+}
export const query = graphql`
query PageQuery($slug: String!) {
diff --git a/server/sonar-web/src/main/js/apps/documentation/styles.css b/server/sonar-web/src/main/js/apps/documentation/styles.css
index 04941089c06..5eb00833df4 100644
--- a/server/sonar-web/src/main/js/apps/documentation/styles.css
+++ b/server/sonar-web/src/main/js/apps/documentation/styles.css
@@ -81,3 +81,23 @@
.documentation-content.markdown .alert p {
margin: 0;
}
+
+.documentation-content.markdown .collapse-container {
+ border: 1px solid var(--barBorderColor);
+ border-radius: 2px;
+ background-color: var(--barBackgroundColor);
+ padding: 8px;
+ margin: 0.8em 0 2em;
+}
+
+.documentation-content.markdown .collapse-container > a:first-child {
+ display: block;
+}
+
+.documentation-content.markdown .collapse-container > a:first-child:focus {
+ color: var(--darkBlue);
+}
+
+.documentation-content.markdown .collapse-container *:last-child {
+ margin-bottom: 0;
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx
new file mode 100644
index 00000000000..47b8c96183d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 OpenCloseIcon from '../icons-components/OpenCloseIcon';
+
+interface State {
+ open: boolean;
+}
+
+export default class DocCollapsibleBlock extends React.PureComponent<{}, State> {
+ state = { open: false };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ this.setState(state => ({ open: !state.open }));
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ renderTitle(children: any) {
+ return (
+ <a
+ aria-expanded={String(this.state.open)}
+ aria-haspopup={true}
+ className="link-no-underline"
+ href="#"
+ onClick={this.handleClick}>
+ <OpenCloseIcon className="vertical-middle little-spacer-right" open={this.state.open} />
+ {children.props ? children.props.children : children}
+ </a>
+ );
+ }
+
+ render() {
+ const childrenAsArray = React.Children.toArray(this.props.children);
+ if (childrenAsArray.length < 1) {
+ return null;
+ }
+
+ const firstChildChildren = React.Children.toArray(
+ (childrenAsArray[0] as React.ReactElement<any>).props.children
+ );
+ if (firstChildChildren.length < 2) {
+ return null;
+ }
+
+ return (
+ <div className="collapse-container">
+ {this.renderTitle(firstChildChildren[0])}
+ {this.state.open && firstChildChildren.slice(1)}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
index 72978acb947..bb033ac2999 100644
--- a/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
+++ b/server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
@@ -26,6 +26,7 @@ import remarkCustomBlocks from 'remark-custom-blocks';
import DocLink from './DocLink';
import DocImg from './DocImg';
import DocTooltipLink from './DocTooltipLink';
+import DocCollapsibleBlock from './DocCollapsibleBlock';
import { separateFrontMatter, filterContent } from '../../helpers/markdown';
import { scrollToElement } from '../../helpers/scrolling';
@@ -63,7 +64,8 @@ export default class DocMarkdownBlock extends React.PureComponent<Props> {
danger: { classes: 'alert alert-danger' },
warning: { classes: 'alert alert-warning' },
info: { classes: 'alert alert-info' },
- success: { classes: 'alert alert-success' }
+ success: { classes: 'alert alert-success' },
+ collapse: { classes: 'collapse' }
})
.use(reactRenderer, {
remarkReactComponents: {
@@ -96,7 +98,11 @@ function withChildProps<P>(
function Block(props: React.HtmlHTMLAttributes<HTMLDivElement>) {
if (props.className) {
- return <div className={classNames('cut-margins', props.className)}>{props.children}</div>;
+ if (props.className.includes('collapse')) {
+ return <DocCollapsibleBlock>{props.children}</DocCollapsibleBlock>;
+ } else {
+ return <div className={classNames('cut-margins', props.className)}>{props.children}</div>;
+ }
} else {
return props.children;
}
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx
new file mode 100644
index 00000000000..c18666d7a2f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 DocCollapsibleBlock from '../DocCollapsibleBlock';
+import { click } from '../../../helpers/testUtils';
+
+const children = (
+ <div>
+ <h2>Foo</h2>
+ <p>Bar</p>
+ </div>
+);
+
+it('should render a collapsible block', () => {
+ const wrapper = shallow(<DocCollapsibleBlock>{children}</DocCollapsibleBlock>);
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('a'));
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render if not at least 2 children', () => {
+ const wrapper = shallow(
+ <DocCollapsibleBlock>
+ <div>foobar</div>
+ </DocCollapsibleBlock>
+ );
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap
new file mode 100644
index 00000000000..763267113d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render if not at least 2 children 1`] = `""`;
+
+exports[`should render a collapsible block 1`] = `
+<div
+ className="collapse-container"
+>
+ <a
+ aria-expanded="false"
+ aria-haspopup={true}
+ className="link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <OpenCloseIcon
+ className="vertical-middle little-spacer-right"
+ open={false}
+ />
+ Foo
+ </a>
+</div>
+`;
+
+exports[`should render a collapsible block 2`] = `
+<div
+ className="collapse-container"
+>
+ <a
+ aria-expanded="true"
+ aria-haspopup={true}
+ className="link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <OpenCloseIcon
+ className="vertical-middle little-spacer-right"
+ open={true}
+ />
+ Foo
+ </a>
+ <p
+ key=".1"
+ >
+ Bar
+ </p>
+</div>
+`;