diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-12-12 14:37:19 +0100 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-01-02 10:38:10 +0100 |
commit | 82d363f3bc37245ea92c77b5c1b0ea418f91dbb5 (patch) | |
tree | 156a7596df64b9198df7ecc7046b9bf580d3777a /server/sonar-web/src/main | |
parent | 0d3c531f4a6ef72a1f0982808543273760ea05ba (diff) | |
download | sonarqube-82d363f3bc37245ea92c77b5c1b0ea418f91dbb5.tar.gz sonarqube-82d363f3bc37245ea92c77b5c1b0ea418f91dbb5.zip |
SONAR-10187 Provide more options to populate empty "My Projects" page
Diffstat (limited to 'server/sonar-web/src/main')
41 files changed, 1078 insertions, 804 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx new file mode 100644 index 00000000000..cbab4678fd5 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 GlobalNav from './nav/global/GlobalNav'; +import GlobalFooterContainer from './GlobalFooterContainer'; +import GlobalMessagesContainer from './GlobalMessagesContainer'; + +interface Props { + children: React.ReactNode; + location: { pathname: string }; +} + +interface State { + isOnboardingTutorialOpen: boolean; +} + +export default class GlobalContainer extends React.PureComponent<Props, State> { + static childContextTypes = { + closeOnboardingTutorial: PropTypes.func, + openOnboardingTutorial: PropTypes.func + }; + + constructor(props: Props) { + super(props); + this.state = { isOnboardingTutorialOpen: false }; + } + + getChildContext() { + return { + closeOnboardingTutorial: this.closeOnboardingTutorial, + openOnboardingTutorial: this.openOnboardingTutorial + }; + } + + openOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: true }); + + closeOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: false }); + + render() { + // 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"> + <GlobalNav + closeOnboardingTutorial={this.closeOnboardingTutorial} + isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen} + location={this.props.location} + openOnboardingTutorial={this.openOnboardingTutorial} + /> + <GlobalMessagesContainer /> + {this.props.children} + </div> + </div> + <GlobalFooterContainer /> + </div> + ); + } +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.js b/server/sonar-web/src/main/js/app/components/GlobalFooter.js deleted file mode 100644 index 6cd008827fa..00000000000 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 GlobalFooterSonarCloud from './GlobalFooterSonarCloud'; -import GlobalFooterBranding from './GlobalFooterBranding'; -import { translate, translateWithParameters } from '../../helpers/l10n'; - -/*:: -type Props = { - hideLoggedInInfo?: boolean, - productionDatabase: boolean, - onSonarCloud?: { value: string }, - sonarqubeVersion?: string -}; -*/ - -export default function GlobalFooter( - { hideLoggedInInfo, productionDatabase, onSonarCloud, sonarqubeVersion } /*: Props */ -) { - if (onSonarCloud && onSonarCloud.value === 'true') { - return <GlobalFooterSonarCloud hideLoggedInInfo={hideLoggedInInfo} />; - } - - return ( - <div id="footer" className="page-footer page-container"> - {productionDatabase === false && ( - <div className="alert alert-danger"> - <p className="big" id="evaluation_warning"> - {translate('footer.production_database_warning')} - </p> - <p>{translate('footer.production_database_explanation')}</p> - </div> - )} - - <GlobalFooterBranding /> - - <div> - {!hideLoggedInInfo && - sonarqubeVersion && - translateWithParameters('footer.version_x', sonarqubeVersion)} - {!hideLoggedInInfo && sonarqubeVersion && ' - '} - <a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.licence')}</a> - {' - '} - <a href="http://www.sonarqube.org">{translate('footer.community')}</a> - {' - '} - <a href="https://redirect.sonarsource.com/doc/home.html"> - {translate('footer.documentation')} - </a> - {' - '} - <a href="https://redirect.sonarsource.com/doc/community.html"> - {translate('footer.support')} - </a> - {' - '} - <a href="https://redirect.sonarsource.com/doc/plugin-library.html"> - {translate('footer.plugins')} - </a> - {!hideLoggedInInfo && ' - '} - {!hideLoggedInInfo && <Link to="/web_api">{translate('footer.web_api')}</Link>} - {!hideLoggedInInfo && ' - '} - {!hideLoggedInInfo && <Link to="/about">{translate('footer.about')}</Link>} - </div> - </div> - ); -} diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx new file mode 100644 index 00000000000..39422c1e761 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -0,0 +1,97 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 { Link } from 'react-router'; +import GlobalFooterSonarCloud from './GlobalFooterSonarCloud'; +import GlobalFooterBranding from './GlobalFooterBranding'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +interface Props { + hideLoggedInInfo?: boolean; + productionDatabase: boolean; + onSonarCloud?: { value: string }; + sonarqubeVersion?: string; +} + +export default function GlobalFooter({ + hideLoggedInInfo, + productionDatabase, + onSonarCloud, + sonarqubeVersion +}: Props) { + if (onSonarCloud && onSonarCloud.value === 'true') { + return <GlobalFooterSonarCloud />; + } + + return ( + <div id="footer" className="page-footer page-container"> + {productionDatabase === false && ( + <div className="alert alert-danger"> + <p className="big" id="evaluation_warning"> + {translate('footer.production_database_warning')} + </p> + <p>{translate('footer.production_database_explanation')}</p> + </div> + )} + + <GlobalFooterBranding /> + + <ul className="page-footer-menu"> + {!hideLoggedInInfo && + sonarqubeVersion && ( + <li className="page-footer-menu-item"> + {translateWithParameters('footer.version_x', sonarqubeVersion)} + </li> + )} + <li className="page-footer-menu-item"> + <a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.license')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="http://www.sonarqube.org">{translate('footer.community')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://redirect.sonarsource.com/doc/home.html"> + {translate('footer.documentation')} + </a> + </li> + <li className="page-footer-menu-item"> + <a href="https://redirect.sonarsource.com/doc/community.html"> + {translate('footer.support')} + </a> + </li> + <li className="page-footer-menu-item"> + <a href="https://redirect.sonarsource.com/doc/plugin-library.html"> + {translate('footer.plugins')} + </a> + </li> + {!hideLoggedInInfo && ( + <li className="page-footer-menu-item"> + <Link to="/web_api">{translate('footer.web_api')}</Link> + </li> + )} + {!hideLoggedInInfo && ( + <li className="page-footer-menu-item"> + <Link to="/about">{translate('footer.about')}</Link> + </li> + )} + </ul> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx index e9b31253cdd..1e85753db06 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx @@ -21,7 +21,13 @@ import { connect } from 'react-redux'; import { getAppState, getGlobalSettingValue } from '../../store/rootReducer'; import GlobalFooter from './GlobalFooter'; -const mapStateToProps = (state: any) => ({ +interface StateProps { + onSonarCloud?: { value: string }; + productionDatabase: boolean; + sonarqubeVersion?: string; +} + +const mapStateToProps = (state: any): StateProps => ({ sonarqubeVersion: getAppState(state).version, productionDatabase: getAppState(state).productionDatabase, onSonarCloud: getGlobalSettingValue(state, 'sonar.sonarcloud.enabled') diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js b/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx index 9c601855952..1ccd6d5c9ff 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js +++ b/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx @@ -17,8 +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. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { translate } from '../../helpers/l10n'; export default function GlobalFooterSonarCloud() { @@ -32,19 +31,26 @@ export default function GlobalFooterSonarCloud() { . All rights reserved. </div> - <div> - <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a> - {' - '} - <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a> - {' - '} - <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a> - {' - '} - <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a> - {' - '} - <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a> - {' - '} - <a href="https://about.sonarcloud.io/">{translate('footer.about')}</a> - </div> + <ul className="page-footer-menu"> + <li className="page-footer-menu-item"> + <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a> + </li> + <li className="page-footer-menu-item"> + <a href="https://about.sonarcloud.io/">{translate('footer.about')}</a> + </li> + </ul> </div> ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx index 74ef097213f..f8325090a85 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; import GlobalFooter from '../GlobalFooter'; it('should render the only logged in information', () => { diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx index 2f41d408c61..98644558082 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as React from 'react'; import { shallow } from 'enzyme'; -import React from 'react'; import GlobalFooterSonarCloud from '../GlobalFooterSonarCloud'; it('should render correctly', () => { diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap deleted file mode 100644 index 438127ce0bc..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap +++ /dev/null @@ -1,173 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display the sq version 1`] = ` -<div - className="page-footer page-container" - id="footer" -> - <GlobalFooterBranding /> - <div> - footer.version_x.6.4-SNAPSHOT - - - <a - href="http://www.gnu.org/licenses/lgpl-3.0.txt" - > - footer.licence - </a> - - - <a - href="http://www.sonarqube.org" - > - footer.community - </a> - - - <a - href="https://redirect.sonarsource.com/doc/home.html" - > - footer.documentation - </a> - - - <a - href="https://redirect.sonarsource.com/doc/community.html" - > - footer.support - </a> - - - <a - href="https://redirect.sonarsource.com/doc/plugin-library.html" - > - footer.plugins - </a> - - - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/web_api" - > - footer.web_api - </Link> - - - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/about" - > - footer.about - </Link> - </div> -</div> -`; - -exports[`should not render the only logged in information 1`] = ` -<div - className="page-footer page-container" - id="footer" -> - <GlobalFooterBranding /> - <div> - <a - href="http://www.gnu.org/licenses/lgpl-3.0.txt" - > - footer.licence - </a> - - - <a - href="http://www.sonarqube.org" - > - footer.community - </a> - - - <a - href="https://redirect.sonarsource.com/doc/home.html" - > - footer.documentation - </a> - - - <a - href="https://redirect.sonarsource.com/doc/community.html" - > - footer.support - </a> - - - <a - href="https://redirect.sonarsource.com/doc/plugin-library.html" - > - footer.plugins - </a> - </div> -</div> -`; - -exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`; - -exports[`should render the only logged in information 1`] = ` -<div - className="page-footer page-container" - id="footer" -> - <GlobalFooterBranding /> - <div> - <a - href="http://www.gnu.org/licenses/lgpl-3.0.txt" - > - footer.licence - </a> - - - <a - href="http://www.sonarqube.org" - > - footer.community - </a> - - - <a - href="https://redirect.sonarsource.com/doc/home.html" - > - footer.documentation - </a> - - - <a - href="https://redirect.sonarsource.com/doc/community.html" - > - footer.support - </a> - - - <a - href="https://redirect.sonarsource.com/doc/plugin-library.html" - > - footer.plugins - </a> - - - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/web_api" - > - footer.web_api - </Link> - - - <Link - onlyActiveOnIndex={false} - style={Object {}} - to="/about" - > - footer.about - </Link> - </div> -</div> -`; - -exports[`should show the db warning message 1`] = ` -<div - className="alert alert-danger" -> - <p - className="big" - id="evaluation_warning" - > - footer.production_database_warning - </p> - <p> - footer.production_database_explanation - </p> -</div> -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap new file mode 100644 index 00000000000..b7069f5b1cb --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap @@ -0,0 +1,242 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should display the sq version 1`] = ` +<div + className="page-footer page-container" + id="footer" +> + <GlobalFooterBranding /> + <ul + className="page-footer-menu" + > + <li + className="page-footer-menu-item" + > + footer.version_x.6.4-SNAPSHOT + </li> + <li + className="page-footer-menu-item" + > + <a + href="http://www.gnu.org/licenses/lgpl-3.0.txt" + > + footer.license + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="http://www.sonarqube.org" + > + footer.community + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/home.html" + > + footer.documentation + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/community.html" + > + footer.support + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/plugin-library.html" + > + footer.plugins + </a> + </li> + <li + className="page-footer-menu-item" + > + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/web_api" + > + footer.web_api + </Link> + </li> + <li + className="page-footer-menu-item" + > + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/about" + > + footer.about + </Link> + </li> + </ul> +</div> +`; + +exports[`should not render the only logged in information 1`] = ` +<div + className="page-footer page-container" + id="footer" +> + <GlobalFooterBranding /> + <ul + className="page-footer-menu" + > + <li + className="page-footer-menu-item" + > + <a + href="http://www.gnu.org/licenses/lgpl-3.0.txt" + > + footer.license + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="http://www.sonarqube.org" + > + footer.community + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/home.html" + > + footer.documentation + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/community.html" + > + footer.support + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/plugin-library.html" + > + footer.plugins + </a> + </li> + </ul> +</div> +`; + +exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`; + +exports[`should render the only logged in information 1`] = ` +<div + className="page-footer page-container" + id="footer" +> + <GlobalFooterBranding /> + <ul + className="page-footer-menu" + > + <li + className="page-footer-menu-item" + > + <a + href="http://www.gnu.org/licenses/lgpl-3.0.txt" + > + footer.license + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="http://www.sonarqube.org" + > + footer.community + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/home.html" + > + footer.documentation + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/community.html" + > + footer.support + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://redirect.sonarsource.com/doc/plugin-library.html" + > + footer.plugins + </a> + </li> + <li + className="page-footer-menu-item" + > + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/web_api" + > + footer.web_api + </Link> + </li> + <li + className="page-footer-menu-item" + > + <Link + onlyActiveOnIndex={false} + style={Object {}} + to="/about" + > + footer.about + </Link> + </li> + </ul> +</div> +`; + +exports[`should show the db warning message 1`] = ` +<div + className="alert alert-danger" +> + <p + className="big" + id="evaluation_warning" + > + footer.production_database_warning + </p> + <p> + footer.production_database_explanation + </p> +</div> +`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap deleted file mode 100644 index 0a3aa1953b1..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<div - className="page-footer page-container" - id="footer" -> - <div> - © 2008-2017, SonarCloud.io by - - <a - href="http://www.sonarsource.com" - title="SonarSource SA" - > - SonarSource SA - </a> - . All rights reserved. - </div> - <div> - <a - href="https://about.sonarcloud.io/news/" - > - footer.news - </a> - - - <a - href="https://about.sonarcloud.io/terms.pdf" - > - footer.terms - </a> - - - <a - href="https://twitter.com/sonarqube" - > - footer.twitter - </a> - - - <a - href="https://about.sonarcloud.io/get-started/" - > - footer.get_started - </a> - - - <a - href="https://about.sonarcloud.io/contact/" - > - footer.help - </a> - - - <a - href="https://about.sonarcloud.io/" - > - footer.about - </a> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap new file mode 100644 index 00000000000..f7a6f4b0228 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="page-footer page-container" + id="footer" +> + <div> + © 2008-2017, SonarCloud.io by + + <a + href="http://www.sonarsource.com" + title="SonarSource SA" + > + SonarSource SA + </a> + . All rights reserved. + </div> + <ul + className="page-footer-menu" + > + <li + className="page-footer-menu-item" + > + <a + href="https://about.sonarcloud.io/news/" + > + footer.news + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://about.sonarcloud.io/terms.pdf" + > + footer.terms + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://twitter.com/sonarqube" + > + footer.twitter + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://about.sonarcloud.io/get-started/" + > + footer.get_started + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://about.sonarcloud.io/contact/" + > + footer.help + </a> + </li> + <li + className="page-footer-menu-item" + > + <a + href="https://about.sonarcloud.io/" + > + footer.about + </a> + </li> + </ul> +</div> +`; diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts new file mode 100644 index 00000000000..ea8fb503279 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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 { CurrentUser, AppState } from '../../types'; + +export interface Props { + currentUser: CurrentUser; + onClose: () => void; + onSonarCloud?: boolean; + onTutorialSelect: () => void; +} + +export default class GlobalHelp extends React.PureComponent<Props> {} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 564775c0179..04404a1fe1e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -17,8 +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. */ -// @flow -import React from 'react'; +import * as React from 'react'; import { connect } from 'react-redux'; import GlobalNavBranding from './GlobalNavBranding'; import GlobalNavMenu from './GlobalNavMenu'; @@ -28,42 +27,41 @@ import GlobalNavPlus from './GlobalNavPlus'; import Search from '../../search/Search'; import GlobalHelp from '../../help/GlobalHelp'; import * as theme from '../../../theme'; -import { isLoggedIn } from '../../../types'; +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 OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal'; +import { translate } from '../../../../helpers/l10n'; import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer'; import { skipOnboarding } from '../../../../store/users/actions'; -import { translate } from '../../../../helpers/l10n'; import './GlobalNav.css'; -/*:: -type Props = { - appState: { organizationsEnabled: boolean }, - currentUser: { isLoggedIn: boolean, showOnboardingTutorial: boolean }, - location: { pathname: string }, - skipOnboarding: () => void, - onSonarCloud: boolean -}; -*/ +interface StateProps { + appState: AppState; + currentUser: CurrentUser; + onSonarCloud: boolean; +} -/*:: -type State = { - helpOpen: boolean, - onboardingTutorialOpen: boolean, - onboardingTutorialTooltip: boolean -}; -*/ - -class GlobalNav extends React.PureComponent { - /*:: interval: ?number; */ - /*:: props: Props; */ - state /*: State */ = { - helpOpen: false, - onboardingTutorialOpen: false, - onboardingTutorialTooltip: false - }; +interface DispatchProps { + skipOnboarding: () => void; +} + +interface Props extends StateProps, DispatchProps { + closeOnboardingTutorial: () => void; + isOnboardingTutorialOpen: boolean; + location: { pathname: string }; + openOnboardingTutorial: () => void; +} + +interface State { + helpOpen: boolean; + onboardingTutorialTooltip: boolean; +} + +class GlobalNav extends React.PureComponent<Props, State> { + interval?: number; + state: State = { helpOpen: false, onboardingTutorialTooltip: false }; componentDidMount() { window.addEventListener('keypress', this.onKeyPress); @@ -79,9 +77,9 @@ class GlobalNav extends React.PureComponent { window.removeEventListener('keypress', this.onKeyPress); } - onKeyPress = e => { - const tagName = e.target.tagName; - const code = e.keyCode || e.which; + 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) { @@ -89,7 +87,7 @@ class GlobalNav extends React.PureComponent { } }; - handleHelpClick = event => { + handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); this.openHelp(); }; @@ -98,12 +96,16 @@ class GlobalNav extends React.PureComponent { closeHelp = () => this.setState({ helpOpen: false }); - openOnboardingTutorial = () => this.setState({ helpOpen: false, onboardingTutorialOpen: true }); + openOnboardingTutorial = () => { + this.setState({ helpOpen: false }); + this.props.openOnboardingTutorial(); + }; closeOnboardingTutorial = () => { - this.setState({ onboardingTutorialOpen: false, onboardingTutorialTooltip: true }); + this.setState({ onboardingTutorialTooltip: true }); this.props.skipOnboarding(); - this.interval = setInterval(() => { + this.props.closeOnboardingTutorial(); + this.interval = window.setInterval(() => { this.setState({ onboardingTutorialTooltip: false }); }, 3000); }; @@ -148,7 +150,7 @@ class GlobalNav extends React.PureComponent { /> )} - {this.state.onboardingTutorialOpen && ( + {this.props.isOnboardingTutorialOpen && ( <OnboardingModal onFinish={this.closeOnboardingTutorial} /> )} </NavBar> @@ -156,7 +158,7 @@ class GlobalNav extends React.PureComponent { } } -const mapStateToProps = state => { +const mapStateToProps = (state: any): StateProps => { const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); return { @@ -166,6 +168,6 @@ const mapStateToProps = state => { }; }; -const mapDispatchToProps = { skipOnboarding }; +const mapDispatchToProps: DispatchProps = { skipOnboarding }; export default connect(mapStateToProps, mapDispatchToProps)(GlobalNav); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index 7cea4c635b2..50277e52187 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -17,31 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { Link } from 'react-router'; -import { isLoggedIn } from '../../../../app/types'; +import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../../app/types'; import { translate } from '../../../../helpers/l10n'; -import { getQualityGatesUrl } from '../../../../helpers/urls'; +import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls'; import { isMySet } from '../../../../apps/issues/utils'; -export default class GlobalNavMenu extends React.PureComponent { - static propTypes = { - appState: PropTypes.object.isRequired, - currentUser: PropTypes.object.isRequired, - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired - }).isRequired, - onSonarCloud: PropTypes.bool - }; - - static defaultProps = { - globalDashboards: [], - globalPages: [] - }; +interface Props { + appState: AppState; + currentUser: CurrentUser; + location: { pathname: string }; + onSonarCloud: boolean; +} - activeLink(url) { - return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null; +export default class GlobalNavMenu extends React.PureComponent<Props> { + activeLink(url: string) { + return window.location.pathname.indexOf(getBaseUrl() + url) === 0 ? 'active' : undefined; } renderProjects() { @@ -144,7 +136,7 @@ export default class GlobalNavMenu extends React.PureComponent { ); } - renderGlobalPageLink = ({ key, name }) => { + renderGlobalPageLink = ({ key, name }: Extension) => { return ( <li key={key}> <Link to={`/extension/${key}`}>{name}</Link> @@ -153,7 +145,7 @@ export default class GlobalNavMenu extends React.PureComponent { }; renderMore() { - const { globalPages } = this.props.appState; + const { globalPages = [] } = this.props.appState; const withoutPortfolios = globalPages.filter(page => page.key !== 'governance/portfolios'); if (withoutPortfolios.length === 0) { return null; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 1649a5e370e..fb19ba06d40 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -25,10 +25,10 @@ import { Link } from 'react-router'; import * as theme from '../../../theme'; import { CurrentUser, LoggedInUser, isLoggedIn, Organization } from '../../../types'; import Avatar from '../../../../components/ui/Avatar'; -import OrganizationLink from '../../../../components/ui/OrganizationLink'; +import OrganizationListItem from '../../../../components/ui/OrganizationListItem'; import { translate } from '../../../../helpers/l10n'; import { getBaseUrl } from '../../../../helpers/urls'; -import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; +import Dropdown from '../../../../components/controls/Dropdown'; interface Props { appState: { organizationsEnabled: boolean }; @@ -36,32 +36,11 @@ interface Props { organizations: Organization[]; } -interface State { - open: boolean; -} - -export default class GlobalNavUser extends React.PureComponent<Props, State> { - node?: HTMLElement | null; - +export default class GlobalNavUser extends React.PureComponent<Props> { static contextTypes = { router: PropTypes.object }; - constructor(props: Props) { - super(props); - this.state = { open: false }; - } - - componentWillUnmount() { - window.removeEventListener('click', this.handleClickOutside); - } - - handleClickOutside = (event: MouseEvent) => { - if (!this.node || !this.node.contains(event.target as Node)) { - this.closeDropdown(); - } - }; - handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`; @@ -76,98 +55,61 @@ export default class GlobalNavUser extends React.PureComponent<Props, State> { handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); - this.closeDropdown(); this.context.router.push('/sessions/logout'); }; - toggleDropdown = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - if (this.state.open) { - this.closeDropdown(); - } else { - this.openDropdown(); - } - }; - - openDropdown = () => { - window.addEventListener('click', this.handleClickOutside, true); - this.setState({ open: true }); - }; - - closeDropdown = () => { - window.removeEventListener('click', this.handleClickOutside); - this.setState({ open: false }); - }; - renderAuthenticated() { const { organizations } = this.props; const currentUser = this.props.currentUser as LoggedInUser; const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; return ( - <li - className={classNames('dropdown js-user-authenticated', { open: this.state.open })} - ref={node => (this.node = node)}> - <a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}> - <Avatar - hash={currentUser.avatar} - name={currentUser.name} - size={theme.globalNavContentHeightRaw} - /> - </a> - {this.state.open && ( - <ul className="dropdown-menu dropdown-menu-right"> - <li className="dropdown-item"> - <div className="text-ellipsis text-muted" title={currentUser.name}> - <strong>{currentUser.name}</strong> - </div> - {currentUser.email != null && ( - <div - className="little-spacer-top text-ellipsis text-muted" - title={currentUser.email}> - {currentUser.email} + <Dropdown> + {({ onToggleClick, open }) => ( + <li className={classNames('dropdown', 'js-user-authenticated', { open })}> + <a className="dropdown-toggle navbar-avatar" href="#" onClick={onToggleClick}> + <Avatar + hash={currentUser.avatar} + name={currentUser.name} + size={theme.globalNavContentHeightRaw} + /> + </a> + <ul className="dropdown-menu dropdown-menu-right"> + <li className="dropdown-item"> + <div className="text-ellipsis text-muted" title={currentUser.name}> + <strong>{currentUser.name}</strong> </div> - )} - </li> - <li className="divider" /> - <li> - <Link to="/account" onClick={this.closeDropdown}> - {translate('my_account.page')} - </Link> - </li> - {hasOrganizations && <li role="separator" className="divider" />} - {hasOrganizations && ( + {currentUser.email != null && ( + <div + className="little-spacer-top text-ellipsis text-muted" + title={currentUser.email}> + {currentUser.email} + </div> + )} + </li> + <li className="divider" /> <li> - <Link to="/account/organizations" onClick={this.closeDropdown}> - {translate('my_organizations')} - </Link> + <Link to="/account">{translate('my_account.page')}</Link> </li> - )} - {hasOrganizations && - sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( - <li key={organization.key}> - <OrganizationLink - className="dropdown-item-flex" - organization={organization} - onClick={this.closeDropdown}> - <div> - <OrganizationAvatar organization={organization} small={true} /> - <span className="spacer-left">{organization.name}</span> - </div> - {organization.isAdmin && ( - <span className="outline-badge spacer-left">{translate('admin')}</span> - )} - </OrganizationLink> + {hasOrganizations && <li role="separator" className="divider" />} + {hasOrganizations && ( + <li> + <Link to="/account/organizations">{translate('my_organizations')}</Link> </li> - ))} - {hasOrganizations && <li role="separator" className="divider" />} - <li> - <a onClick={this.handleLogout} href="#"> - {translate('layout.logout')} - </a> - </li> - </ul> + )} + {hasOrganizations && + sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + {hasOrganizations && <li role="separator" className="divider" />} + <li> + <a onClick={this.handleLogout} href="#"> + {translate('layout.logout')} + </a> + </li> + </ul> + </li> )} - </li> + </Dropdown> ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index 659fe6def16..dbec4b97992 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => { <GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} /> ); wrapper.setState({ open: true }); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); }); it('should render user organizations', () => { @@ -51,7 +51,7 @@ it('should render user organizations', () => { <GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} /> ); wrapper.setState({ open: true }); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); }); it('should not render user organizations when they are not activated', () => { @@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => { /> ); wrapper.setState({ open: true }); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('Dropdown').dive()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap index 673cc003e3c..34dd8f91396 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap @@ -32,7 +32,6 @@ exports[`should show administration menu if the user has the rights 1`] = ` </li> <li> <Link - className={null} onlyActiveOnIndex={false} style={Object {}} to="/coding_rules" @@ -109,7 +108,6 @@ exports[`should work with extensions 1`] = ` </li> <li> <Link - className={null} onlyActiveOnIndex={false} style={Object {}} to="/coding_rules" diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap index 23fef6f2d99..378367bce15 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should not render user organizations when they are not activated 1`] = ` <li - className="dropdown js-user-authenticated open" + className="dropdown js-user-authenticated" > <a className="dropdown-toggle navbar-avatar" @@ -41,7 +41,6 @@ exports[`should not render user organizations when they are not activated 1`] = /> <li> <Link - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} to="/account" @@ -75,7 +74,7 @@ exports[`should render the right interface for anonymous user 1`] = ` exports[`should render the right interface for logged in user 1`] = ` <li - className="dropdown js-user-authenticated open" + className="dropdown js-user-authenticated" > <a className="dropdown-toggle navbar-avatar" @@ -114,7 +113,6 @@ exports[`should render the right interface for logged in user 1`] = ` /> <li> <Link - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} to="/account" @@ -136,7 +134,7 @@ exports[`should render the right interface for logged in user 1`] = ` exports[`should render user organizations 1`] = ` <li - className="dropdown js-user-authenticated open" + className="dropdown js-user-authenticated" > <a className="dropdown-toggle navbar-avatar" @@ -175,7 +173,6 @@ exports[`should render user organizations 1`] = ` /> <li> <Link - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} to="/account" @@ -189,7 +186,6 @@ exports[`should render user organizations 1`] = ` /> <li> <Link - onClick={[Function]} onlyActiveOnIndex={false} style={Object {}} to="/account/organizations" @@ -197,105 +193,36 @@ exports[`should render user organizations 1`] = ` my_organizations </Link> </li> - <li + <OrganizationListItem key="bar" - > - <OrganizationLink - className="dropdown-item-flex" - onClick={[Function]} - organization={ - Object { - "key": "bar", - "name": "bar", - "projectVisibility": "public", - } + organization={ + Object { + "key": "bar", + "name": "bar", + "projectVisibility": "public", } - > - <div> - <OrganizationAvatar - organization={ - Object { - "key": "bar", - "name": "bar", - "projectVisibility": "public", - } - } - small={true} - /> - <span - className="spacer-left" - > - bar - </span> - </div> - </OrganizationLink> - </li> - <li + } + /> + <OrganizationListItem key="foo" - > - <OrganizationLink - className="dropdown-item-flex" - onClick={[Function]} - organization={ - Object { - "key": "foo", - "name": "Foo", - "projectVisibility": "public", - } + organization={ + Object { + "key": "foo", + "name": "Foo", + "projectVisibility": "public", } - > - <div> - <OrganizationAvatar - organization={ - Object { - "key": "foo", - "name": "Foo", - "projectVisibility": "public", - } - } - small={true} - /> - <span - className="spacer-left" - > - Foo - </span> - </div> - </OrganizationLink> - </li> - <li + } + /> + <OrganizationListItem key="myorg" - > - <OrganizationLink - className="dropdown-item-flex" - onClick={[Function]} - organization={ - Object { - "key": "myorg", - "name": "MyOrg", - "projectVisibility": "public", - } + organization={ + Object { + "key": "myorg", + "name": "MyOrg", + "projectVisibility": "public", } - > - <div> - <OrganizationAvatar - organization={ - Object { - "key": "myorg", - "name": "MyOrg", - "projectVisibility": "public", - } - } - small={true} - /> - <span - className="spacer-left" - > - MyOrg - </span> - </div> - </OrganizationLink> - </li> + } + /> <li className="divider" role="separator" diff --git a/server/sonar-web/src/main/js/app/components/search/Search.d.ts b/server/sonar-web/src/main/js/app/components/search/Search.d.ts new file mode 100644 index 00000000000..4aa6e4ea685 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/search/Search.d.ts @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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 { CurrentUser, AppState } from '../../types'; + +export interface Props { + appState: AppState; + currentUser: CurrentUser; +} + +export default class Search extends React.PureComponent<Props> {} diff --git a/server/sonar-web/src/main/js/app/styles/components/page.css b/server/sonar-web/src/main/js/app/styles/components/page.css index a1a7d2b8610..53855c6747f 100644 --- a/server/sonar-web/src/main/js/app/styles/components/page.css +++ b/server/sonar-web/src/main/js/app/styles/components/page.css @@ -142,16 +142,8 @@ .page-footer a:hover, .page-footer a:active, .page-footer a:focus { - color: var(--blue); -} - -.page-footer a:hover { - border-bottom-color: var(--lightBlue); -} - -.page-footer a:active, -.page-footer a:focus { border-bottom-color: var(--lightBlue); + color: var(--blue); } .page-footer-with-sidebar { @@ -162,6 +154,16 @@ max-width: 980px; } +.page-footer-menu-item { + display: inline-block; +} + +.page-footer-menu-item + .page-footer-menu-item::before { + content: '-'; + padding: 0 calc(0.5 * var(--gridSize)); + user-select: none; +} + .page-with-sidebar { display: flex; } diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css index a9a610f6632..97287e21ac0 100644 --- a/server/sonar-web/src/main/js/app/styles/init/forms.css +++ b/server/sonar-web/src/main/js/app/styles/init/forms.css @@ -118,7 +118,7 @@ input[type='button'] { display: inline-block; vertical-align: baseline; height: var(--controlHeight); - line-height: 22px; + line-height: calc(var(--controlHeight) - 2px); padding: 0 12px; border: 1px solid var(--darkBlue); border-radius: 2px; @@ -184,6 +184,10 @@ input[type='button']:disabled:focus { box-shadow: none; } +.button svg { + padding-top: calc((var(--controlHeight) - 16px - 2px) / 2); +} + .button-red, input[type='submit'].button-red { border-color: var(--red); diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index af3c21a26db..2b2bc7a64a8 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -150,3 +150,13 @@ export interface LoggedInUser extends CurrentUser { export function isLoggedIn(user: CurrentUser): user is LoggedInUser { return user.isLoggedIn; } + +export interface AppState { + adminPages?: Extension[]; + authenticationError: boolean; + authorizationError: boolean; + canAdmin?: boolean; + globalPages?: Extension[]; + organizationsEnabled: boolean; + qualifiers: string[]; +} diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx index 1bded7bb20f..0f8b57127a7 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx @@ -17,61 +17,44 @@ * 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 * as React from 'react'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; import OrganizationLink from '../../../components/ui/OrganizationLink'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ import { translate } from '../../../helpers/l10n'; +import { Organization } from '../../../app/types'; -/*:: -type Props = { - organization: Organization -}; -*/ - -export default function OrganizationCard(props /*: Props */) { - const { organization } = props; +interface Props { + organization: Organization; +} +export default function OrganizationCard({ organization }: Props) { return ( <div className="account-project-card clearfix"> - <aside className="account-project-side"> - {!!organization.avatar && ( - <div className="spacer-bottom"> - <img src={organization.avatar} height={30} alt={organization.name} /> - </div> - )} - {!!organization.url && ( - <div className="text-limited text-top spacer-bottom"> - <a className="small" href={organization.url} title={organization.url} rel="nofollow"> - {organization.url} - </a> - </div> - )} + <aside className="account-project-side note"> + <strong>{translate('organization.key')}:</strong> {organization.key} </aside> <h3 className="account-project-name"> - <OrganizationLink organization={organization}>{organization.name}</OrganizationLink> + <OrganizationAvatar organization={organization} /> + <OrganizationLink className="spacer-left text-middle" organization={organization}> + {organization.name} + </OrganizationLink> {organization.isAdmin && ( <span className="outline-badge spacer-left">{translate('admin')}</span> )} </h3> {!!organization.description && ( - <div className="account-project-description">{organization.description}</div> + <div className="markdown spacer-top">{organization.description}</div> )} - <div className="account-project-key"> - <span className="little-spacer-right"> - {translate('key')} - {':'} - </span> - <input - onClick={event => event.currentTarget.select()} - readOnly={true} - type="text" - value={organization.key} - /> - </div> + {!!organization.url && ( + <div className="markdown spacer-top"> + <a href={organization.url} title={organization.url} rel="nofollow"> + {organization.url} + </a> + </div> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx index 921b636dd3a..5b0b8ccc367 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx @@ -17,22 +17,19 @@ * 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 * as React from 'react'; import { sortBy } from 'lodash'; import OrganizationCard from './OrganizationCard'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ +import { Organization } from '../../../app/types'; -/*:: -type Props = { - organizations: Array<Organization> -}; -*/ +interface Props { + organizations: Organization[]; +} -export default function OrganizationsList(props /*: Props */) { +export default function OrganizationsList({ organizations }: Props) { return ( <ul className="account-projects-list"> - {sortBy(props.organizations, organization => organization.name.toLocaleLowerCase()).map( + {sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map( organization => ( <li key={organization.key}> <OrganizationCard organization={organization} /> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx index 98d50b19d61..accf47cbd70 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx @@ -56,7 +56,7 @@ export default function OrganizationNavigationAdministration({ location, organiz href="#" onClick={onToggleClick}> {translate('layout.settings')} - <DropdownIcon /> + <DropdownIcon className="little-spacer-left" /> </a> <ul className="dropdown-menu"> {extensions.map(extension => ( diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx index afa4332544c..84f0256e579 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx @@ -24,8 +24,7 @@ import { Organization } from '../../../app/types'; import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; import Dropdown from '../../../components/controls/Dropdown'; import DropdownIcon from '../../../components/icons-components/DropdownIcon'; -import OrganizationLink from '../../../components/ui/OrganizationLink'; -import { translate } from '../../../helpers/l10n'; +import OrganizationListItem from '../../../components/ui/OrganizationListItem'; interface Props { organization: Organization; @@ -49,17 +48,7 @@ export default function OrganizationNavigationHeader({ organization, organizatio </a> <ul className="dropdown-menu"> {sortBy(other, org => org.name.toLowerCase()).map(organization => ( - <li key={organization.key}> - <OrganizationLink className="dropdown-item-flex" organization={organization}> - <div> - <OrganizationAvatar organization={organization} small={true} /> - <span className="spacer-left">{organization.name}</span> - </div> - {organization.isAdmin && ( - <span className="outline-badge spacer-left">{translate('admin')}</span> - )} - </OrganizationLink> - </li> + <OrganizationListItem key={organization.key} organization={organization} /> ))} </ul> </div> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap index 61f766d7cf7..713261b2290 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap @@ -11,7 +11,9 @@ exports[`renders 1`] = ` onClick={[Function]} > layout.settings - <DropdownIcon /> + <DropdownIcon + className="little-spacer-left" + /> </a> <ul className="dropdown-menu" diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap index 32683a0f5be..78f6ea2ae7c 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap @@ -42,79 +42,28 @@ exports[`renders dropdown 1`] = ` <ul className="dropdown-menu" > - <li + <OrganizationListItem key="org1" - > - <OrganizationLink - className="dropdown-item-flex" - organization={ - Object { - "isAdmin": true, - "key": "org1", - "name": "org1", - "projectVisibility": "public", - } + organization={ + Object { + "isAdmin": true, + "key": "org1", + "name": "org1", + "projectVisibility": "public", } - > - <div> - <OrganizationAvatar - organization={ - Object { - "isAdmin": true, - "key": "org1", - "name": "org1", - "projectVisibility": "public", - } - } - small={true} - /> - <span - className="spacer-left" - > - org1 - </span> - </div> - <span - className="outline-badge spacer-left" - > - admin - </span> - </OrganizationLink> - </li> - <li + } + /> + <OrganizationListItem key="org2" - > - <OrganizationLink - className="dropdown-item-flex" - organization={ - Object { - "isAdmin": false, - "key": "org2", - "name": "org2", - "projectVisibility": "public", - } + organization={ + Object { + "isAdmin": false, + "key": "org2", + "name": "org2", + "projectVisibility": "public", } - > - <div> - <OrganizationAvatar - organization={ - Object { - "isAdmin": false, - "key": "org2", - "name": "org2", - "projectVisibility": "public", - } - } - small={true} - /> - <span - className="spacer-left" - > - org2 - </span> - </div> - </OrganizationLink> - </li> + } + /> </ul> </div> `; diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx index b562b7d46bf..d1bb1d60a34 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx @@ -19,22 +19,90 @@ */ import * as React from 'react'; import { Link } from 'react-router'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; +import * as PropTypes from 'prop-types'; +import { sortBy } from 'lodash'; +import { Organization } from '../../../app/types'; +import DropdownIcon from '../../../components/icons-components/DropdownIcon'; +import Dropdown from '../../../components/controls/Dropdown'; +import { getMyOrganizations } from '../../../store/rootReducer'; +import OrganizationListItem from '../../../components/ui/OrganizationListItem'; import { translate } from '../../../helpers/l10n'; -interface Props { +interface StateProps { + organizations: Organization[]; +} + +interface Props extends StateProps { onSonarCloud: boolean; } -export default function NoFavoriteProjects({ onSonarCloud }: Props) { - return ( - <div className="projects-empty-list"> - <h3>{translate('projects.no_favorite_projects')}</h3> - <p className="big-spacer-top">{translate('projects.no_favorite_projects.engagement')}</p> - <p className="big-spacer-top"> - <Link to={onSonarCloud ? '/explore/projects' : '/projects/all'} className="button"> - {translate('projects.explore_projects')} - </Link> - </p> - </div> - ); +export class NoFavoriteProjects extends React.PureComponent<Props> { + static contextTypes = { + openOnboardingTutorial: PropTypes.func + }; + + onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.context.openOnboardingTutorial(); + }; + + render() { + const { onSonarCloud, organizations } = this.props; + return ( + <div className="projects-empty-list"> + <h3>{translate('projects.no_favorite_projects')}</h3> + {onSonarCloud ? ( + <div className="spacer-top"> + <p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p> + <div className="huge-spacer-top"> + <a className="button" href="#" onClick={this.onAnalyzeProjectClick}> + {translate('my_account.analyze_new_project')} + </a> + <Dropdown> + {({ onToggleClick, open }) => ( + <div + className={classNames('display-inline-block', 'big-spacer-left', 'dropdown', { + open + })}> + <a className="button" href="#" onClick={onToggleClick}> + {translate('projects.no_favorite_projects.favorite_projects_from_orgs')} + <DropdownIcon className="little-spacer-left" /> + </a> + <ul className="dropdown-menu"> + {sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + </ul> + </div> + )} + </Dropdown> + <Link className="button big-spacer-left" to="/explore/projects"> + {translate('projects.no_favorite_projects.favorite_public_projects')} + </Link> + </div> + </div> + ) : ( + <div> + <p className="big-spacer-top"> + {translate('projects.no_favorite_projects.engagement')} + </p> + <p className="big-spacer-top"> + <Link to="/projects/all" className="button"> + {translate('projects.explore_projects')} + </Link> + </p> + </div> + )} + </div> + ); + } } + +const mapStateToProps = (state: any): StateProps => ({ + organizations: getMyOrganizations(state) +}); + +export default connect(mapStateToProps)(NoFavoriteProjects); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx index 3635c476f77..8f767812c60 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx @@ -19,8 +19,19 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import NoFavoriteProjects from '../NoFavoriteProjects'; +import { NoFavoriteProjects } from '../NoFavoriteProjects'; +import { Visibility } from '../../../../app/types'; it('renders', () => { - expect(shallow(<NoFavoriteProjects onSonarCloud={false} />)).toMatchSnapshot(); + expect(shallow(<NoFavoriteProjects onSonarCloud={false} organizations={[]} />)).toMatchSnapshot(); +}); + +it('renders for SonarCloud', () => { + const organizations = [ + { isAdmin: true, key: 'org1', name: 'org1', projectVisibility: Visibility.Public }, + { isAdmin: false, key: 'org2', name: 'org2', projectVisibility: Visibility.Public } + ]; + expect( + shallow(<NoFavoriteProjects onSonarCloud={true} organizations={organizations} />) + ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap index 3b6652f10f8..a7dede57348 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap @@ -7,22 +7,61 @@ exports[`renders 1`] = ` <h3> projects.no_favorite_projects </h3> - <p - className="big-spacer-top" - > - projects.no_favorite_projects.engagement - </p> - <p - className="big-spacer-top" + <div> + <p + className="big-spacer-top" + > + projects.no_favorite_projects.engagement + </p> + <p + className="big-spacer-top" + > + <Link + className="button" + onlyActiveOnIndex={false} + style={Object {}} + to="/projects/all" + > + projects.explore_projects + </Link> + </p> + </div> +</div> +`; + +exports[`renders for SonarCloud 1`] = ` +<div + className="projects-empty-list" +> + <h3> + projects.no_favorite_projects + </h3> + <div + className="spacer-top" > - <Link - className="button" - onlyActiveOnIndex={false} - style={Object {}} - to="/projects/all" + <p> + projects.no_favorite_projects.how_to_add_projects + </p> + <div + className="huge-spacer-top" > - projects.explore_projects - </Link> - </p> + <a + className="button" + href="#" + onClick={[Function]} + > + my_account.analyze_new_project + </a> + <Dropdown /> + <Link + className="button big-spacer-left" + onlyActiveOnIndex={false} + style={Object {}} + to="/explore/projects" + > + projects.no_favorite_projects.favorite_public_projects + </Link> + </div> + </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap index 93fb447b814..b2552033bef 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap @@ -47,6 +47,6 @@ exports[`renders different types of "no projects" 3`] = ` <div className="projects-list" > - <NoFavoriteProjects /> + <Connect(NoFavoriteProjects) /> </div> `; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 707f0243994..e1084c7f01f 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -267,3 +267,8 @@ margin-left: -250px; text-align: center; } + +.projects-empty-list { + padding: calc(4 * var(--gridSize)) 0; + text-align: center; +} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts new file mode 100644 index 00000000000..d5d3100cbfa --- /dev/null +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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'; + +export interface Props { + onFinish: () => void; +} + +export default class OnboardingModal extends React.PureComponent<Props> {} diff --git a/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx index 20bc9629f31..7cd3226777a 100644 --- a/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx @@ -24,13 +24,13 @@ export default function DropdownIcon({ className, fill = 'currentColor', size = return ( <svg className={className} - width={size} + width={size / 16 * 7} height={size} - viewBox="0 0 16 16" + viewBox="0 0 7 16" version="1.1" xmlnsXlink="http://www.w3.org/1999/xlink" xmlSpace="preserve"> - <g transform="matrix(0.0273438,0,0,0.0273438,4.5,2.65625)"> + <g transform="matrix(0.0273438,0,0,0.0273438,-6.4e-06,2.65625)"> <path style={{ fill }} d="M256,176C256,180.333 254.417,184.083 251.25,187.25L139.25,299.25C136.083,302.417 132.333,304 128,304C123.667,304 119.917,302.417 116.75,299.25L4.75,187.25C1.583,184.083 0,180.333 0,176C0,171.667 1.583,167.917 4.75,164.75C7.917,161.583 11.667,160 16,160L240,160C244.333,160 248.083,161.583 251.25,164.75C254.417,167.917 256,171.667 256,176Z" diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.tsx b/server/sonar-web/src/main/js/components/nav/NavBar.tsx index 19397032533..f5647516be8 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBar.tsx +++ b/server/sonar-web/src/main/js/components/nav/NavBar.tsx @@ -26,6 +26,7 @@ interface Props { className?: string; height: number; notif?: React.ReactNode; + [prop: string]: any; } export default function NavBar({ children, className, height, notif, ...other }: Props) { diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.js b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx index 43f8b058ec3..384849e61c1 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.js +++ b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx @@ -17,25 +17,28 @@ * 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 GlobalNav from './nav/global/GlobalNav'; -import GlobalFooterContainer from './GlobalFooterContainer'; -import GlobalMessagesContainer from './GlobalMessagesContainer'; +import * as React from 'react'; +import { Organization } from '../../app/types'; +import OrganizationLink from './OrganizationLink'; +import OrganizationAvatar from '../common/OrganizationAvatar'; +import { translate } from '../../helpers/l10n'; -export default function GlobalContainer(props /*: Object */) { - // it is important to pass `location` down to `GlobalNav` to trigger render on url change +interface Props { + organization: Organization; +} +export default function OrganizationListItem({ organization }: Props) { return ( - <div className="global-container"> - <div className="page-wrapper" id="container"> - <div className="page-container"> - <GlobalNav location={props.location} /> - <GlobalMessagesContainer /> - {props.children} + <li> + <OrganizationLink className="dropdown-item-flex" organization={organization}> + <div> + <OrganizationAvatar organization={organization} small={true} /> + <span className="spacer-left">{organization.name}</span> </div> - </div> - <GlobalFooterContainer /> - </div> + {organization.isAdmin && ( + <span className="outline-badge spacer-left">{translate('admin')}</span> + )} + </OrganizationLink> + </li> ); } diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx new file mode 100644 index 00000000000..dd340f58686 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import OrganizationListItem from '../OrganizationListItem'; +import { Visibility } from '../../../app/types'; + +it('renders', () => { + expect( + shallow( + <OrganizationListItem + organization={{ + isAdmin: true, + key: 'org', + name: 'org', + projectVisibility: Visibility.Public + }} + /> + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap new file mode 100644 index 00000000000..bd3a76ad927 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +<li> + <OrganizationLink + className="dropdown-item-flex" + organization={ + Object { + "isAdmin": true, + "key": "org", + "name": "org", + "projectVisibility": "public", + } + } + > + <div> + <OrganizationAvatar + organization={ + Object { + "isAdmin": true, + "key": "org", + "name": "org", + "projectVisibility": "public", + } + } + small={true} + /> + <span + className="spacer-left" + > + org + </span> + </div> + <span + className="outline-badge spacer-left" + > + admin + </span> + </OrganizationLink> +</li> +`; diff --git a/server/sonar-web/src/main/js/store/appState/duck.ts b/server/sonar-web/src/main/js/store/appState/duck.ts index abb05d0b309..dd0f2a8c962 100644 --- a/server/sonar-web/src/main/js/store/appState/duck.ts +++ b/server/sonar-web/src/main/js/store/appState/duck.ts @@ -18,15 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Extension } from '../../app/types'; - -interface AppState { - adminPages?: Extension[]; - authenticationError: boolean; - authorizationError: boolean; - organizationsEnabled: boolean; - qualifiers?: string[]; -} +import { Extension, AppState } from '../../app/types'; interface SetAppStateAction { type: 'SET_APP_STATE'; @@ -62,7 +54,8 @@ export function requireAuthorization(): RequireAuthorizationAction { const defaultValue: AppState = { authenticationError: false, authorizationError: false, - organizationsEnabled: false + organizationsEnabled: false, + qualifiers: [] }; export default function(state: AppState = defaultValue, action: Action): AppState { diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.js index 4b4023f97ce..35578963545 100644 --- a/server/sonar-web/src/main/js/store/organizations/duck.js +++ b/server/sonar-web/src/main/js/store/organizations/duck.js @@ -199,7 +199,7 @@ function byKey(state /*: ByKeyState */ = {}, action /*: Action */) { case 'RECEIVE_MY_ORGANIZATIONS': return onReceiveOrganizations(state, action); case 'CREATE_ORGANIZATION': - return { ...state, [action.organization.key]: action.organization }; + return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } }; case 'UPDATE_ORGANIZATION': return { ...state, |