From: Grégoire Aubert Date: Wed, 10 May 2017 16:13:28 +0000 (+0200) Subject: SONAR-9044 Easy access to my organizations X-Git-Tag: 6.4-RC1~31 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=af0fb06721dcc88c53622f0380f1016aa0bd17c8;p=sonarqube.git SONAR-9044 Easy access to my organizations --- 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.js index fca0171d9ec..86ed0974c5c 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.js @@ -21,7 +21,7 @@ import React from 'react'; import { connect } from 'react-redux'; import GlobalNavBranding from './GlobalNavBranding'; import GlobalNavMenu from './GlobalNavMenu'; -import GlobalNavUser from './GlobalNavUser'; +import GlobalNavUserContainer from './GlobalNavUserContainer'; import Search from '../../search/Search'; import ShortcutsHelpView from './ShortcutsHelpView'; import { getCurrentUser, getAppState } from '../../../../store/rootReducer'; @@ -76,7 +76,7 @@ class GlobalNav extends React.PureComponent { - + diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js index 6d60902e1c8..ac0f288819b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js @@ -19,21 +19,47 @@ */ // @flow import React from 'react'; -import { Link, withRouter } from 'react-router'; +import classNames from 'classnames'; +import { sortBy } from 'lodash'; +import { Link } from 'react-router'; import Avatar from '../../../../components/ui/Avatar'; +import OrganizationLink from '../../../../components/ui/OrganizationLink'; import { translate } from '../../../../helpers/l10n'; -class GlobalNavUser extends React.PureComponent { - props: { - currentUser: { - email?: string, - name: string - }, - location: Object, - router: { push: string => void } +type CurrentUser = { + email?: string, + isLoggedIn: boolean, + name: string +}; + +type Props = { + currentUser: CurrentUser, + fetchMyOrganizations: () => Promise<*>, + location: Object, + organizations: Array<{ key: string, name: string }>, + router: { push: string => void } +}; + +type State = { + open: boolean +}; + +export default class GlobalNavUser extends React.PureComponent { + node: HTMLElement; + props: Props; + state: State = { open: false }; + + componentWillUnmount() { + window.removeEventListener('click', this.handleClickOutside); + } + + handleClickOutside = (event: { target: HTMLElement }) => { + if (!this.node || !this.node.contains(event.target)) { + this.closeDropdown(); + } }; - handleLogin = e => { + handleLogin = (e: Event) => { e.preventDefault(); const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`; if (shouldReturnToCurrentPage) { @@ -45,36 +71,76 @@ class GlobalNavUser extends React.PureComponent { } }; - handleLogout = e => { + handleLogout = (e: Event) => { e.preventDefault(); + this.closeDropdown(); this.props.router.push('/sessions/logout'); }; + toggleDropdown = (evt: Event) => { + evt.preventDefault(); + if (this.state.open) { + this.closeDropdown(); + } else { + this.openDropdown(); + } + }; + + openDropdown = () => { + this.props.fetchMyOrganizations().then(() => { + window.addEventListener('click', this.handleClickOutside, true); + this.setState({ open: true }); + }); + }; + + closeDropdown = () => { + window.removeEventListener('click', this.handleClickOutside); + this.setState({ open: false }); + }; + renderAuthenticated() { - const { currentUser } = this.props; + const { currentUser, organizations } = this.props; + const hasOrganizations = organizations.length > 0; return ( -
  • - +
  • (this.node = node)}> + -
      -
    • -
      - {currentUser.name} -
      - {currentUser.email != null && -
      - {currentUser.email} -
      } -
    • -
    • -
    • - {translate('my_account.page')} -
    • -
    • - {translate('layout.logout')} -
    • -
    + {this.state.open && +
      +
    • +
      + {currentUser.name} +
      + {currentUser.email != null && +
      + {currentUser.email} +
      } +
    • +
    • +
    • + {translate('my_account.page')} +
    • + {hasOrganizations &&
    • } + {hasOrganizations && +
    • {translate('my_organizations')}
    • } + {hasOrganizations && + sortBy(organizations, org => org.name.toLowerCase()).map(organization => ( +
    • + + {organization.name} + +
    • + ))} + {hasOrganizations &&
    • } +
    • + {translate('layout.logout')} +
    • +
    }
  • ); } @@ -91,5 +157,3 @@ class GlobalNavUser extends React.PureComponent { return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous(); } } - -export default withRouter(GlobalNavUser); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js new file mode 100644 index 00000000000..c84bdff2a7a --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js @@ -0,0 +1,35 @@ +/* + * 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 { withRouter } from 'react-router'; +import { connect } from 'react-redux'; +import GlobalNavUser from './GlobalNavUser'; +import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions'; +import { getMyOrganizations } from '../../../../store/rootReducer'; + +const mapStateToProps = state => ({ + organizations: getMyOrganizations(state) +}); + +const mapDispatchToProps = { + fetchMyOrganizations +}; + +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser)); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js new file mode 100644 index 00000000000..0a9f7fb9fa3 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js @@ -0,0 +1,107 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import GlobalNavUser from '../GlobalNavUser'; + +const currentUser = { isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' }; +const organizations = [ + { key: 'myorg', name: 'MyOrg' }, + { key: 'foo', name: 'Foo' }, + { key: 'bar', name: 'bar' } +]; + +it('should render the right interface for anonymous user', () => { + const currentUser = { isLoggedIn: false }; + const wrapper = shallow( + {}} organizations={[]} /> + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the right interface for logged in user', () => { + const wrapper = shallow( + {}} organizations={[]} /> + ); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the users organizations', () => { + const wrapper = shallow( + {}} + organizations={organizations} + /> + ); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should update the component correctly when the user changes to anonymous', () => { + const fetchMyOrganizations = jest.fn(); + const wrapper = shallow( + + ); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); + wrapper.setProps({ currentUser: { isLoggedIn: false } }); + expect(fetchMyOrganizations.mock.calls.length).toBe(0); + expect(wrapper).toMatchSnapshot(); +}); + +it('should lazyload the organizations when opening the dropdown', () => { + const fetchMyOrganizations = jest.fn(() => Promise.resolve()); + const wrapper = shallow( + + ); + expect(fetchMyOrganizations.mock.calls.length).toBe(0); + wrapper.instance().openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(1); + wrapper.instance().openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(2); +}); + +it('should update the organizations when the user changes', () => { + const fetchMyOrganizations = jest.fn(() => Promise.resolve()); + const wrapper = shallow( + + ); + wrapper.instance().openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(1); + wrapper.setProps({ + currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' } + }); + wrapper.instance().openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(2); +}); diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap new file mode 100644 index 00000000000..50f52bb687c --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap @@ -0,0 +1,270 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render the right interface for anonymous user 1`] = ` +
  • + + layout.login + +
  • +`; + +exports[`should render the right interface for logged in user 1`] = ` +
  • + + + + +
  • +`; + +exports[`should render the users organizations 1`] = ` +
  • + + + +
      +
    • +
      + + foo + +
      +
      + foo@bar.baz +
      +
    • +
    • +
    • + + my_account.page + +
    • +
    • +
    • + my_organizations +
    • +
    • + + + bar + + +
    • +
    • + + + Foo + + +
    • +
    • + + + MyOrg + + +
    • +
    • +
    • + + layout.logout + +
    • +
    +
  • +`; + +exports[`should update the component correctly when the user changes to anonymous 1`] = ` +
  • + + + + +
  • +`; + +exports[`should update the component correctly when the user changes to anonymous 2`] = ` +
  • + + layout.login + +
  • +`; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js index 739948592e3..fe3ad60da2e 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.js @@ -31,34 +31,45 @@ type OwnProps = { params: { organizationKey: string } }; +type Props = { + children?: React.Element<*>, + location: Object, + organization: null | Organization, + params: { organizationKey: string }, + fetchOrganization: string => Promise<*> +}; + class OrganizationPage extends React.PureComponent { mounted: boolean; - - props: { - children?: React.Element<*>, - location: Object, - organization: null | Organization, - params: { organizationKey: string }, - fetchOrganization: string => Promise<*> - }; - - state = { - loading: true - }; + props: Props; + state = { loading: true }; componentDidMount() { this.mounted = true; - this.props.fetchOrganization(this.props.params.organizationKey).then(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); + this.updateOrganization(this.props.params.organizationKey); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.params.organizationKey !== this.props.params.organizationKey) { + this.updateOrganization(nextProps.params.organizationKey); + } } componentWillUnmount() { this.mounted = false; } + updateOrganization = (organizationKey: string) => { + if (this.mounted) { + this.setState({ loading: true }); + } + this.props.fetchOrganization(organizationKey).then(() => { + if (this.mounted) { + this.setState({ loading: false }); + } + }); + }; + render() { const { organization } = this.props; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js index 69afb1dc902..4ced6f1a369 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationPage-test.js @@ -23,7 +23,7 @@ import { UnconnectedOrganizationPage } from '../OrganizationPage'; it('smoke test', () => { const wrapper = shallow( - +
    hello
    ); @@ -36,10 +36,23 @@ it('smoke test', () => { it('not found', () => { const wrapper = shallow( - +
    hello
    ); wrapper.setState({ loading: false }); expect(wrapper).toMatchSnapshot(); }); + +it('should correctly update when the organization changes', () => { + const fetchOrganization = jest.fn(() => Promise.resolve()); + const wrapper = shallow( + +
    hello
    +
    + ); + wrapper.setProps({ params: { organizationKey: 'bar' } }); + expect(fetchOrganization.mock.calls).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap index 6c8f73738d4..d9579e1bf4a 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationPage-test.js.snap @@ -2,6 +2,14 @@ exports[`not found 1`] = ``; +exports[`should correctly update when the organization changes 1`] = ` +Array [ + Array [ + "bar", + ], +] +`; + exports[`smoke test 1`] = `null`; exports[`smoke test 2`] = ` diff --git a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js b/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js index ebe90b8f947..e17e8668963 100644 --- a/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js +++ b/server/sonar-web/src/main/js/apps/projects-admin/__tests__/projects-test.js @@ -28,7 +28,14 @@ it('should render list of projects with no selection', () => { { id: '2', key: 'b', name: 'B', qualifier: 'TRK' } ]; - const result = shallow(); + const result = shallow( + + ); expect(result.find('tr').length).toBe(2); expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(0); }); @@ -41,7 +48,12 @@ it('should render list of projects with one selected', () => { const selection = ['a']; const result = shallow( - + ); expect(result.find('tr').length).toBe(2); expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(1); 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 d09c631e58c..6dbc794bc24 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -254,6 +254,7 @@ logging_out=You're logging out, please wait... manage=Manage move_left=Move left move_right=Move right +my_organizations=My Organizations new_issues=New issues new_violations=New violations new_window=New window