From: Stas Vilchik Date: Thu, 30 Nov 2017 12:41:38 +0000 (+0100) Subject: SONAR-8829 move organization avatar to the left X-Git-Tag: 7.0-RC1~165 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=aa02b659cf9920ab6757611706b0dcf3286894da;p=sonarqube.git SONAR-8829 move organization avatar to the left --- diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css index 72b8cbb1c27..ccfd5d9c63e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css @@ -1,6 +1,8 @@ .navbar-context-favorite { - float: left; - padding: 7px 10px 0 0; + display: inline-block; + vertical-align: top; + padding-top: var(--gridSize); + padding-left: calc(1.5 * var(--gridSize)); } .navbar-context-title-qualifier { @@ -11,9 +13,10 @@ } .navbar-context-branches { - float: left; - padding: 8px 0 6px; - margin-left: 16px; + display: inline-block; + vertical-align: top; + padding: var(--gridSize) 0; + margin-left: calc(2 * var(--gridSize)); line-height: 16px; } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 110c8b2a2ef..8f13a285693 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -25,6 +25,7 @@ import ComponentNavMeta from './ComponentNavMeta'; import ComponentNavMenu from './ComponentNavMenu'; import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; import RecentHistory from '../../RecentHistory'; +import * as theme from '../../../theme'; import { Branch, Component } from '../../../types'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; import { getTasksForComponent, PendingTask, Task } from '../../../../api/ce'; @@ -109,16 +110,16 @@ export default class ComponentNav extends React.PureComponent { return ( - + {this.props.currentBranch && ( - {!displayOrganization && - index === 0 && ( - - - - )} + {index === 0 && ( + + + + )} - {index === breadcrumbs.length - 1 ? ( - {itemName} - ) : ( - {itemName} - )} + to={getProjectUrl(item.key)}> + {itemName} {index < breadcrumbs.length - 1 && } @@ -81,12 +78,10 @@ class ComponentNavBreadcrumbs extends React.PureComponent { /> {displayOrganization && ( - - - + + className="link-base-color link-no-underline spacer-left"> {organization.name} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index f92f0e066ea..1baaad7dc1f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -2,7 +2,7 @@ exports[`renders 1`] = ` } > - + - - My Project - + My Project @@ -54,15 +53,16 @@ exports[`should render organization 1`] = ` title="My Project" /> - - - + + + + - - My Project - + My Project 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 deleted file mode 100644 index 3b577558541..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js +++ /dev/null @@ -1,204 +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 classNames from 'classnames'; -import { sortBy } from 'lodash'; -import { Link } from 'react-router'; -import * as theme from '../../../theme'; -import Avatar from '../../../../components/ui/Avatar'; -import OrganizationIcon from '../../../../components/icons-components/OrganizationIcon'; -import OrganizationLink from '../../../../components/ui/OrganizationLink'; -import { translate } from '../../../../helpers/l10n'; - -/*:: -type CurrentUser = { - avatar?: string, - email?: string, - isLoggedIn: boolean, - name: string -}; -*/ - -/*:: -type Props = { - appState: { - organizationsEnabled: boolean - }, - currentUser: CurrentUser, - fetchMyOrganizations: () => Promise<*>, - location: Object, - organizations: Array<{ isAdmin: bool, 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 /*: Event */) => { - e.preventDefault(); - const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`; - if (shouldReturnToCurrentPage) { - const returnTo = encodeURIComponent(window.location.pathname + window.location.search); - window.location = - window.baseUrl + `/sessions/new?return_to=${returnTo}${window.location.hash}`; - } else { - window.location = `${window.baseUrl}/sessions/new`; - } - }; - - 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.fetchMyOrganizations().then(() => { - window.addEventListener('click', this.handleClickOutside, true); - this.setState({ open: true }); - }); - }; - - closeDropdown = () => { - window.removeEventListener('click', this.handleClickOutside); - this.setState({ open: false }); - }; - - fetchMyOrganizations = () => { - if (this.props.appState.organizationsEnabled) { - return this.props.fetchMyOrganizations(); - } - return Promise.resolve(); - }; - - renderAuthenticated() { - const { currentUser, organizations } = this.props; - const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; - return ( -
  • (this.node = node)}> - - - - {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} -
      - {organization.isAdmin && ( - {translate('admin')} - )} -
      -
    • - ))} - {hasOrganizations &&
    • } -
    • - - {translate('layout.logout')} - -
    • -
    - )} -
  • - ); - } - - renderAnonymous() { - return ( -
  • - - {translate('layout.login')} - -
  • - ); - } - - render() { - return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous(); - } -} 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 new file mode 100644 index 00000000000..3a5a143f963 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -0,0 +1,197 @@ +/* + * 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 classNames from 'classnames'; +import { sortBy } from 'lodash'; +import * as PropTypes from 'prop-types'; +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 { translate } from '../../../../helpers/l10n'; +import { getBaseUrl } from '../../../../helpers/urls'; +import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; + +interface Props { + appState: { organizationsEnabled: boolean }; + currentUser: CurrentUser; + fetchMyOrganizations: () => Promise; + organizations: Organization[]; +} + +interface State { + open: boolean; +} + +export default class GlobalNavUser extends React.PureComponent { + node?: HTMLElement | null; + + 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) => { + event.preventDefault(); + const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`; + if (shouldReturnToCurrentPage) { + const returnTo = encodeURIComponent(window.location.pathname + window.location.search); + window.location.href = + getBaseUrl() + `/sessions/new?return_to=${returnTo}${window.location.hash}`; + } else { + window.location.href = `${getBaseUrl()}/sessions/new`; + } + }; + + handleLogout = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.closeDropdown(); + this.context.router.push('/sessions/logout'); + }; + + toggleDropdown = (event: React.SyntheticEvent) => { + event.preventDefault(); + if (this.state.open) { + this.closeDropdown(); + } else { + this.openDropdown(); + } + }; + + openDropdown = () => { + this.fetchMyOrganizations().then(() => { + window.addEventListener('click', this.handleClickOutside, true); + this.setState({ open: true }); + }); + }; + + closeDropdown = () => { + window.removeEventListener('click', this.handleClickOutside); + this.setState({ open: false }); + }; + + fetchMyOrganizations = () => { + if (this.props.appState.organizationsEnabled) { + return this.props.fetchMyOrganizations(); + } + return Promise.resolve(); + }; + + renderAuthenticated() { + const { organizations } = this.props; + const currentUser = this.props.currentUser as LoggedInUser; + const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; + return ( +
  • (this.node = node)}> + + + + {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} +
      + {organization.canAdmin && ( + {translate('admin')} + )} +
      +
    • + ))} + {hasOrganizations &&
    • } +
    • + + {translate('layout.logout')} + +
    • +
    + )} +
  • + ); + } + + renderAnonymous() { + return ( +
  • + + {translate('layout.login')} + +
  • + ); + } + + render() { + return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous(); + } +} 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 deleted file mode 100644 index c84bdff2a7a..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js +++ /dev/null @@ -1,35 +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 { 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/GlobalNavUserContainer.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx new file mode 100644 index 00000000000..f73200791a0 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx @@ -0,0 +1,42 @@ +/* + * 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 { connect } from 'react-redux'; +import GlobalNavUser from './GlobalNavUser'; +import { Organization } from '../../../types'; +import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions'; +import { getMyOrganizations } from '../../../../store/rootReducer'; + +interface StateProps { + organizations: Organization[]; +} + +const mapStateToProps = (state: any): StateProps => ({ + organizations: getMyOrganizations(state) +}); + +interface DispatchProps { + fetchMyOrganizations: () => Promise; +} + +const mapDispatchToProps = { + fetchMyOrganizations: fetchMyOrganizations as any +} as DispatchProps; + +export default connect(mapStateToProps, mapDispatchToProps)(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 deleted file mode 100644 index 4fb4f28f2d2..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js +++ /dev/null @@ -1,135 +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. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import GlobalNavUser from '../GlobalNavUser'; - -const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' }; -const organizations = [ - { key: 'myorg', name: 'MyOrg' }, - { key: 'foo', name: 'Foo' }, - { key: 'bar', name: 'bar' } -]; -const appState = { organizationsEnabled: true }; - -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 not render the users organizations when they are not activated', () => { - 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__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx new file mode 100644 index 00000000000..1ffec34be8f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { shallow } from 'enzyme'; +import GlobalNavUser from '../GlobalNavUser'; + +const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' }; +const organizations = [ + { key: 'myorg', name: 'MyOrg', projectVisibility: 'public' }, + { key: 'foo', name: 'Foo', projectVisibility: 'public' }, + { key: 'bar', name: 'bar', projectVisibility: 'public' } +]; +const appState = { organizationsEnabled: true }; + +it('should render the right interface for anonymous user', () => { + const currentUser = { isLoggedIn: false }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the right interface for logged in user', () => { + const wrapper = shallow( + + ); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render the users organizations', () => { + const wrapper = shallow( + + ); + wrapper.setState({ open: true }); + expect(wrapper).toMatchSnapshot(); +}); + +it('should not render the users organizations when they are not activated', () => { + const wrapper = shallow( + + ); + 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() as GlobalNavUser).openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(1); + (wrapper.instance() as GlobalNavUser).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() as GlobalNavUser).openDropdown(); + expect(fetchMyOrganizations.mock.calls.length).toBe(1); + wrapper.setProps({ + currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' } + }); + (wrapper.instance() as GlobalNavUser).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 deleted file mode 100644 index 85ca26d59b2..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap +++ /dev/null @@ -1,356 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render the users organizations when they are not activated 1`] = ` -
  • - - - - -
  • -`; - -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/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 new file mode 100644 index 00000000000..3bb56aac675 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap @@ -0,0 +1,386 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render the users organizations when they are not activated 1`] = ` +
  • + + + + +
  • +`; + +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/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index db5f9efcf95..54d91d77c75 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { IndexLink, Link } from 'react-router'; +import * as theme from '../../../../app/theme'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; @@ -203,11 +204,9 @@ export default class SettingsNav extends React.PureComponent { return ( -

    - {translate('layout.settings')} -

    +

    {translate('layout.settings')}

    {this.renderConfigurationTab()} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index e33f8690286..3208552f512 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -2,15 +2,13 @@ exports[`should work with extensions 1`] = `

    - - layout.settings - + layout.settings

  • , - organizations: Array, - fetchIfAnyoneCanCreateOrganizations: () => Promise<*>, - fetchMyOrganizations: () => Promise<*> - }; -*/ - - state /*: { loading: boolean } */ = { - loading: true - }; - - componentDidMount() { - this.mounted = true; - Promise.all([ - this.props.fetchMyOrganizations(), - this.props.fetchIfAnyoneCanCreateOrganizations() - ]).then(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); - } - - componentWillUnmount() { - this.mounted = false; - } - - render() { - const anyoneCanCreate = - this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; - - const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin); - - return ( -
    - - -
    -

    {translate('my_account.organizations')}

    - {canCreateOrganizations && ( -
    - - {translate('create')} - -
    - )} - {this.props.organizations.length > 0 ? ( -
    - {translate('my_account.organizations.description')} -
    - ) : ( -
    - {translate('my_account.organizations.no_results')} -
    - )} -
    - - {this.state.loading ? ( - - ) : ( - - )} - - {this.props.children} -
    - ); - } -} - -const mapStateToProps = state => ({ - anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'), - canAdmin: getAppState(state).canAdmin, - organizations: getMyOrganizations(state) -}); - -const mapDispatchToProps = { - fetchMyOrganizations, - fetchIfAnyoneCanCreateOrganizations -}; - -export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx new file mode 100644 index 00000000000..75a48b2bed9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx @@ -0,0 +1,124 @@ +/* + * 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 Helmet from 'react-helmet'; +import { connect } from 'react-redux'; +import { Link } from 'react-router'; +import OrganizationsList from './OrganizationsList'; +import { translate } from '../../../helpers/l10n'; +import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions'; +import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; +import { Organization } from '../../../app/types'; + +interface StateProps { + anyoneCanCreate?: { value: string }; + canAdmin: boolean; + organizations: Array; +} + +interface DispatchProps { + fetchIfAnyoneCanCreateOrganizations: () => Promise; + fetchMyOrganizations: () => Promise; +} + +interface Props extends StateProps, DispatchProps { + children?: React.ReactNode; +} + +interface State { + loading: boolean; +} + +class UserOrganizations extends React.PureComponent { + mounted: boolean; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + Promise.all([ + this.props.fetchMyOrganizations(), + this.props.fetchIfAnyoneCanCreateOrganizations() + ]).then(this.stopLoading, this.stopLoading); + } + + componentWillUnmount() { + this.mounted = false; + } + + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + + render() { + const anyoneCanCreate = + this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; + + const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin); + + return ( +
    + + +
    +

    {translate('my_account.organizations')}

    + {canCreateOrganizations && ( +
    + + {translate('create')} + +
    + )} + {this.props.organizations.length > 0 ? ( +
    + {translate('my_account.organizations.description')} +
    + ) : ( +
    + {translate('my_account.organizations.no_results')} +
    + )} +
    + + {this.state.loading ? ( + + ) : ( + + )} + + {this.props.children} +
    + ); + } +} + +const mapStateToProps = (state: any): StateProps => ({ + anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'), + canAdmin: getAppState(state).canAdmin, + organizations: getMyOrganizations(state) +}); + +const mapDispatchToProps = { + fetchMyOrganizations: fetchMyOrganizations as any, + fetchIfAnyoneCanCreateOrganizations: fetchIfAnyoneCanCreateOrganizations as any +} as DispatchProps; + +export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.js b/server/sonar-web/src/main/js/apps/account/organizations/actions.js deleted file mode 100644 index 38f30b159b0..00000000000 --- a/server/sonar-web/src/main/js/apps/account/organizations/actions.js +++ /dev/null @@ -1,35 +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. - */ -import { getOrganizations } from '../../../api/organizations'; -import { receiveMyOrganizations } from '../../../store/organizations/duck'; -import { getValues } from '../../../api/settings'; -import { receiveValues } from '../../settings/store/values/actions'; - -export const fetchMyOrganizations = () => dispatch => { - return getOrganizations({ member: true }).then(({ organizations }) => { - return dispatch(receiveMyOrganizations(organizations)); - }); -}; - -export const fetchIfAnyoneCanCreateOrganizations = () => dispatch => { - return getValues('sonar.organizations.anyoneCanCreate').then(values => { - dispatch(receiveValues(values)); - }); -}; diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts new file mode 100644 index 00000000000..f93bcdd1633 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts @@ -0,0 +1,36 @@ +/* + * 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 { Dispatch } from 'redux'; +import { getOrganizations } from '../../../api/organizations'; +import { receiveMyOrganizations } from '../../../store/organizations/duck'; +import { getValues } from '../../../api/settings'; +import { receiveValues } from '../../settings/store/values/actions'; + +export const fetchMyOrganizations = () => (dispatch: Dispatch) => { + return getOrganizations({ member: true }).then(({ organizations }) => { + return dispatch(receiveMyOrganizations(organizations)); + }); +}; + +export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch) => { + return getValues('sonar.organizations.anyoneCanCreate').then(values => { + dispatch(receiveValues(values, undefined)); + }); +}; diff --git a/server/sonar-web/src/main/js/apps/explore/Explore.tsx b/server/sonar-web/src/main/js/apps/explore/Explore.tsx index 7cb0c776b2b..a1ed154d36a 100644 --- a/server/sonar-web/src/main/js/apps/explore/Explore.tsx +++ b/server/sonar-web/src/main/js/apps/explore/Explore.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { Link } from 'react-router'; +import * as theme from '../../app/theme'; import ContextNavBar from '../../components/nav/ContextNavBar'; import NavBarTabs from '../../components/nav/NavBarTabs'; import { translate } from '../../helpers/l10n'; @@ -30,7 +31,7 @@ interface Props { export default function Explore(props: Props) { return (
    - +

    {translate('explore')}

    diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js index 43728684b84..507d43bbfb0 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js @@ -21,10 +21,11 @@ import React from 'react'; import { Link } from 'react-router'; import classNames from 'classnames'; +import * as theme from '../../../app/theme'; import { translate } from '../../../helpers/l10n'; import ContextNavBar from '../../../components/nav/ContextNavBar'; import NavBarTabs from '../../../components/nav/NavBarTabs'; -import OrganizationIcon from '../../../components/icons-components/OrganizationIcon'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; import { getQualityGatesUrl } from '../../../helpers/urls'; /*:: import type { Organization } from '../../../store/organizations/duck'; */ @@ -153,14 +154,14 @@ export default class OrganizationNavigation extends React.PureComponent { const moreActive = !adminActive && location.pathname.includes('/extension/'); return ( - +

    - + - {organization.name} + className="link-base-color link-no-underline spacer-left"> + {organization.name}

    {organization.description != null && ( @@ -173,9 +174,9 @@ export default class OrganizationNavigation extends React.PureComponent {
    - {!!organization.avatar && ( - {organization.name} - )} +
    + {translate('organization.key')}: {organization.key} +
    {organization.url != null && (

    @@ -191,7 +192,7 @@ export default class OrganizationNavigation extends React.PureComponent { )}

    - +
  • - - - Foo - + Foo
    - + > +
    + + organization.key + : + + + foo +
    +
    +
  • - - - Foo - + Foo
    - + > +
    + + organization.key + : + + + foo +
    +
    +
  • - - - Foo - + Foo
    - + > +
    + + organization.key + : + + + foo +
    +
    +
  • + {organization.avatar ? ( + {organization.name} + ) : ( + + )} + + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx deleted file mode 100644 index dc89017c23d..00000000000 --- a/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx +++ /dev/null @@ -1,40 +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. - */ -import * as React from 'react'; -import * as theme from '../../app/theme'; -import { IconProps } from './types'; - -export default function OrganizationIcon({ className, fill = theme.blue, size = 16 }: IconProps) { - return ( - - - - ); -} diff --git a/server/sonar-web/src/main/js/components/icons-components/icons.ts b/server/sonar-web/src/main/js/components/icons-components/icons.ts index d84d2ac30c4..fad7ac6b86c 100644 --- a/server/sonar-web/src/main/js/components/icons-components/icons.ts +++ b/server/sonar-web/src/main/js/components/icons-components/icons.ts @@ -40,7 +40,6 @@ export { default as LinkIcon } from './LinkIcon'; export { default as ListIcon } from './ListIcon'; export { default as LongLivingBranchIcon } from './LongLivingBranchIcon'; export { default as OpenCloseIcon } from './OpenCloseIcon'; -export { default as OrganizationIcon } from './OrganizationIcon'; export { default as PendingIcon } from './PendingIcon'; export { default as ProjectEventIcon } from './ProjectEventIcon'; export { default as PullRequestIcon } from './PullRequestIcon'; diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css index 4aa4bd60b16..489747900bf 100644 --- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css +++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css @@ -5,7 +5,7 @@ } .navbar-context .navbar-inner { - padding-top: 5px; + padding-top: var(--gridSize); border-bottom: 1px solid var(--barBorderColor); } @@ -14,16 +14,35 @@ } .navbar-context-header { - float: left; - line-height: 30px; - font-size: 15px; + display: inline-block; + height: calc(4 * var(--gridSize)); + line-height: calc(4 * var(--gridSize)); + font-size: var(--bigFontSize); +} + +.navbar-context-header h1 { + vertical-align: top; + line-height: calc(4 * var(--gridSize)); +} + +.navbar-context-header .slash-separator { + display: inline-block; + vertical-align: top; + height: calc(4 * var(--gridSize)); + margin-left: var(--gridSize); + margin-right: var(--gridSize); + font-size: 24px; +} + +.navbar-context-header .slash-separator::after { + color: rgba(68, 68, 68, 0.2); } .navbar-context-meta { position: absolute; top: 0; right: 0; - line-height: 30px; + line-height: calc(4 * var(--gridSize)); padding: 0 10px; color: var(--secondFontColor); font-size: var(--smallFontSize); diff --git a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css index d08552ddf87..288ce5fd315 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css +++ b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css @@ -2,6 +2,8 @@ display: flex; align-items: center; clear: left; + height: var(--controlHeight); + margin-top: var(--gridSize); } .navbar-tabs > li + li { @@ -10,8 +12,10 @@ .navbar-tabs > li > a { display: block; - padding: 7px 0 4px; + height: var(--controlHeight); + line-height: calc(var(--controlHeight) - 6px); border-bottom: 3px solid transparent; + box-sizing: border-box; color: var(--baseFontColor); transition: none; } diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.tsx b/server/sonar-web/src/main/js/components/ui/Avatar.tsx index d9f09b24076..feb5253d1cb 100644 --- a/server/sonar-web/src/main/js/components/ui/Avatar.tsx +++ b/server/sonar-web/src/main/js/components/ui/Avatar.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { connect } from 'react-redux'; import * as classNames from 'classnames'; import { getGlobalSettingValue } from '../../store/rootReducer'; +import GenericAvatar from './GenericAvatar'; interface Props { className?: string; @@ -31,58 +32,24 @@ interface Props { size: number; } -class Avatar extends React.PureComponent { - renderFallback() { - const className = classNames(this.props.className, 'rounded'); - const color = stringToColor(this.props.name); - - let text = ''; - const words = this.props.name.split(/\s+/).filter(word => word.length > 0); - if (words.length >= 2) { - text = words[0][0] + words[1][0]; - } else if (this.props.name.length > 0) { - text = this.props.name[0]; - } - - return ( -
    - {text.toUpperCase()} -
    - ); +function Avatar(props: Props) { + if (!props.enableGravatar || !props.hash) { + return ; } - render() { - if (!this.props.enableGravatar || !this.props.hash) { - return this.renderFallback(); - } - - const url = this.props.gravatarServerUrl - .replace('{EMAIL_MD5}', this.props.hash) - .replace('{SIZE}', String(this.props.size * 2)); - - return ( - {this.props.name} - ); - } + const url = props.gravatarServerUrl + .replace('{EMAIL_MD5}', props.hash) + .replace('{SIZE}', String(props.size * 2)); + + return ( + {props.name} + ); } const mapStateToProps = (state: any) => ({ @@ -93,26 +60,3 @@ const mapStateToProps = (state: any) => ({ export default connect(mapStateToProps)(Avatar); export const unconnectedAvatar = Avatar; - -/* eslint-disable no-bitwise, no-mixed-operators */ -function stringToColor(str: string) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xff; - color += ('00' + value.toString(16)).substr(-2); - } - return color; -} - -function getTextColor(background: string) { - const rgb = parseInt(background.substr(1), 16); - const r = (rgb >> 16) & 0xff; - const g = (rgb >> 8) & 0xff; - const b = (rgb >> 0) & 0xff; - const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; - return luma > 140 ? '#222' : '#fff'; -} diff --git a/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx new file mode 100644 index 00000000000..36498b69b1b --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx @@ -0,0 +1,81 @@ +/* + * 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 classNames from 'classnames'; + +interface Props { + className?: string; + name: string; + size: number; +} + +export default function GenericAvatar({ className, name, size }: Props) { + const color = stringToColor(name); + + let text = ''; + const words = name.split(/\s+/).filter(word => word.length > 0); + if (words.length >= 2) { + text = words[0][0] + words[1][0]; + } else if (name.length > 0) { + text = name[0]; + } + + return ( +
    + {text.toUpperCase()} +
    + ); +} + +/* eslint-disable no-bitwise, no-mixed-operators */ +function stringToColor(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +function getTextColor(background: string) { + const rgb = parseInt(background.substr(1), 16); + const r = (rgb >> 16) & 0xff; + const g = (rgb >> 8) & 0xff; + const b = (rgb >> 0) & 0xff; + const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luma > 140 ? '#222' : '#fff'; +} diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap index 6dd7bc59142..0083612ce2f 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap @@ -1,25 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`falls back to dummy avatar 1`] = ` -
    - FB -
    + `; exports[`should be able to render with hash only 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 8b3a473149e..c055ad8d0a5 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1400,8 +1400,8 @@ my_account.projects.no_results=You are not administering any project yet. my_account.projects.analyzed_x=Analyzed {0} my_account.projects.never_analyzed=Never analyzed my_account.organizations=Organizations -my_account.organizations.description=Those organizations are the ones you are administering. -my_account.organizations.no_results=You are not administering any organizations yet. +my_account.organizations.description=Those organizations are the ones you are member of. +my_account.organizations.no_results=You are not a member of any organizations yet. my_account.create_organization=Create Organization my_account.search_project=Search Project my_account.set_notifications_for=Set notifications for