diff options
author | Pascal Mugnier <pascal.mugnier@sonarsource.com> | 2018-04-26 15:31:42 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-05-03 20:20:50 +0200 |
commit | 4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6 (patch) | |
tree | bdb0ff5255e953a466c67629e214f6f0e544fee3 | |
parent | 4c2edb7abdb283c6bed56ce6be32304f67529045 (diff) | |
download | sonarqube-4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6.tar.gz sonarqube-4a31f1ff8f39bdd5189d75f420ab052ec5a75cd6.zip |
SONAR-10609 Rework the global help modal to a dropdown (#177)
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 # #------------------------------------------------------------------------------ |