From aa02b659cf9920ab6757611706b0dcf3286894da Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 30 Nov 2017 13:41:38 +0100 Subject: [PATCH] SONAR-8829 move organization avatar to the left --- .../components/nav/component/ComponentNav.css | 13 ++- .../components/nav/component/ComponentNav.tsx | 11 +- .../nav/component/ComponentNavBreadcrumbs.js | 29 ++--- .../__snapshots__/ComponentNav-test.tsx.snap | 8 +- .../ComponentNavBreadcrumbs-test.js.snap | 36 +++--- .../{GlobalNavUser.js => GlobalNavUser.tsx} | 99 ++++++++-------- ...ontainer.js => GlobalNavUserContainer.tsx} | 19 ++- ...NavUser-test.js => GlobalNavUser-test.tsx} | 24 ++-- ...st.js.snap => GlobalNavUser-test.tsx.snap} | 36 +++++- .../components/nav/settings/SettingsNav.tsx | 7 +- .../__snapshots__/SettingsNav-test.tsx.snap | 6 +- server/sonar-web/src/main/js/app/theme.js | 20 ++-- ...Organizations.js => UserOrganizations.tsx} | 63 +++++----- .../organizations/{actions.js => actions.ts} | 7 +- .../src/main/js/apps/explore/Explore.tsx | 3 +- .../navigation/OrganizationNavigation.js | 19 +-- .../OrganizationNavigation-test.js.snap | 108 +++++++++++++----- .../components/common/OrganizationAvatar.css | 48 ++++++++ .../components/common/OrganizationAvatar.tsx | 47 ++++++++ .../icons-components/OrganizationIcon.tsx | 40 ------- .../js/components/icons-components/icons.ts | 1 - .../main/js/components/nav/ContextNavBar.css | 29 ++++- .../src/main/js/components/nav/NavBarTabs.css | 6 +- .../src/main/js/components/ui/Avatar.tsx | 90 +++------------ .../main/js/components/ui/GenericAvatar.tsx | 81 +++++++++++++ .../__snapshots__/Avatar-test.tsx.snap | 23 +--- .../resources/org/sonar/l10n/core.properties | 4 +- 27 files changed, 533 insertions(+), 344 deletions(-) rename server/sonar-web/src/main/js/app/components/nav/global/{GlobalNavUser.js => GlobalNavUser.tsx} (73%) rename server/sonar-web/src/main/js/app/components/nav/global/{GlobalNavUserContainer.js => GlobalNavUserContainer.tsx} (75%) rename server/sonar-web/src/main/js/app/components/nav/global/__tests__/{GlobalNavUser-test.js => GlobalNavUser-test.tsx} (86%) rename server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/{GlobalNavUser-test.js.snap => GlobalNavUser-test.tsx.snap} (87%) rename server/sonar-web/src/main/js/apps/account/organizations/{UserOrganizations.js => UserOrganizations.tsx} (76%) rename server/sonar-web/src/main/js/apps/account/organizations/{actions.js => actions.ts} (84%) create mode 100644 server/sonar-web/src/main/js/components/common/OrganizationAvatar.css create mode 100644 server/sonar-web/src/main/js/components/common/OrganizationAvatar.tsx delete mode 100644 server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx create mode 100644 server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx 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.tsx similarity index 73% rename from server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 3b577558541..3a5a143f963 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.tsx @@ -17,80 +17,72 @@ * 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 * 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 OrganizationIcon from '../../../../components/icons-components/OrganizationIcon'; import OrganizationLink from '../../../../components/ui/OrganizationLink'; import { translate } from '../../../../helpers/l10n'; +import { getBaseUrl } from '../../../../helpers/urls'; +import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; -/*:: -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 }; +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 /*: { target: HTMLElement } */) => { - if (!this.node || !this.node.contains(event.target)) { + handleClickOutside = (event: MouseEvent) => { + if (!this.node || !this.node.contains(event.target as Node)) { this.closeDropdown(); } }; - handleLogin = (e /*: Event */) => { - e.preventDefault(); - const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`; + 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 = - window.baseUrl + `/sessions/new?return_to=${returnTo}${window.location.hash}`; + window.location.href = + getBaseUrl() + `/sessions/new?return_to=${returnTo}${window.location.hash}`; } else { - window.location = `${window.baseUrl}/sessions/new`; + window.location.href = `${getBaseUrl()}/sessions/new`; } }; - handleLogout = (e /*: Event */) => { - e.preventDefault(); + handleLogout = (event: React.SyntheticEvent) => { + event.preventDefault(); this.closeDropdown(); - this.props.router.push('/sessions/logout'); + this.context.router.push('/sessions/logout'); }; - toggleDropdown = (evt /*: Event */) => { - evt.preventDefault(); + toggleDropdown = (event: React.SyntheticEvent) => { + event.preventDefault(); if (this.state.open) { this.closeDropdown(); } else { @@ -118,7 +110,8 @@ export default class GlobalNavUser extends React.PureComponent { }; renderAuthenticated() { - const { currentUser, organizations } = this.props; + const { organizations } = this.props; + const currentUser = this.props.currentUser as LoggedInUser; const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; return (
  • - + {organization.name}
    - {organization.isAdmin && ( + {organization.canAdmin && ( {translate('admin')} )} @@ -199,6 +192,6 @@ export default class GlobalNavUser extends React.PureComponent { } render() { - return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous(); + 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.tsx similarity index 75% rename from server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js rename to server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx index c84bdff2a7a..f73200791a0 100644 --- 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.tsx @@ -17,19 +17,26 @@ * 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 { Organization } from '../../../types'; import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions'; import { getMyOrganizations } from '../../../../store/rootReducer'; -const mapStateToProps = state => ({ +interface StateProps { + organizations: Organization[]; +} + +const mapStateToProps = (state: any): StateProps => ({ organizations: getMyOrganizations(state) }); +interface DispatchProps { + fetchMyOrganizations: () => Promise; +} + const mapDispatchToProps = { - fetchMyOrganizations -}; + fetchMyOrganizations: fetchMyOrganizations as any +} as DispatchProps; -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser)); +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.tsx similarity index 86% rename from server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js rename to server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx index 4fb4f28f2d2..1ffec34be8f 100644 --- 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.tsx @@ -17,15 +17,15 @@ * 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 * 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' }, - { key: 'foo', name: 'Foo' }, - { key: 'bar', name: 'bar' } + { key: 'myorg', name: 'MyOrg', projectVisibility: 'public' }, + { key: 'foo', name: 'Foo', projectVisibility: 'public' }, + { key: 'bar', name: 'bar', projectVisibility: 'public' } ]; const appState = { organizationsEnabled: true }; @@ -35,7 +35,7 @@ it('should render the right interface for anonymous user', () => { {}} + fetchMyOrganizations={jest.fn()} organizations={[]} /> ); @@ -47,7 +47,7 @@ it('should render the right interface for logged in user', () => { {}} + fetchMyOrganizations={jest.fn()} organizations={[]} /> ); @@ -60,7 +60,7 @@ it('should render the users organizations', () => { {}} + fetchMyOrganizations={jest.fn()} organizations={organizations} /> ); @@ -73,7 +73,7 @@ it('should not render the users organizations when they are not activated', () = {}} + fetchMyOrganizations={jest.fn()} organizations={organizations} /> ); @@ -109,9 +109,9 @@ it('should lazyload the organizations when opening the dropdown', () => { /> ); expect(fetchMyOrganizations.mock.calls.length).toBe(0); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(1); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(2); }); @@ -125,11 +125,11 @@ it('should update the organizations when the user changes', () => { organizations={organizations} /> ); - wrapper.instance().openDropdown(); + (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().openDropdown(); + (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.tsx.snap similarity index 87% rename from server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap rename to server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap index 85ca26d59b2..3bb56aac675 100644 --- 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.tsx.snap @@ -207,11 +207,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "bar", "name": "bar", + "projectVisibility": "public", } } >
    - + @@ -230,11 +240,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "foo", "name": "Foo", + "projectVisibility": "public", } } >
    - + @@ -253,11 +273,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "myorg", "name": "MyOrg", + "projectVisibility": "public", } } >
    - + 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<*> - }; -*/ +import { Organization } from '../../../app/types'; - state /*: { loading: boolean } */ = { - loading: true - }; +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(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); + ]).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'; @@ -103,15 +110,15 @@ class UserOrganizations extends React.PureComponent { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any): StateProps => ({ anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'), canAdmin: getAppState(state).canAdmin, organizations: getMyOrganizations(state) }); const mapDispatchToProps = { - fetchMyOrganizations, - fetchIfAnyoneCanCreateOrganizations -}; + 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.ts similarity index 84% rename from server/sonar-web/src/main/js/apps/account/organizations/actions.js rename to server/sonar-web/src/main/js/apps/account/organizations/actions.ts index 38f30b159b0..f93bcdd1633 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts @@ -17,19 +17,20 @@ * 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 => { +export const fetchMyOrganizations = () => (dispatch: Dispatch) => { return getOrganizations({ member: true }).then(({ organizations }) => { return dispatch(receiveMyOrganizations(organizations)); }); }; -export const fetchIfAnyoneCanCreateOrganizations = () => dispatch => { +export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch) => { return getValues('sonar.organizations.anyoneCanCreate').then(values => { - dispatch(receiveValues(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 -- 2.39.5