aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-04-26 15:31:42 +0200
committerSonarTech <sonartech@sonarsource.com>2018-05-03 20:20:50 +0200
commit4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6 (patch)
treebdb0ff5255e953a466c67629e214f6f0e544fee3
parent4c2edb7abdb283c6bed56ce6be32304f67529045 (diff)
downloadsonarqube-4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6.tar.gz
sonarqube-4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6.zip
SONAR-10609 Rework the global help modal to a dropdown (#177)
-rw-r--r--server/sonar-web/.eslintrc6
-rw-r--r--server/sonar-web/package.json2
-rw-r--r--server/sonar-web/public/images/embed-doc/google-group-icon.svg1
-rw-r--r--server/sonar-web/public/images/embed-doc/sc-icon.svg1
-rw-r--r--server/sonar-web/public/images/embed-doc/so-icon.svg1
-rw-r--r--server/sonar-web/public/images/embed-doc/sq-icon.svg1
-rw-r--r--server/sonar-web/public/images/embed-doc/twitter-icon.svg1
-rw-r--r--server/sonar-web/scripts/start.js3
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx38
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx159
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx95
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json11
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx53
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts (renamed from server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts)13
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx84
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx (renamed from server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js)33
-rw-r--r--server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap151
-rw-r--r--server/sonar-web/src/main/js/app/components/help/GlobalHelp.js118
-rw-r--r--server/sonar-web/src/main/js/app/components/help/LinksHelp.js69
-rw-r--r--server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js65
-rw-r--r--server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js135
-rw-r--r--server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js70
-rw-r--r--server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap69
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx47
-rw-r--r--server/sonar-web/src/main/js/app/theme.js1
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/components/common/BubblePopup.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx3
-rw-r--r--server/sonar-web/src/main/js/typings/json.d.ts23
-rw-r--r--server/sonar-web/yarn.lock6
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties12
33 files changed, 663 insertions, 637 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc
index f3c47f9df45..801383c08c7 100644
--- a/server/sonar-web/.eslintrc
+++ b/server/sonar-web/.eslintrc
@@ -1,3 +1,7 @@
{
- "extends": "sonarqube"
+ "extends": "sonarqube",
+
+ "rules": {
+ "import/extensions": ["error", "never", { "json": "always" }]
+ }
}
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index a8b3f522569..69537bc59a5 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -103,7 +103,7 @@
"style-loader": "0.20.3",
"ts-jest": "22.0.1",
"ts-loader": "4.1.0",
- "typescript": "2.8.1",
+ "typescript": "2.8.3",
"typescript-eslint-parser": "15.0.0",
"webpack": "4.1.1",
"webpack-bundle-analyzer": "2.11.1",
diff --git a/server/sonar-web/public/images/embed-doc/google-group-icon.svg b/server/sonar-web/public/images/embed-doc/google-group-icon.svg
new file mode 100644
index 00000000000..fd581b716c0
--- /dev/null
+++ b/server/sonar-web/public/images/embed-doc/google-group-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M.563 0L.562 10.125H9l3.937 3.375V0H.562zm6.188 2.25a1.13 1.13 0 0 1 1.125 1.125A1.13 1.13 0 0 1 6.751 4.5a1.13 1.13 0 0 1-1.125-1.125A1.13 1.13 0 0 1 6.751 2.25zM3.938 6.917v.396H2.251v-.247c0-.208.395-.808 1.406-.808.18 0 .337.024.477.057-.127.22-.196.433-.196.602zm-.281-1.292a.848.848 0 0 1-.844-.844c0-.463.381-.844.844-.844.463 0 .844.381.844.844a.848.848 0 0 1-.844.844zm5.344 1.687h-4.5v-.395c0-.325.633-1.292 2.25-1.292s2.25.958 2.25 1.292v.395zm0-2.531c0-.463.381-.844.844-.844.463 0 .843.381.843.844 0 .463-.38.844-.843.844a.848.848 0 0 1-.844-.844zm2.25 2.531H9.563v-.395c0-.169-.068-.382-.195-.602.14-.033.297-.057.477-.057 1.011 0 1.406.598 1.406.809v.245z" fill="#4169e1" fill-rule="nonzero"/></svg> \ No newline at end of file
diff --git a/server/sonar-web/public/images/embed-doc/sc-icon.svg b/server/sonar-web/public/images/embed-doc/sc-icon.svg
new file mode 100644
index 00000000000..bc3d84e95d0
--- /dev/null
+++ b/server/sonar-web/public/images/embed-doc/sc-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M12.625 6.154a3.991 3.991 0 0 0-1.902-1.279v-.046C10.723 2.65 8.93.857 6.75.857c-2.179 0-3.972 1.793-3.972 3.972v.053A3.982 3.982 0 0 0 0 8.671c0 2.179 1.793 3.972 3.972 3.972a3.978 3.978 0 0 0 2.791-1.144 3.97 3.97 0 0 0 2.766 1.122c2.178 0 3.971-1.793 3.971-3.972 0-.905-.31-1.784-.877-2.489l.002-.006zm-3.073 5.489a2.99 2.99 0 0 1-2.973-2.971c0-.275-.225-.5-.5-.5a.501.501 0 0 0-.499.5 3.952 3.952 0 0 0 .56 2.032 2.971 2.971 0 0 1-2.164.936 2.985 2.985 0 0 1-2.97-2.97 2.985 2.985 0 0 1 2.97-2.971c.35 0 .697.062 1.026.183h.012c.114.038.223.09.324.155a.5.5 0 1 0 .65-.759 2.224 2.224 0 0 0-.646-.341 3.974 3.974 0 0 0-1.369-.243h-.192A2.985 2.985 0 0 1 6.75 1.85a2.986 2.986 0 0 1 2.972 2.972c0 .96-.466 1.863-1.249 2.42a.5.5 0 1 0 .58.814 3.983 3.983 0 0 0 1.526-2.184 2.979 2.979 0 0 1 1.941 2.789 2.986 2.986 0 0 1-2.969 2.972l.001.01z" fill="#f3702a" fill-rule="nonzero"/></svg> \ No newline at end of file
diff --git a/server/sonar-web/public/images/embed-doc/so-icon.svg b/server/sonar-web/public/images/embed-doc/so-icon.svg
new file mode 100644
index 00000000000..12ba4acd52c
--- /dev/null
+++ b/server/sonar-web/public/images/embed-doc/so-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><g fill-rule="nonzero"><path d="M10.686 12.3V8.683h1.201V13.5H1.052V8.683h1.201V12.3h8.433z" fill="#bcbbbb"/><path d="M3.578 8.34L9.47 9.572l.25-1.185-5.893-1.232-.249 1.185zm.779-2.806l5.456 2.541.499-1.091-5.456-2.557-.499 1.107zm1.512-2.681l4.63 3.85.764-.92-4.63-3.85-.764.92zM8.862 0l-.966.717 3.585 4.833.967-.717L8.862 0zM3.453 11.084H9.47V9.883H3.453v1.201z" fill="#f48023"/></g></svg> \ No newline at end of file
diff --git a/server/sonar-web/public/images/embed-doc/sq-icon.svg b/server/sonar-web/public/images/embed-doc/sq-icon.svg
new file mode 100644
index 00000000000..da6e96895ed
--- /dev/null
+++ b/server/sonar-web/public/images/embed-doc/sq-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M11.939 13.282h-.756C11.183 7.203 6.167 2.256 0 2.256v-.755c6.585 0 11.939 5.286 11.939 11.781zm.522-4.053c-.906-3.816-3.997-7-7.872-8.111l.173-.603c4.09 1.174 7.353 4.538 8.311 8.57l-.612.144zm.581-3.575A11.572 11.572 0 0 0 8.562.648l.261-.43a12.081 12.081 0 0 1 4.677 5.22l-.458.216z" fill="#4e9bcd" fill-rule="nonzero"/></svg> \ No newline at end of file
diff --git a/server/sonar-web/public/images/embed-doc/twitter-icon.svg b/server/sonar-web/public/images/embed-doc/twitter-icon.svg
new file mode 100644
index 00000000000..55f5f705251
--- /dev/null
+++ b/server/sonar-web/public/images/embed-doc/twitter-icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M13.5 2.57a5.52 5.52 0 0 1-1.593.428 2.685 2.685 0 0 0 1.216-1.524 5.431 5.431 0 0 1-1.756.668 2.666 2.666 0 0 0-2.022-.874c-.765 0-1.417.27-1.957.809a2.67 2.67 0 0 0-.809 1.958c0 .205.023.417.068.634a7.724 7.724 0 0 1-3.182-.853A7.84 7.84 0 0 1 .942 1.773a2.713 2.713 0 0 0-.377 1.396c0 .474.112.914.334 1.32.223.405.523.733.9.985a2.746 2.746 0 0 1-1.251-.352v.035c0 .668.21 1.254.63 1.76.42.506.949.824 1.589.955a2.846 2.846 0 0 1-.728.094c-.16 0-.334-.014-.523-.042a2.71 2.71 0 0 0 .977 1.366c.474.357 1.01.541 1.61.552a5.417 5.417 0 0 1-3.435 1.182c-.245 0-.468-.011-.668-.034a7.684 7.684 0 0 0 4.249 1.242c.982 0 1.904-.155 2.766-.467.863-.311 1.599-.728 2.21-1.25a8.407 8.407 0 0 0 1.581-1.803c.442-.68.772-1.39.989-2.129A7.86 7.86 0 0 0 12.112 4 5.805 5.805 0 0 0 13.5 2.57z" fill="#4d9bce" fill-rule="nonzero"/></svg> \ No newline at end of file
diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js
index dfb765d1b65..cef0a40eaab 100644
--- a/server/sonar-web/scripts/start.js
+++ b/server/sonar-web/scripts/start.js
@@ -26,9 +26,9 @@ const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const errorOverlayMiddleware = require('react-error-overlay/middleware');
+const getMessages = require('./utils/getMessages');
const getConfig = require('../config/webpack.config');
const paths = require('../config/paths');
-const getMessages = require('./utils/getMessages');
const config = getConfig({ production: false });
@@ -107,7 +107,6 @@ function runDevServer(compiler, host, port, protocol) {
proxy: {
'/api': proxy,
'/fonts': proxy,
- '/images': proxy,
'/static': proxy,
'/integration': proxy
}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
index a89319b690a..3b81367b34f 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
@@ -22,6 +22,7 @@ import * as PropTypes from 'prop-types';
import GlobalNav from './nav/global/GlobalNav';
import GlobalFooterContainer from './GlobalFooterContainer';
import GlobalMessagesContainer from './GlobalMessagesContainer';
+import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider';
import Workspace from '../../components/workspace/Workspace';
interface Props {
@@ -59,23 +60,28 @@ export default class GlobalContainer extends React.PureComponent<Props, State> {
// it is important to pass `location` down to `GlobalNav` to trigger render on url change
return (
- <div className="global-container">
- <div className="page-wrapper" id="container">
- <div className="page-container">
- <Workspace>
- <GlobalNav
- closeOnboardingTutorial={this.closeOnboardingTutorial}
- isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
- location={this.props.location}
- openOnboardingTutorial={this.openOnboardingTutorial}
- />
- <GlobalMessagesContainer />
- {this.props.children}
- </Workspace>
+ <SuggestionsProvider>
+ {({ suggestions }) => (
+ <div className="global-container">
+ <div className="page-wrapper" id="container">
+ <div className="page-container">
+ <Workspace>
+ <GlobalNav
+ closeOnboardingTutorial={this.closeOnboardingTutorial}
+ isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
+ location={this.props.location}
+ openOnboardingTutorial={this.openOnboardingTutorial}
+ suggestions={suggestions}
+ />
+ <GlobalMessagesContainer />
+ {this.props.children}
+ </Workspace>
+ </div>
+ </div>
+ <GlobalFooterContainer />
</div>
- </div>
- <GlobalFooterContainer />
- </div>
+ )}
+ </SuggestionsProvider>
);
}
}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
new file mode 100644
index 00000000000..4a75361d7e5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 * as PropTypes from 'prop-types';
+import { Link } from 'react-router';
+import { SuggestionLink } from './SuggestionsProvider';
+import BubblePopup, { BubblePopupPosition } from '../../../components/common/BubblePopup';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ onClose: () => void;
+ popupPosition?: BubblePopupPosition;
+ suggestions: Array<SuggestionLink>;
+}
+
+export default class EmbedDocsPopup extends React.PureComponent<Props> {
+ static contextTypes = {
+ onSonarCloud: PropTypes.bool,
+ openOnboardingTutorial: PropTypes.func
+ };
+
+ onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.context.openOnboardingTutorial();
+ };
+
+ renderTitle(text: string) {
+ return <li className="dropdown-header">{text}</li>;
+ }
+
+ renderSuggestions() {
+ if (this.props.suggestions.length === 0) {
+ return null;
+ }
+ return (
+ <>
+ {this.renderTitle(translate('embed_docs.suggestion'))}
+ {this.props.suggestions.map((suggestion, index) => (
+ <li key={index}>
+ <a href={suggestion.link} target="_blank">
+ {suggestion.text} <i className="icon-detach" />
+ </a>
+ </li>
+ ))}
+ <li className="divider" />
+ </>
+ );
+ }
+
+ renderIconLink(link: string, icon: string, text: string) {
+ return (
+ <a href={link} rel="noopener noreferrer" target="_blank">
+ <img
+ alt={text}
+ className="spacer-right"
+ height="18"
+ src={'/images/embed-doc/' + icon}
+ width="18"
+ />
+ {text}
+ </a>
+ );
+ }
+
+ renderSonarCloudLinks() {
+ return (
+ <React.Fragment>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.get_support'))}
+ <li>
+ {this.renderIconLink(
+ 'https://about.sonarcloud.io/contact/',
+ 'sc-icon.svg',
+ translate('embed_docs.contact_form')
+ )}
+ </li>
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink('https://about.sonarcloud.io/news/', 'sc-icon.svg', 'Product News')}
+ </li>
+ <li>
+ {this.renderIconLink('https://twitter.com/sonarcloud', 'twitter-icon.svg', 'Twitter')}
+ </li>
+ </React.Fragment>
+ );
+ }
+
+ renderSonarQubeLinks() {
+ return (
+ <React.Fragment>
+ <li>
+ <a href="#" onClick={this.onAnalyzeProjectClick}>
+ {translate('embed_docs.analyze_new_project')}
+ </a>
+ </li>
+ <li className="divider" />
+ {this.renderTitle(translate('embed_docs.get_support'))}
+ <li>
+ {this.renderIconLink(
+ 'https://groups.google.com/forum/#!forum/sonarqube',
+ 'google-group-icon.svg',
+ 'Google Groups'
+ )}
+ </li>
+ <li>
+ {this.renderIconLink(
+ 'http://stackoverflow.com/questions/tagged/sonarqube',
+ 'so-icon.svg',
+ 'Stack Overflow'
+ )}
+ </li>
+ {this.renderTitle(translate('embed_docs.stay_connected'))}
+ <li>
+ {this.renderIconLink('https://blog.sonarsource.com/', 'sq-icon.svg', 'Product News')}
+ </li>
+ <li>
+ {this.renderIconLink('https://twitter.com/SonarQube', 'twitter-icon.svg', 'Twitter')}
+ </li>
+ </React.Fragment>
+ );
+ }
+
+ render() {
+ return (
+ <BubblePopup
+ customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup"
+ position={this.props.popupPosition}>
+ <ul className="menu">
+ {this.renderSuggestions()}
+ <li>
+ <Link target="_blank" to="/documentation">
+ {translate('embed_docs.documentation_index')} <i className="icon-detach" />
+ </Link>
+ </li>
+ {this.context.onSonarCloud && this.renderSonarCloudLinks()}
+ {!this.context.onSonarCloud && this.renderSonarQubeLinks()}
+ </ul>
+ </BubblePopup>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
new file mode 100644
index 00000000000..aca948c6aa9
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopupHelper.tsx
@@ -0,0 +1,95 @@
+/*
+ * 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 EmbedDocsPopup from './EmbedDocsPopup';
+import { SuggestionLink } from './SuggestionsProvider';
+import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
+import HelpIcon from '../../../components/icons-components/HelpIcon';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ showTooltip: boolean;
+ suggestions: Array<SuggestionLink>;
+ tooltip: boolean;
+}
+interface State {
+ helpOpen: boolean;
+}
+
+export default class EmbedDocsPopupHelper extends React.PureComponent<Props, State> {
+ state: State = { helpOpen: false };
+
+ componentDidMount() {
+ window.addEventListener('keypress', this.onKeyPress);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('keypress', this.onKeyPress);
+ }
+
+ onKeyPress = (event: KeyboardEvent) => {
+ const { tagName } = event.target as HTMLElement;
+ const code = event.keyCode || event.which;
+ const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
+ const isTriggerKey = code === 63;
+ if (!isInput && isTriggerKey) {
+ this.toggleHelp();
+ }
+ };
+
+ setHelpDisplay = (helpOpen: boolean) => {
+ this.setState({ helpOpen });
+ };
+
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ this.toggleHelp();
+ };
+
+ toggleHelp = () => {
+ this.setState(state => {
+ return { helpOpen: !state.helpOpen };
+ });
+ };
+
+ closeHelp = () => {
+ this.setState({ helpOpen: false });
+ };
+
+ render() {
+ return (
+ <BubblePopupHelper
+ isOpen={this.state.helpOpen}
+ offset={{ horizontal: 12, vertical: -10 }}
+ popup={<EmbedDocsPopup onClose={this.closeHelp} suggestions={this.props.suggestions} />}
+ position="bottomleft"
+ togglePopup={this.setHelpDisplay}>
+ <Tooltip
+ overlay={this.props.tooltip ? translate('tutorials.follow_later') : undefined}
+ visible={this.props.showTooltip}>
+ <a className="navbar-help" href="#" onClick={this.handleClick}>
+ <HelpIcon />
+ </a>
+ </Tooltip>
+ </BubblePopupHelper>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json
new file mode 100644
index 00000000000..ae1ce94f6fd
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsSuggestions.json
@@ -0,0 +1,11 @@
+{
+ "projects": [
+ { "link": "#", "text": "Foo Suggestion " },
+ { "link": "#", "text": "Bar Suggestion " }
+ ],
+ "profiles": [
+ { "link": "#", "text": "Foo Suggestion " },
+ { "link": "#", "text": "Bar Suggestion " },
+ { "link": "#", "text": "Baz Suggestion " }
+ ]
+}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx
new file mode 100644
index 00000000000..7ffd50b40cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/Suggestions.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 * as PropTypes from 'prop-types';
+import { SuggestionsContext } from './SuggestionsContext';
+
+interface Props {
+ suggestions: string;
+}
+
+export default class Suggestions extends React.PureComponent<Props> {
+ context!: { suggestions: SuggestionsContext };
+
+ static contextTypes = {
+ suggestions: PropTypes.object.isRequired
+ };
+
+ componentDidMount() {
+ this.context.suggestions.addSuggestions(this.props.suggestions);
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.suggestions !== this.props.suggestions) {
+ this.context.suggestions.removeSuggestions(this.props.suggestions);
+ this.context.suggestions.addSuggestions(prevProps.suggestions);
+ }
+ }
+
+ componentWillUnmount() {
+ this.context.suggestions.removeSuggestions(this.props.suggestions);
+ }
+
+ render() {
+ return null;
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts
index 45dcbde901d..8292383c5db 100644
--- a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsContext.ts
@@ -17,14 +17,7 @@
* 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 { CurrentUser, AppState } from '../../types';
-
-export interface Props {
- currentUser: CurrentUser;
- onClose: () => void;
- onSonarCloud?: boolean;
- onTutorialSelect: () => void;
+export interface SuggestionsContext {
+ addSuggestions: (key: string) => void;
+ removeSuggestions: (key: string) => void;
}
-
-export default class GlobalHelp extends React.PureComponent<Props> {}
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
new file mode 100644
index 00000000000..833c382e066
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/SuggestionsProvider.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 * as PropTypes from 'prop-types';
+import * as suggestionsJson from './EmbedDocsSuggestions.json';
+import { SuggestionsContext } from './SuggestionsContext';
+
+export interface SuggestionLink {
+ text: string;
+ link: string;
+}
+
+interface SuggestionsJson {
+ [key: string]: Array<SuggestionLink>;
+}
+
+interface Props {
+ children: ({ suggestions }: { suggestions: Array<SuggestionLink> }) => React.ReactNode;
+}
+
+interface State {
+ suggestions: Array<SuggestionLink>;
+}
+
+export default class SuggestionsProvider extends React.Component<Props, State> {
+ keys: string[] = [];
+
+ static childContextTypes = {
+ suggestions: PropTypes.object
+ };
+
+ state = { suggestions: [] };
+
+ getChildContext = (): { suggestions: SuggestionsContext } => {
+ return {
+ suggestions: {
+ addSuggestions: this.addSuggestions,
+ removeSuggestions: this.removeSuggestions
+ }
+ };
+ };
+
+ fetchSuggestions = () => {
+ const jsonList = suggestionsJson as SuggestionsJson;
+ let suggestions: Array<SuggestionLink> = [];
+ this.keys.forEach(key => {
+ if (jsonList[key]) {
+ suggestions = [...jsonList[key], ...suggestions];
+ }
+ });
+ this.setState({ suggestions });
+ };
+
+ addSuggestions = (newKey: string) => {
+ this.keys = [...this.keys, newKey];
+ this.fetchSuggestions();
+ };
+
+ removeSuggestions = (oldKey: string) => {
+ this.keys = this.keys.filter(key => key !== oldKey);
+ this.fetchSuggestions();
+ };
+
+ render() {
+ return this.props.children({ suggestions: this.state.suggestions });
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
index 1bc62190778..647715c847a 100644
--- a/server/sonar-web/src/main/js/app/components/help/TutorialsHelp.js
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/EmbedDocsPopup-test.tsx
@@ -17,26 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import EmbedDocsPopups from '../EmbedDocsPopup';
-/*::
-type Props = { onTutorialSelect: () => void };
-*/
+const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }];
-export default function TutorialsHelp({ onTutorialSelect } /*: Props */) {
- const handleClick = (event /*: Event */) => {
- event.preventDefault();
- onTutorialSelect();
- };
-
- return (
- <div>
- <h2 className="spacer-top spacer-bottom">{translate('help.section.tutorials')}</h2>
- <a href="#" onClick={handleClick}>
- {translate('tutorials.onboarding')}
- </a>
- </div>
- );
-}
+it('should display suggestion links', () => {
+ const context = {};
+ const wrapper = shallow(<EmbedDocsPopups onClose={jest.fn()} suggestions={suggestions} />, {
+ context
+ });
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
new file mode 100644
index 00000000000..e6405633094
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/EmbedDocsPopup-test.tsx.snap
@@ -0,0 +1,151 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display suggestion links 1`] = `
+<BubblePopup
+ customClass="bubble-popup-bottom bubble-popup-menu abs-width-240 embed-docs-popup"
+>
+ <ul
+ className="menu"
+ >
+ <React.Fragment>
+ <li
+ className="dropdown-header"
+ >
+ embed_docs.suggestion
+ </li>
+ <li
+ key="0"
+ >
+ <a
+ href="#"
+ target="_blank"
+ >
+ foo
+
+ <i
+ className="icon-detach"
+ />
+ </a>
+ </li>
+ <li
+ key="1"
+ >
+ <a
+ href="#"
+ target="_blank"
+ >
+ bar
+
+ <i
+ className="icon-detach"
+ />
+ </a>
+ </li>
+ <li
+ className="divider"
+ />
+ </React.Fragment>
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ target="_blank"
+ to="/documentation"
+ >
+ embed_docs.documentation_index
+
+ <i
+ className="icon-detach"
+ />
+ </Link>
+ </li>
+ <React.Fragment>
+ <li>
+ <a
+ href="#"
+ onClick={[Function]}
+ >
+ embed_docs.analyze_new_project
+ </a>
+ </li>
+ <li
+ className="divider"
+ />
+ <li
+ className="dropdown-header"
+ >
+ embed_docs.get_support
+ </li>
+ <li>
+ <a
+ href="https://groups.google.com/forum/#!forum/sonarqube"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <img
+ alt="Google Groups"
+ className="spacer-right"
+ height="18"
+ src="/images/embed-doc/google-group-icon.svg"
+ width="18"
+ />
+ Google Groups
+ </a>
+ </li>
+ <li>
+ <a
+ href="http://stackoverflow.com/questions/tagged/sonarqube"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <img
+ alt="Stack Overflow"
+ className="spacer-right"
+ height="18"
+ src="/images/embed-doc/so-icon.svg"
+ width="18"
+ />
+ Stack Overflow
+ </a>
+ </li>
+ <li
+ className="dropdown-header"
+ >
+ embed_docs.stay_connected
+ </li>
+ <li>
+ <a
+ href="https://blog.sonarsource.com/"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <img
+ alt="Product News"
+ className="spacer-right"
+ height="18"
+ src="/images/embed-doc/sq-icon.svg"
+ width="18"
+ />
+ Product News
+ </a>
+ </li>
+ <li>
+ <a
+ href="https://twitter.com/SonarQube"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <img
+ alt="Twitter"
+ className="spacer-right"
+ height="18"
+ src="/images/embed-doc/twitter-icon.svg"
+ width="18"
+ />
+ Twitter
+ </a>
+ </li>
+ </React.Fragment>
+ </ul>
+</BubblePopup>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
deleted file mode 100644
index 0b9af453a7b..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import LinksHelp from './LinksHelp';
-import LinksHelpSonarCloud from './LinksHelpSonarCloud';
-import ShortcutsHelp from './ShortcutsHelp';
-import TutorialsHelp from './TutorialsHelp';
-import Modal from '../../../components/controls/Modal';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
- currentUser: { isLoggedIn: boolean },
- onClose: () => void,
- onTutorialSelect: () => void,
- onSonarCloud?: boolean
-};
-*/
-
-/*::
-type State = {
- section: string
-};
-*/
-
-export default class GlobalHelp extends React.PureComponent {
- /*:: props: Props; */
- state /*: State */ = { section: 'shortcuts' };
-
- handleCloseClick = (event /*: Event */) => {
- event.preventDefault();
- this.props.onClose();
- };
-
- handleSectionClick = (event /*: Event & { currentTarget: HTMLElement } */) => {
- event.preventDefault();
- const { section } = event.currentTarget.dataset;
- this.setState({ section });
- };
-
- renderSection = () => {
- switch (this.state.section) {
- case 'shortcuts':
- return <ShortcutsHelp />;
- case 'links':
- return this.props.onSonarCloud ? (
- <LinksHelpSonarCloud onClose={this.props.onClose} />
- ) : (
- <LinksHelp onClose={this.props.onClose} />
- );
- case 'tutorials':
- return <TutorialsHelp onTutorialSelect={this.props.onTutorialSelect} />;
- default:
- return null;
- }
- };
-
- renderMenuItem = (section /*: string */) => (
- <li key={section}>
- <a
- className={classNames({ active: section === this.state.section })}
- data-section={section}
- href="#"
- onClick={this.handleSectionClick}>
- {translate('help.section', section)}
- </a>
- </li>
- );
-
- renderMenu = () => (
- <ul className="side-tabs-menu">
- {(this.props.currentUser.isLoggedIn && !this.props.onSonarCloud
- ? ['shortcuts', 'tutorials', 'links']
- : ['shortcuts', 'links']
- ).map(this.renderMenuItem)}
- </ul>
- );
-
- render() {
- return (
- <Modal contentLabel={translate('help')} medium={true} onRequestClose={this.props.onClose}>
- <div className="modal-head">
- <h2>{translate('help')}</h2>
- </div>
-
- <div className="side-tabs-layout">
- <div className="side-tabs-side">{this.renderMenu()}</div>
- <div className="side-tabs-main">{this.renderSection()}</div>
- </div>
-
- <div className="modal-foot">
- <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
- {translate('close')}
- </a>
- </div>
- </Modal>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelp.js b/server/sonar-web/src/main/js/app/components/help/LinksHelp.js
deleted file mode 100644
index d363c8493d3..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/LinksHelp.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = { onClose: () => void };
-*/
-
-export default function LinksHelp({ onClose } /*: Props */) {
- return (
- <div>
- <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2>
-
- <p className="spacer-bottom">
- <a href="http://www.sonarqube.org">{translate('footer.community')}</a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://redirect.sonarsource.com/doc/home.html">
- {translate('footer.documentation')}
- </a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://redirect.sonarsource.com/doc/community.html">
- {translate('footer.support')}
- </a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://redirect.sonarsource.com/doc/plugin-library.html">
- {translate('footer.plugins')}
- </a>
- </p>
-
- <p className="spacer-bottom">
- <Link to="/web_api" onClick={onClose}>
- {translate('footer.web_api')}
- </Link>
- </p>
-
- <p>
- <Link to="/about" onClick={onClose}>
- {translate('footer.about')}
- </Link>
- </p>
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js b/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js
deleted file mode 100644
index ce276efea8d..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/LinksHelpSonarCloud.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { Link } from 'react-router';
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = { onClose: () => void };
-*/
-
-export default function LinksHelpSonarCloud({ onClose } /*: Props */) {
- return (
- <div>
- <h2 className="spacer-top spacer-bottom">{translate('help.section.links')}</h2>
-
- <p className="spacer-bottom">
- <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
- </p>
-
- <p className="spacer-bottom">
- <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
- </p>
-
- <p className="spacer-bottom">
- <Link to="/web_api" onClick={onClose}>
- {translate('footer.web_api')}
- </Link>
- </p>
-
- <p>
- <a href="https://about.sonarcloud.io/">{translate('footer.about')}</a>
- </p>
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js b/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js
deleted file mode 100644
index 40ce74f4c23..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/ShortcutsHelp.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-export default function ShortcutsHelp() {
- return (
- <div>
- <h2 className="spacer-top spacer-bottom">{translate('help.section.shortcuts')}</h2>
-
- <div className="columns">
- <div className="column-half">
- <div className="spacer-bottom">
- <h3 className="shortcuts-section-title">{translate('shortcuts.section.global')}</h3>
- <ul className="shortcuts-list">
- <li>
- <span className="shortcut-button spacer-right">s</span>
- {translate('shortcuts.section.global.search')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">?</span>
- {translate('shortcuts.section.global.shortcuts')}
- </li>
- </ul>
- </div>
-
- <h3 className="shortcuts-section-title">{translate('shortcuts.section.rules')}</h3>
- <ul className="shortcuts-list">
- <li>
- <span className="shortcut-button little-spacer-right">↑</span>
- <span className="shortcut-button spacer-right">↓</span>
- {translate('shortcuts.section.rules.navigate_between_rules')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">→</span>
- {translate('shortcuts.section.rules.open_details')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">←</span>
- {translate('shortcuts.section.rules.return_to_list')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">a</span>
- {translate('shortcuts.section.rules.activate')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">d</span>
- {translate('shortcuts.section.rules.deactivate')}
- </li>
- </ul>
- </div>
-
- <div className="column-half">
- <h3 className="shortcuts-section-title">{translate('shortcuts.section.issues')}</h3>
- <ul className="shortcuts-list">
- <li>
- <span className="shortcut-button little-spacer-right">↑</span>
- <span className="shortcut-button spacer-right">↓</span>
- {translate('shortcuts.section.issues.navigate_between_issues')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">→</span>
- {translate('shortcuts.section.issues.open_details')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">←</span>
- {translate('shortcuts.section.issues.return_to_list')}
- </li>
- <li>
- <span className="shortcut-button little-spacer-right">alt</span>
- <span className="little-spacer-right">+</span>
- <span className="shortcut-button little-spacer-right">↑</span>
- <span className="shortcut-button spacer-right">↓</span>
- {translate('issues.to_navigate_issue_locations')}
- </li>
- <li>
- <span className="shortcut-button little-spacer-right">alt</span>
- <span className="little-spacer-right">+</span>
- <span className="shortcut-button little-spacer-right">←</span>
- <span className="shortcut-button spacer-right">→</span>
- {translate('issues.to_switch_flows')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">f</span>
- {translate('shortcuts.section.issue.do_transition')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">a</span>
- {translate('shortcuts.section.issue.assign')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">m</span>
- {translate('shortcuts.section.issue.assign_to_me')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">i</span>
- {translate('shortcuts.section.issue.change_severity')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">c</span>
- {translate('shortcuts.section.issue.comment')}
- </li>
- <li>
- <span className="shortcut-button little-spacer-right">ctrl</span>
- <span className="shortcut-button spacer-right">enter</span>
- {translate('shortcuts.section.issue.submit_comment')}
- </li>
- <li>
- <span className="shortcut-button spacer-right">t</span>
- {translate('shortcuts.section.issue.change_tags')}
- </li>
- </ul>
- </div>
- </div>
- </div>
- );
-}
diff --git a/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js b/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js
deleted file mode 100644
index bac69262c54..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/__tests__/GlobalHelp-test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import React from 'react';
-import { shallow } from 'enzyme';
-import GlobalHelp from '../GlobalHelp';
-import { click } from '../../../../helpers/testUtils';
-
-it('switches between tabs', () => {
- const wrapper = shallow(
- <GlobalHelp
- currentUser={{ isLoggedIn: true }}
- onClose={jest.fn()}
- onTutorialSelect={jest.fn()}
- />
- );
- expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
- clickOnSection(wrapper, 'links');
- expect(wrapper.find('LinksHelp')).toHaveLength(1);
- clickOnSection(wrapper, 'tutorials');
- expect(wrapper.find('TutorialsHelp')).toHaveLength(1);
- clickOnSection(wrapper, 'shortcuts');
- expect(wrapper.find('ShortcutsHelp')).toHaveLength(1);
-});
-
-it('does not show tutorials for anonymous', () => {
- expect(
- shallow(
- <GlobalHelp
- currentUser={{ isLoggedIn: false }}
- onClose={jest.fn()}
- onTutorialSelect={jest.fn()}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('display special links page for SonarCloud', () => {
- const wrapper = shallow(
- <GlobalHelp
- currentUser={{ isLoggedIn: false }}
- onClose={jest.fn()}
- onTutorialSelect={jest.fn()}
- onSonarCloud={true}
- />
- );
- clickOnSection(wrapper, 'links');
- expect(wrapper.find('LinksHelpSonarCloud')).toHaveLength(1);
-});
-
-function clickOnSection(wrapper /*: Object */, section /*: string */) {
- click(wrapper.find(`[data-section="${section}"]`), { currentTarget: { dataset: { section } } });
-}
diff --git a/server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap b/server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap
deleted file mode 100644
index 972930cce48..00000000000
--- a/server/sonar-web/src/main/js/app/components/help/__tests__/__snapshots__/GlobalHelp-test.js.snap
+++ /dev/null
@@ -1,69 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`does not show tutorials for anonymous 1`] = `
-<Modal
- contentLabel="help"
- medium={true}
- onRequestClose={[MockFunction]}
->
- <div
- className="modal-head"
- >
- <h2>
- help
- </h2>
- </div>
- <div
- className="side-tabs-layout"
- >
- <div
- className="side-tabs-side"
- >
- <ul
- className="side-tabs-menu"
- >
- <li
- key="shortcuts"
- >
- <a
- className="active"
- data-section="shortcuts"
- href="#"
- onClick={[Function]}
- >
- help.section.shortcuts
- </a>
- </li>
- <li
- key="links"
- >
- <a
- className=""
- data-section="links"
- href="#"
- onClick={[Function]}
- >
- help.section.links
- </a>
- </li>
- </ul>
- </div>
- <div
- className="side-tabs-main"
- >
- <ShortcutsHelp />
- </div>
- </div>
- <div
- className="modal-foot"
- >
- <a
- className="js-modal-close"
- href="#"
- onClick={[Function]}
- >
- close
- </a>
- </div>
-</Modal>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
index 84d8cde8305..b91c43abb47 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
@@ -25,16 +25,16 @@ import GlobalNavExplore from './GlobalNavExplore';
import GlobalNavUserContainer from './GlobalNavUserContainer';
import GlobalNavPlus from './GlobalNavPlus';
import Search from '../../search/Search';
-import GlobalHelp from '../../help/GlobalHelp';
+import EmbedDocsPopupHelper from '../../embed-docs-modal/EmbedDocsPopupHelper';
import * as theme from '../../../theme';
import { isLoggedIn, CurrentUser, AppState } from '../../../types';
import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
import NavBar from '../../../../components/nav/NavBar';
import Tooltip from '../../../../components/controls/Tooltip';
-import HelpIcon from '../../../../components/icons-components/HelpIcon';
import { translate } from '../../../../helpers/l10n';
import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer';
import { skipOnboarding } from '../../../../store/users/actions';
+import { SuggestionLink } from '../../embed-docs-modal/SuggestionsProvider';
import './GlobalNav.css';
interface StateProps {
@@ -52,6 +52,7 @@ interface Props extends StateProps, DispatchProps {
isOnboardingTutorialOpen: boolean;
location: { pathname: string };
openOnboardingTutorial: () => void;
+ suggestions: Array<SuggestionLink>;
}
interface State {
@@ -64,7 +65,6 @@ class GlobalNav extends React.PureComponent<Props, State> {
state: State = { helpOpen: false, onboardingTutorialTooltip: false };
componentDidMount() {
- window.addEventListener('keypress', this.onKeyPress);
if (this.props.currentUser.showOnboardingTutorial) {
this.openOnboardingTutorial();
}
@@ -74,28 +74,8 @@ class GlobalNav extends React.PureComponent<Props, State> {
if (this.interval) {
clearInterval(this.interval);
}
- window.removeEventListener('keypress', this.onKeyPress);
}
- onKeyPress = (event: KeyboardEvent) => {
- const { tagName } = event.target as HTMLElement;
- const code = event.keyCode || event.which;
- const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
- const isTriggerKey = code === 63;
- if (!isInput && isTriggerKey) {
- this.openHelp();
- }
- };
-
- handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
- event.preventDefault();
- this.openHelp();
- };
-
- openHelp = () => this.setState({ helpOpen: true });
-
- closeHelp = () => this.setState({ helpOpen: false });
-
openOnboardingTutorial = () => {
this.setState({ helpOpen: false });
this.props.openOnboardingTutorial();
@@ -120,13 +100,11 @@ class GlobalNav extends React.PureComponent<Props, State> {
<ul className="global-navbar-menu pull-right">
<GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} />
<li>
- <Tooltip
- overlay={this.props.onSonarCloud ? undefined : translate('tutorials.follow_later')}
- visible={this.state.onboardingTutorialTooltip}>
- <a className="navbar-help" href="#" onClick={this.handleHelpClick}>
- <HelpIcon />
- </a>
- </Tooltip>
+ <EmbedDocsPopupHelper
+ showTooltip={this.state.onboardingTutorialTooltip}
+ suggestions={this.props.suggestions}
+ tooltip={!this.props.onSonarCloud}
+ />
</li>
<Search appState={this.props.appState} currentUser={this.props.currentUser} />
{isLoggedIn(this.props.currentUser) &&
@@ -140,15 +118,6 @@ class GlobalNav extends React.PureComponent<Props, State> {
<GlobalNavUserContainer {...this.props} />
</ul>
- {this.state.helpOpen && (
- <GlobalHelp
- currentUser={this.props.currentUser}
- onClose={this.closeHelp}
- onSonarCloud={this.props.onSonarCloud}
- onTutorialSelect={this.openOnboardingTutorial}
- />
- )}
-
{this.props.isOnboardingTutorialOpen && (
<OnboardingModal onFinish={this.closeOnboardingTutorial} />
)}
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 37ff59c71bd..59808a135ba 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -59,6 +59,7 @@ module.exports = {
gridSize: `${grid}px`,
baseFontSize: '13px',
+ verySmallFontSize: '10px',
smallFontSize: '12px',
mediumFontSize: '14px',
bigFontSize: '16px',
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
index cd9547a11b4..2f164bcc170 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
@@ -24,6 +24,7 @@ import { omitBy } from 'lodash';
import PageHeader from './PageHeader';
import ProjectsList from './ProjectsList';
import PageSidebar from './PageSidebar';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import Visualizations from '../visualizations/Visualizations';
import { CurrentUser, isLoggedIn } from '../../../app/types';
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
@@ -317,6 +318,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
render() {
return (
<div className="layout-page projects-page" id="projects-page">
+ <Suggestions suggestions="projects" />
<Helmet title={translate('projects.page')} />
{this.renderSide()}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
index e3fac302fb4..b6339e6c3fb 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
@@ -19,7 +19,7 @@
*/
/* eslint-disable import/order */
import * as React from 'react';
-import { mount, shallow } from 'enzyme';
+import { shallow } from 'enzyme';
import AllProjects, { Props } from '../AllProjects';
import { get, save } from '../../../../helpers/storage';
@@ -71,7 +71,7 @@ it('renders', () => {
});
it('fetches projects', () => {
- mountRender();
+ shallowRender();
expect(fetchProjects).lastCalledWith(
{
coverage: undefined,
@@ -104,7 +104,7 @@ it('redirects to the saved search', () => {
(key: string) => (key === 'sonarqube.projects.view' ? 'leak' : null)
);
const replace = jest.fn();
- mountRender({}, jest.fn(), replace);
+ shallowRender({}, jest.fn(), replace);
expect(replace).lastCalledWith({ pathname: '/projects', query: { view: 'leak' } });
});
@@ -161,19 +161,6 @@ it('changes perspective to risk visualization', () => {
expect(save).toHaveBeenCalledWith('sonarqube.projects.visualization', 'risk', undefined);
});
-function mountRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) {
- return mount(
- <AllProjects
- currentUser={{ isLoggedIn: true }}
- fetchProjects={jest.fn()}
- isFavorite={false}
- location={{ pathname: '/projects', query: {} }}
- {...props}
- />,
- { context: { router: { push, replace } } }
- );
-}
-
function shallowRender(
props: Partial<Props> = {},
push: Function = jest.fn(),
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
index 3dd798e4094..ec7df22da87 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
@@ -5,6 +5,9 @@ exports[`renders 1`] = `
className="layout-page projects-page"
id="projects-page"
>
+ <Suggestions
+ suggestions="projects"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -134,6 +137,9 @@ exports[`renders 2`] = `
className="layout-page projects-page"
id="projects-page"
>
+ <Suggestions
+ suggestions="projects"
+ />
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
diff --git a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx
index 7020ee57490..e23a99b7e25 100644
--- a/server/sonar-web/src/main/js/components/common/BubblePopup.tsx
+++ b/server/sonar-web/src/main/js/components/common/BubblePopup.tsx
@@ -29,7 +29,7 @@ export interface BubblePopupPosition {
interface Props {
customClass?: string;
children: React.ReactNode;
- position: BubblePopupPosition;
+ position?: BubblePopupPosition;
}
/**
diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
index 627dad9cde5..9f66d022cc9 100644
--- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
+++ b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx
@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import { BubblePopupPosition } from './BubblePopup';
interface Props {
className?: string;
@@ -31,7 +32,7 @@ interface Props {
}
interface State {
- position: { top: number; left?: number; right?: number };
+ position: BubblePopupPosition;
}
export default class BubblePopupHelper extends React.PureComponent<Props, State> {
diff --git a/server/sonar-web/src/main/js/typings/json.d.ts b/server/sonar-web/src/main/js/typings/json.d.ts
new file mode 100644
index 00000000000..0f858f5d256
--- /dev/null
+++ b/server/sonar-web/src/main/js/typings/json.d.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+declare module '*.json' {
+ const value: any;
+ export default value;
+}
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index d9881ef214d..7bbd7883247 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -8290,9 +8290,9 @@ typescript-eslint-parser@15.0.0:
lodash.unescape "4.0.1"
semver "5.5.0"
-typescript@2.8.1:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.1.tgz#6160e4f8f195d5ba81d4876f9c0cc1fbc0820624"
+typescript@2.8.3:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.3.tgz#5d817f9b6f31bb871835f4edf0089f21abe6c170"
ua-parser-js@^0.7.9:
version "0.7.17"
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 331c0b74f45..696cafb5d08 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2547,6 +2547,18 @@ organization.change_visibility_form.submit=Change Default Visibility
#------------------------------------------------------------------------------
#
+# EMBEDED DOCS
+#
+#------------------------------------------------------------------------------
+embed_docs.suggestion=Suggestions For This Page
+embed_docs.documentation_index=Documentation index
+embed_docs.get_support=Get Support
+embed_docs.stay_connected=Stay Connected
+embed_docs.contact_form=Contact form
+embed_docs.analyze_new_project=Analyze new project
+
+#------------------------------------------------------------------------------
+#
# GLOBAL FOOTER
#
#------------------------------------------------------------------------------