From 027514e6f94607fcd7df8e69e668fe32aeb2873e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 13 Dec 2017 13:44:12 +0100 Subject: [PATCH] SONAR-10182 Users should be able to choose their homepage --- server/sonar-web/src/main/js/api/users.ts | 6 +- .../src/main/js/app/components/Landing.tsx | 10 +- .../main/js/app/components/help/GlobalHelp.js | 2 +- .../components/nav/component/ComponentNav.css | 19 --- .../components/nav/component/ComponentNav.tsx | 10 +- .../nav/component/ComponentNavBreadcrumbs.js | 107 ----------------- .../nav/component/ComponentNavBreadcrumbs.tsx | 111 ++++++++++++++++++ .../nav/component/ComponentNavFavorite.js | 48 -------- .../nav/component/ComponentNavMeta.tsx | 67 +++++++---- .../component/__tests__/ComponentNav-test.tsx | 4 +- ...st.js => ComponentNavBreadcrumbs-test.tsx} | 30 +++-- .../__tests__/ComponentNavMeta-test.tsx | 14 ++- .../__snapshots__/ComponentNav-test.tsx.snap | 12 -- .../ComponentNavBreadcrumbs-test.js.snap | 108 ----------------- .../ComponentNavBreadcrumbs-test.tsx.snap | 98 ++++++++++++++++ .../ComponentNavMeta-test.tsx.snap | 84 ++++++------- .../app/components/nav/global/GlobalNav.tsx | 23 ++-- .../components/nav/global/GlobalNavMenu.tsx | 2 +- .../components/nav/global/GlobalNavUser.tsx | 2 +- ...NavMenu-test.js => GlobalNavMenu-test.tsx} | 2 +- ...st.js.snap => GlobalNavMenu-test.tsx.snap} | 0 .../components/nav/settings/SettingsNav.tsx | 4 +- .../__snapshots__/SettingsNav-test.tsx.snap | 8 +- .../main/js/app/components/search/Search.css | 2 +- .../src/main/js/app/styles/init/forms.css | 2 +- .../src/main/js/app/styles/init/icons.css | 10 +- server/sonar-web/src/main/js/app/types.ts | 17 ++- .../src/main/js/apps/explore/Explore.tsx | 6 +- .../src/main/js/apps/issues/components/App.js | 8 ++ .../js/apps/issues/components/PageActions.js | 8 +- .../navigation/OrganizationNavigation.css | 4 +- .../OrganizationNavigationHeader.tsx | 46 ++++---- .../navigation/OrganizationNavigationMeta.tsx | 26 ++-- ...OrganizationNavigationHeader-test.tsx.snap | 34 +++--- .../OrganizationNavigationMeta-test.tsx.snap | 12 ++ .../apps/projects/components/AllProjects.tsx | 2 + .../apps/projects/components/PageHeader.tsx | 10 ++ .../components/__tests__/PageHeader-test.tsx | 2 + .../__snapshots__/AllProjects-test.tsx.snap | 4 + .../apps/tutorials/onboarding/Onboarding.js | 6 +- .../tutorials/onboarding/OrganizationStep.js | 2 +- .../__tests__/OrganizationStep-test.js | 4 +- .../__snapshots__/Onboarding-test.js.snap | 6 +- .../js/components/controls/FavoriteBase.tsx | 19 ++- .../js/components/controls/HomePageSelect.tsx | 90 ++++++++++++++ .../__snapshots__/FavoriteBase-test.tsx.snap | 42 ++++--- .../icons-components/FavoriteIcon.tsx | 31 +++-- .../components/icons-components/HomeIcon.tsx | 50 ++++++++ .../main/js/components/nav/ContextNavBar.css | 25 ++-- server/sonar-web/src/main/js/helpers/urls.ts | 22 +++- .../js/store/users/{actions.js => actions.ts} | 23 +++- .../js/store/users/{reducer.js => reducer.ts} | 36 ++++-- .../resources/org/sonar/l10n/core.properties | 23 +++- 53 files changed, 792 insertions(+), 551 deletions(-) delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js rename server/sonar-web/src/main/js/app/components/nav/component/__tests__/{ComponentNavBreadcrumbs-test.js => ComponentNavBreadcrumbs-test.tsx} (66%) delete mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap create mode 100644 server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap rename server/sonar-web/src/main/js/app/components/nav/global/__tests__/{GlobalNavMenu-test.js => GlobalNavMenu-test.tsx} (98%) rename server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/{GlobalNavMenu-test.js.snap => GlobalNavMenu-test.tsx.snap} (100%) create mode 100644 server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx create mode 100644 server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx rename server/sonar-web/src/main/js/store/users/{actions.js => actions.ts} (64%) rename server/sonar-web/src/main/js/store/users/{reducer.js => reducer.ts} (65%) diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index fa1b73f80fa..7b3f4de1b77 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -19,7 +19,7 @@ */ import { getJSON, post, postJSON, RequestData } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; -import { CurrentUser, Paging } from '../app/types'; +import { Paging, HomePage, CurrentUser } from '../app/types'; export interface IdentityProvider { backgroundColor: string; @@ -102,3 +102,7 @@ export function deactivateUser(data: { login: string }): Promise { export function skipOnboarding(): Promise { return post('/api/users/skip_onboarding_tutorial').catch(throwGlobalError); } + +export function setHomePage(homepage: HomePage): Promise { + return post('/api/users/set_homepage', homepage).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/app/components/Landing.tsx b/server/sonar-web/src/main/js/app/components/Landing.tsx index 792826f1091..cdf7a236ad2 100644 --- a/server/sonar-web/src/main/js/app/components/Landing.tsx +++ b/server/sonar-web/src/main/js/app/components/Landing.tsx @@ -20,8 +20,9 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer'; import { CurrentUser, isLoggedIn } from '../types'; +import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer'; +import { getHomePageUrl } from '../../helpers/urls'; interface Props { currentUser: CurrentUser; @@ -36,7 +37,12 @@ class Landing extends React.PureComponent { componentDidMount() { const { currentUser, onSonarCloud } = this.props; if (isLoggedIn(currentUser)) { - this.context.router.replace('/projects'); + if (onSonarCloud && currentUser.homepage) { + const homepage = getHomePageUrl(currentUser.homepage); + this.context.router.replace(homepage); + } else { + this.context.router.replace('/projects'); + } } else if (onSonarCloud) { window.location.href = 'https://about.sonarcloud.io'; } else { diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js index a4ec0a4c324..a20b7e80dd2 100644 --- a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js +++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js @@ -88,7 +88,7 @@ export default class GlobalHelp extends React.PureComponent { renderMenu = () => (
    - {(this.props.currentUser.isLoggedIn + {(this.props.currentUser.isLoggedIn && !this.props.onSonarCloud ? ['shortcuts', 'tutorials', 'links'] : ['shortcuts', 'links'] ).map(this.renderMenuItem)} 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 ccfd5d9c63e..fd0f3a46df1 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,17 +1,3 @@ -.navbar-context-favorite { - display: inline-block; - vertical-align: top; - padding-top: var(--gridSize); - padding-left: calc(1.5 * var(--gridSize)); -} - -.navbar-context-title-qualifier { - display: inline-block; - line-height: 16px; - padding-top: 5px; - box-sizing: border-box; -} - .navbar-context-branches { display: inline-block; vertical-align: top; @@ -20,11 +6,6 @@ line-height: 16px; } -.navbar-context-meta-branch { - margin-top: 3px; - line-height: 16px; -} - .navbar-context-meta-branch-menu-item { display: flex !important; justify-content: space-between; 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 2dbc6fe9ea4..ff2205adbb6 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 @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import ComponentNavFavorite from './ComponentNavFavorite'; import ComponentNavBranch from './ComponentNavBranch'; import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs'; import ComponentNavMeta from './ComponentNavMeta'; @@ -112,14 +111,7 @@ export default class ComponentNav extends React.PureComponent { id="context-navigation" height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - - + {this.props.currentBranch && ( { - const isPath = item.qualifier === 'DIR'; - const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name); - return ( - - {index === 0 && ( - - - - )} - - {itemName} - - {index < breadcrumbs.length - 1 && } - - ); - }); - - return ( -

    - - {displayOrganization && ( - - - - {organization.name} - - - - )} - {items} - {component.visibility === 'private' && ( - - )} -

    - ); - } -} - -const mapStateToProps = (state, ownProps) => ({ - organization: - ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization), - shouldOrganizationBeDisplayed: areThereCustomOrganizations(state) -}); - -export default connect(mapStateToProps)(ComponentNavBreadcrumbs); - -export const Unconnected = ComponentNavBreadcrumbs; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx new file mode 100644 index 00000000000..c8a75e8a105 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx @@ -0,0 +1,111 @@ +/* + * 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 { connect } from 'react-redux'; +import { Link } from 'react-router'; +import { Component, Organization } from '../../../types'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; +import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer'; +import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; +import OrganizationHelmet from '../../../../components/common/OrganizationHelmet'; +import OrganizationLink from '../../../../components/ui/OrganizationLink'; +import PrivateBadge from '../../../../components/common/PrivateBadge'; +import { collapsePath, limitComponentName } from '../../../../helpers/path'; +import { getProjectUrl } from '../../../../helpers/urls'; + +interface StateProps { + organization?: Organization; + shouldOrganizationBeDisplayed: boolean; +} + +interface OwnProps { + component: Component; +} + +interface Props extends StateProps, OwnProps {} + +export function ComponentNavBreadcrumbs(props: Props) { + const { component, organization, shouldOrganizationBeDisplayed } = props; + const { breadcrumbs } = component; + + const lastItem = breadcrumbs[breadcrumbs.length - 1]; + + const items: JSX.Element[] = []; + breadcrumbs.forEach((item, index) => { + const isPath = item.qualifier === 'DIR'; + const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name); + + if (index === 0) { + items.push( + + ); + } + + items.push( + + {itemName} + + ); + + if (index < breadcrumbs.length - 1) { + items.push(); + } + }); + + return ( +
    + + {organization && + shouldOrganizationBeDisplayed && } + {organization && + shouldOrganizationBeDisplayed && ( + + {organization.name} + + )} + {organization && shouldOrganizationBeDisplayed && } + {items} + {component.visibility === 'private' && ( + + )} +
    + ); +} + +const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({ + organization: + ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization), + shouldOrganizationBeDisplayed: areThereCustomOrganizations(state) +}); + +export default connect(mapStateToProps)(ComponentNavBreadcrumbs); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js deleted file mode 100644 index 9d6c59c000b..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js +++ /dev/null @@ -1,48 +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 PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import Favorite from '../../../../components/controls/Favorite'; -import { getCurrentUser } from '../../../../store/rootReducer'; - -class ComponentNavFavorite extends React.PureComponent { - static propTypes = { - currentUser: PropTypes.object.isRequired - }; - - render() { - if (!this.props.currentUser.isLoggedIn) { - return null; - } - - return ( -
    - -
    - ); - } -} - -const mapStateToProps = state => ({ - currentUser: getCurrentUser(state) -}); - -export default connect(mapStateToProps)(ComponentNavFavorite); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx index 0e157ebe219..01be74777a6 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx @@ -18,47 +18,62 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { connect } from 'react-redux'; +import { Branch, Component, CurrentUser, isLoggedIn } from '../../../types'; import BranchStatus from '../../../../components/common/BranchStatus'; -import { Branch, Component } from '../../../types'; import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; +import Favorite from '../../../../components/controls/Favorite'; +import HomePageSelect from '../../../../components/controls/HomePageSelect'; import Tooltip from '../../../../components/controls/Tooltip'; import { isShortLivingBranch } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; +import { getCurrentUser } from '../../../../store/rootReducer'; -interface Props { +interface StateProps { + currentUser: CurrentUser; +} + +interface Props extends StateProps { branch?: Branch; component: Component; } -export default function ComponentNavMeta(props: Props) { - const shortBranch = props.branch && isShortLivingBranch(props.branch); - const showVersion = props.component.version && !shortBranch; +export function ComponentNavMeta({ branch, component, currentUser }: Props) { + const shortBranch = branch && isShortLivingBranch(branch); + const mainBranch = !branch || branch.isMain; return (
    -
      - {props.component.analysisDate && ( -
    • - -
    • - )} - {showVersion && ( -
    • - - - {translate('version')} {props.component.version} - - -
    • - )} -
    - {shortBranch && ( -
    - + {component.analysisDate && ( +
    +
    )} + {component.version && + !shortBranch && ( + +
    + {translate('version')} {component.version} +
    +
    + )} + {isLoggedIn(currentUser) && + mainBranch && ( +
    + + +
    + )} + {shortBranch && }
    ); } + +const mapStateToProps = (state: any): StateProps => ({ + currentUser: getCurrentUser(state) +}); + +export default connect(mapStateToProps)(ComponentNavMeta); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx index 170bce7eb27..26f876d34f0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx @@ -22,9 +22,9 @@ import * as React from 'react'; import { mount, shallow } from 'enzyme'; import ComponentNav from '../ComponentNav'; -jest.mock('../ComponentNavFavorite', () => ({ +jest.mock('../ComponentNavMeta', () => ({ // eslint-disable-next-line - default: function ComponentNavFavorite() { + default: function ComponentNavMeta() { return null; } })); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx similarity index 66% rename from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js rename to server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx index 3a524a9ee61..78a2eed1234 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx @@ -17,35 +17,42 @@ * 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 { Unconnected } from '../ComponentNavBreadcrumbs'; +import { ComponentNavBreadcrumbs } from '../ComponentNavBreadcrumbs'; +import { Visibility } from '../../../../types'; it('should not render breadcrumbs with one element', () => { const component = { + breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }], key: 'my-project', name: 'My Project', + organization: 'org', qualifier: 'TRK', visibility: 'public' }; - const breadcrumbs = [component]; - const result = shallow(); + const result = shallow( + + ); expect(result).toMatchSnapshot(); }); it('should render organization', () => { const component = { + breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }], key: 'my-project', name: 'My Project', organization: 'foo', qualifier: 'TRK', visibility: 'public' }; - const breadcrumbs = [component]; - const organization = { key: 'foo', name: 'The Foo Organization' }; + const organization = { + key: 'foo', + name: 'The Foo Organization', + projectVisibility: Visibility.Public + }; const result = shallow( - { it('renders private badge', () => { const component = { + breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }], key: 'my-project', name: 'My Project', + organization: 'org', qualifier: 'TRK', visibility: 'private' }; - const breadcrumbs = [component]; - const result = shallow(); + const result = shallow( + + ); expect(result.find('PrivateBadge')).toHaveLength(1); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx index 331c7c034e7..b989df12142 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import ComponentNavMeta from '../ComponentNavMeta'; +import { ComponentNavMeta } from '../ComponentNavMeta'; import { BranchType, ShortLivingBranch, LongLivingBranch } from '../../../../types'; const component = { @@ -40,7 +40,11 @@ it('renders status of short-living branch', () => { status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, type: BranchType.SHORT }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); }); it('renders meta for long-living branch', () => { @@ -50,5 +54,9 @@ it('renders meta for long-living branch', () => { status: { qualityGateStatus: 'OK' }, type: BranchType.LONG }; - expect(shallow()).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); }); 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 1baaad7dc1f..6fea2eff38e 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 @@ -28,15 +28,6 @@ exports[`renders 1`] = ` } > - - - - - - - - My Project - - - -`; - -exports[`should render organization 1`] = ` -

    - - - - - The Foo Organization - - - - - - - - - My Project - - -

    -`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap new file mode 100644 index 00000000000..e907a97d1b5 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render breadcrumbs with one element 1`] = ` +
    + + + + My Project + +
    +`; + +exports[`should render organization 1`] = ` +
    + + + + The Foo Organization + + + + + My Project + +
    +`; diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap index a1dd339dfce..726f3168f9f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap @@ -4,30 +4,26 @@ exports[`renders meta for long-living branch 1`] = `
    -
      + +
    + -
  • - -
  • -
  • - - - version - - 0.0.1 - - -
  • -
+
+ version + + 0.0.1 +
+ `; @@ -35,33 +31,27 @@ exports[`renders status of short-living branch 1`] = `
-
    -
  • - -
  • -
-
+
`; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx index 04404a1fe1e..107251d3629 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx @@ -110,6 +110,15 @@ class GlobalNav extends React.PureComponent { }, 3000); }; + withTutorialTooltip = (element: React.ReactNode) => + this.state.onboardingTutorialTooltip ? ( + + {element} + + ) : ( + element + ); + render() { return ( @@ -121,21 +130,13 @@ class GlobalNav extends React.PureComponent {
  • - {this.state.onboardingTutorialTooltip ? ( - - - - ) : ( - - )} + {this.props.onSonarCloud ? : this.withTutorialTooltip()}
  • {isLoggedIn(this.props.currentUser) && - this.props.onSonarCloud && ( + this.props.onSonarCloud && + this.withTutorialTooltip( )} diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index 50277e52187..ba7dfcdbf64 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -28,7 +28,7 @@ interface Props { appState: AppState; currentUser: CurrentUser; location: { pathname: string }; - onSonarCloud: boolean; + onSonarCloud?: boolean; } export default class GlobalNavMenu extends React.PureComponent { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index fb19ba06d40..b2e8119ec2a 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -31,7 +31,7 @@ import { getBaseUrl } from '../../../../helpers/urls'; import Dropdown from '../../../../components/controls/Dropdown'; interface Props { - appState: { organizationsEnabled: boolean }; + appState: { organizationsEnabled?: boolean }; currentUser: CurrentUser; organizations: Organization[]; } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx similarity index 98% rename from server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js rename to server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx index 33546be3731..7b3afe86905 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import GlobalNavMenu from '../GlobalNavMenu'; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap similarity index 100% rename from server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap rename to server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap 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 54d91d77c75..f45d95441dc 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 @@ -206,7 +206,9 @@ export default class SettingsNav extends React.PureComponent { id="context-navigation" height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> -

    {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 3208552f512..64822b179f6 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 @@ -5,11 +5,13 @@ exports[`should work with extensions 1`] = ` height={72} id="context-navigation" > -

    - layout.settings -

    +

    + layout.settings +

    +
  • .icon-star, +.navbar-search-item-icons > .icon-outline, .navbar-search-item-icons > .icon-clock { z-index: 6; top: -4px; diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css index 97287e21ac0..d63fe1e3b15 100644 --- a/server/sonar-web/src/main/js/app/styles/init/forms.css +++ b/server/sonar-web/src/main/js/app/styles/init/forms.css @@ -304,7 +304,7 @@ input[type='submit'].button-grey.button-active { } .button-small > svg { - margin-top: 2px; + padding-top: 2px; } .button-group { diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css index 315803eb185..2033873f784 100644 --- a/server/sonar-web/src/main/js/app/styles/init/icons.css +++ b/server/sonar-web/src/main/js/app/styles/init/icons.css @@ -476,11 +476,11 @@ a:hover > .icon-radio { background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M15.428%205.777c0%20.13-.078.274-.233.428l-3.24%203.16.767%204.465c.006.042.01.102.01.18%200%20.124-.032.23-.095.316-.062.086-.153.13-.272.13-.113%200-.232-.036-.357-.108l-4.01-2.107L3.99%2014.35c-.13.072-.25.107-.357.107-.125%200-.22-.043-.28-.13-.064-.085-.095-.19-.095-.316%200-.037.006-.096.018-.18l.768-4.464-3.25-3.16C.644%206.045.57%205.9.57%205.775c0-.22.167-.356.5-.41l4.482-.652L7.562.652c.112-.244.258-.366.437-.366.177%200%20.323.122.436.366l2.01%204.062%204.48.652c.335.054.5.19.5.41h.002z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E'); } -.icon-star { +.icon-outline { transition: all 0.2s ease !important; } -.icon-star path { +.icon-outline path { stroke: var(--secondFontColor); stroke-width: 1.41421356; stroke-opacity: 1; @@ -488,9 +488,9 @@ a:hover > .icon-radio { transition: all 0.2s ease; } -.icon-star-favorite path { - fill: #ff9900; - stroke-opacity: 0; +.icon-outline.is-filled path { + fill: currentColor; + stroke: currentColor; fill-opacity: 1; } diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 2b2bc7a64a8..70df316dea2 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -80,6 +80,7 @@ export interface Component { qualifier: string; refKey?: string; version?: string; + visibility?: string; } interface ComponentConfiguration { @@ -140,9 +141,19 @@ export interface CurrentUser { showOnboardingTutorial?: boolean; } +export interface HomePage { + key?: string; + type: string; +} + +export function isSameHomePage(a: HomePage, b: HomePage) { + return a.type === b.type && a.key === b.key; +} + export interface LoggedInUser extends CurrentUser { avatar?: string; email?: string; + homepage?: HomePage; isLoggedIn: true; name: string; } @@ -153,10 +164,10 @@ export function isLoggedIn(user: CurrentUser): user is LoggedInUser { export interface AppState { adminPages?: Extension[]; - authenticationError: boolean; - authorizationError: boolean; + authenticationError?: boolean; + authorizationError?: boolean; canAdmin?: boolean; globalPages?: Extension[]; - organizationsEnabled: boolean; + organizationsEnabled?: boolean; qualifiers: string[]; } 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 a1ed154d36a..62510555da8 100644 --- a/server/sonar-web/src/main/js/apps/explore/Explore.tsx +++ b/server/sonar-web/src/main/js/apps/explore/Explore.tsx @@ -32,9 +32,9 @@ export default function Explore(props: Props) { return (
    -
    -

    {translate('explore')}

    -
    +
    +

    {translate('explore')}

    +
  • diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index a7121ced22d..cc68fbb41c9 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -56,6 +56,7 @@ import { CurrentUser } from '../utils'; */ import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; +import { isLoggedIn } from '../../../app/types'; import ListFooter from '../../../components/controls/ListFooter'; import EmptySearch from '../../../components/common/EmptySearch'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; @@ -923,6 +924,13 @@ export default class App extends React.PureComponent { ) : ( void, paging: ?Paging, @@ -70,6 +72,10 @@ export default class PageActions extends React.PureComponent { )} + + {this.props.canSetHome && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css index f61bbd33bd2..05af0bd0bbe 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css @@ -22,9 +22,9 @@ } .organization-switch .dropdown-toggle { - display: block; + display: flex; + align-items: center; height: calc(4 * var(--gridSize)); - line-height: calc(4 * var(--gridSize) - 2px); padding: 0 var(--gridSize); border: 1px solid transparent; border-radius: 2px; diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx index 84f0256e579..22e7d9e480d 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx @@ -35,29 +35,27 @@ export default function OrganizationNavigationHeader({ organization, organizatio const other = organizations.filter(o => o.key !== organization.key); return ( -
    -

    - - {other.length ? ( - - {({ onToggleClick, open }) => ( -
    - - {organization.name} - - -
      - {sortBy(other, org => org.name.toLowerCase()).map(organization => ( - - ))} -
    -
    - )} -
    - ) : ( - {organization.name} - )} -

    +
    + + {other.length ? ( + + {({ onToggleClick, open }) => ( +
    + + {organization.name} + + +
      + {sortBy(other, org => org.name.toLowerCase()).map(organization => ( + + ))} +
    +
    + )} +
    + ) : ( + {organization.name} + )} {organization.description != null && (

    @@ -65,6 +63,6 @@ export default function OrganizationNavigationHeader({ organization, organizatio

    )} -
    + ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx index b720223abe1..077cc4a67ee 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { Organization } from '../../../app/types'; +import HomePageSelect from '../../../components/controls/HomePageSelect'; import { translate } from '../../../helpers/l10n'; interface Props { @@ -28,22 +29,21 @@ interface Props { export default function OrganizationNavigationMeta({ organization }: Props) { return (
    + {organization.url != null && ( + + {organization.url} + + )}
    {translate('organization.key')}: {organization.key}
    - {organization.url != null && ( - - )} +
    + +
    ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap index 78f6ea2ae7c..949bb39ff04 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap @@ -1,28 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders 1`] = ` -
    -

    - - - Foo - -

    -
    + } + /> + + Foo + + `; exports[`renders dropdown 1`] = ` diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap index 3da71c25930..bdd7594127b 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap @@ -14,5 +14,17 @@ exports[`renders 1`] = ` foo +
    + +
    `; diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index daa3ffa6fa4..15dc8f4e7ca 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -263,9 +263,11 @@ export default class AllProjects extends React.PureComponent {
    void; onQueryChange: (change: RawQuery) => void; + onSonarCloud: boolean; onSortChange: (sort: string, desc: boolean) => void; organization?: { key: string }; projects?: Project[]; @@ -97,6 +100,13 @@ export default function PageHeader(props: Props) { )}
    + + {props.onSonarCloud && + isLoggedIn(currentUser) && + props.isFavorite && + !props.organization && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx index ec11f741410..2c2f8ab5333 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx @@ -71,9 +71,11 @@ function shallowRender(props?: {}) { return shallow( )} -

    {translate('tutorials.find_it_back_in_help')}

    +

    + {translate( + sonarCloud ? 'tutorials.find_it_back_in_plus' : 'tutorials.find_it_back_in_help' + )} +

    {translateWithParameters( diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js index 4db0fec476c..3ff62c4f7d8 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js @@ -71,7 +71,7 @@ export default class OrganizationStep extends React.PureComponent { getOrganizations({ member: true }).then( ({ organizations }) => { if (this.mounted) { - const organizationKeys = organizations.map(o => o.key); + const organizationKeys = organizations.filter(o => o.isAdmin).map(o => o.key); // best guess: if there is only one organization, then it is personal // otherwise, we can't guess, let's display them all as just "existing organizations" const personalOrganization = diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js index be9c2542a87..dda967a28bf 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js @@ -26,7 +26,9 @@ import { getOrganizations } from '../../../../api/organizations'; jest.mock('../../../../api/organizations', () => ({ getOrganizations: jest.fn(() => - Promise.resolve({ organizations: [{ key: 'user' }, { key: 'another' }] }) + Promise.resolve({ + organizations: [{ isAdmin: true, key: 'user' }, { isAdmin: true, key: 'another' }] + }) ) })); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap index 9c24b5b42a4..57a706a0d5d 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap @@ -169,7 +169,7 @@ exports[`guides for sonarcloud 1`] = `

    - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus

    - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus

    - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus

    Promise; @@ -80,13 +82,18 @@ export default class FavoriteBase extends React.PureComponent { } render() { + const tooltip = this.state.favorite + ? translate('favorite.current') + : translate('favorite.check'); return ( - - - + + + + + ); } } diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx new file mode 100644 index 00000000000..9d68ef7e1fc --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; +import Tooltip from './Tooltip'; +import HomeIcon from '../icons-components/HomeIcon'; +import { CurrentUser, isLoggedIn, HomePage, isSameHomePage } from '../../app/types'; +import { translate } from '../../helpers/l10n'; +import { getCurrentUser } from '../../store/rootReducer'; +import { setHomePage } from '../../store/users/actions'; + +interface StateProps { + currentUser: CurrentUser; +} + +interface DispatchProps { + setHomePage: (homepage: HomePage) => void; +} + +interface Props extends StateProps, DispatchProps { + className?: string; + currentPage: HomePage; +} + +class HomePageSelect extends React.PureComponent { + handleClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.setHomePage(this.props.currentPage); + }; + + render() { + const { currentPage, currentUser } = this.props; + + if (!isLoggedIn(currentUser)) { + return null; + } + + const { homepage } = currentUser; + const checked = homepage !== undefined && isSameHomePage(homepage, currentPage); + const tooltip = checked ? translate('homepage.current') : translate('homepage.check'); + + return ( + + {checked ? ( + + + + ) : ( + + + + )} + + ); + } +} + +const mapStateToProps = (state: any): StateProps => ({ + currentUser: getCurrentUser(state) +}); + +const mapDispatchToProps: DispatchProps = { setHomePage }; + +export default connect(mapStateToProps, mapDispatchToProps)(HomePageSelect); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap index 98dc0d71b5b..bb842b986b0 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap @@ -1,25 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render favorite 1`] = ` - - - + + + + `; exports[`should render not favorite 1`] = ` - - - + + + + `; diff --git a/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx index a07533ae13f..abf8844a6cf 100644 --- a/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx @@ -19,19 +19,32 @@ */ import * as React from 'react'; import * as classNames from 'classnames'; +import { IconProps } from './types'; +import * as theme from '../../app/theme'; -interface Props { - className?: string; +export interface Props extends IconProps { favorite: boolean; - size?: number; } -export default function FavoriteIcon({ className, favorite, size = 16 }: Props) { +export default function FavoriteIcon({ + className, + favorite, + fill = theme.orange, + size = 16 +}: Props) { return ( - - - - - + + + + + ); } diff --git a/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx new file mode 100644 index 00000000000..c63d84f20b0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx @@ -0,0 +1,50 @@ +/* + * 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 { IconProps } from './types'; +import * as theme from '../../app/theme'; + +export interface Props extends IconProps { + filled?: boolean; +} + +export default function HomeIcon({ + className, + fill = theme.orange, + filled = false, + size = 16 +}: Props) { + return ( + + + + + + ); +} 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 489747900bf..81a469b239b 100644 --- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css +++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css @@ -14,21 +14,13 @@ } .navbar-context-header { - display: inline-block; + display: inline-flex; + align-items: center; 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; @@ -42,13 +34,22 @@ position: absolute; top: 0; right: 0; - line-height: calc(4 * var(--gridSize)); - padding: 0 10px; + display: flex; + align-items: center; + height: calc(4 * var(--gridSize)); + padding: 0 20px; color: var(--secondFontColor); font-size: var(--smallFontSize); text-align: right; } +.navbar-context-meta-secondary { + position: absolute; + top: 36px; + right: 0; + padding: 0 20px; +} + .navbar-context-description { display: inline-block; line-height: var(--controlHeight); diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index ffe6fa17794..10ab02703cb 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -21,7 +21,7 @@ import { stringify } from 'querystring'; import { omitBy, isNil } from 'lodash'; import { isShortLivingBranch } from './branches'; import { getProfilePath } from '../apps/quality-profiles/utils'; -import { Branch } from '../app/types'; +import { Branch, HomePage } from '../app/types'; interface Query { [x: string]: string | undefined; @@ -167,3 +167,23 @@ export function getMarkdownHelpUrl(): string { export function getCodeUrl(project: string, branch?: string, selected?: string) { return { pathname: '/code', query: { id: project, branch, selected } }; } + +export function getOrganizationUrl(organization: string) { + return `/organizations/${organization}`; +} + +export function getHomePageUrl(homepage: HomePage) { + switch (homepage.type) { + case 'project': + return getProjectUrl(homepage.key!); + case 'organization': + return getOrganizationUrl(homepage.key!); + case 'my-projects': + return '/projects'; + case 'my-issues': + return { pathname: '/issues', query: { resolved: 'false' } }; + } + + // should never happen, but just in case... + return '/projects'; +} diff --git a/server/sonar-web/src/main/js/store/users/actions.js b/server/sonar-web/src/main/js/store/users/actions.ts similarity index 64% rename from server/sonar-web/src/main/js/store/users/actions.js rename to server/sonar-web/src/main/js/store/users/actions.ts index aaee4498138..c55fff019c9 100644 --- a/server/sonar-web/src/main/js/store/users/actions.js +++ b/server/sonar-web/src/main/js/store/users/actions.ts @@ -17,23 +17,36 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getCurrentUser } from '../../api/users'; +import { Dispatch } from 'redux'; +import * as api from '../../api/users'; +import { CurrentUser, HomePage } from '../../app/types'; export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; export const RECEIVE_USER = 'RECEIVE_USER'; export const SKIP_ONBOARDING = 'SKIP_ONBOARDING'; +export const SET_HOMEPAGE = 'SET_HOMEPAGE'; -export const receiveCurrentUser = user => ({ +export const receiveCurrentUser = (user: CurrentUser) => ({ type: RECEIVE_CURRENT_USER, user }); -export const receiveUser = user => ({ +export const receiveUser = (user: any) => ({ type: RECEIVE_USER, user }); export const skipOnboarding = () => ({ type: SKIP_ONBOARDING }); -export const fetchCurrentUser = () => dispatch => - getCurrentUser().then(user => dispatch(receiveCurrentUser(user))); +export const fetchCurrentUser = () => (dispatch: Dispatch) => { + return api.getCurrentUser().then(user => dispatch(receiveCurrentUser(user))); +}; + +export const setHomePage = (homepage: HomePage) => (dispatch: Dispatch) => { + api.setHomePage(homepage).then( + () => { + dispatch({ type: SET_HOMEPAGE, homepage }); + }, + () => {} + ); +}; diff --git a/server/sonar-web/src/main/js/store/users/reducer.js b/server/sonar-web/src/main/js/store/users/reducer.ts similarity index 65% rename from server/sonar-web/src/main/js/store/users/reducer.js rename to server/sonar-web/src/main/js/store/users/reducer.ts index 79472f1f8d3..1cee5bae5e9 100644 --- a/server/sonar-web/src/main/js/store/users/reducer.js +++ b/server/sonar-web/src/main/js/store/users/reducer.ts @@ -19,10 +19,15 @@ */ import { combineReducers } from 'redux'; import { uniq, keyBy } from 'lodash'; -import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING } from './actions'; +import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING, SET_HOMEPAGE } from './actions'; import { actions as membersActions } from '../organizationsMembers/actions'; +import { CurrentUser } from '../../app/types'; -const usersByLogin = (state = {}, action = {}) => { +interface UsersByLogin { + [login: string]: any; +} + +const usersByLogin = (state: UsersByLogin = {}, action: any = {}) => { switch (action.type) { case RECEIVE_CURRENT_USER: case RECEIVE_USER: @@ -37,14 +42,16 @@ const usersByLogin = (state = {}, action = {}) => { } }; -const userLogins = (state = [], action = {}) => { +type UserLogins = string[]; + +const userLogins = (state: UserLogins = [], action: any = {}) => { switch (action.type) { case RECEIVE_CURRENT_USER: case RECEIVE_USER: return uniq([...state, action.user.login]); case membersActions.RECEIVE_MEMBERS: case membersActions.RECEIVE_MORE_MEMBERS: - return uniq([...state, action.members.map(member => member.login)]); + return uniq([...state, action.members.map((member: any) => member.login)]); case membersActions.ADD_MEMBER: { return uniq([...state, action.member.login]).sort(); } @@ -53,21 +60,30 @@ const userLogins = (state = [], action = {}) => { } }; -const currentUser = (state = null, action = {}) => { +const currentUser = (state: CurrentUser | null = null, action: any = {}) => { if (action.type === RECEIVE_CURRENT_USER) { return action.user; } if (action.type === SKIP_ONBOARDING) { return state ? { ...state, showOnboardingTutorial: false } : null; } + if (action.type === SET_HOMEPAGE) { + return state && { ...state, homepage: action.homepage }; + } return state; }; +interface State { + usersByLogin: UsersByLogin; + userLogins: UserLogins; + currentUser: CurrentUser | null; +} + export default combineReducers({ usersByLogin, userLogins, currentUser }); -export const getCurrentUser = state => state.currentUser; -export const getUserLogins = state => state.userLogins; -export const getUserByLogin = (state, login) => state.usersByLogin[login]; -export const getUsersByLogins = (state, logins) => +export const getCurrentUser = (state: State) => state.currentUser!; +export const getUserLogins = (state: State) => state.userLogins; +export const getUserByLogin = (state: State, login: string) => state.usersByLogin[login]; +export const getUsersByLogins = (state: State, logins: string[]) => logins.map(login => getUserByLogin(state, login)); -export const getUsers = state => getUsersByLogins(state, getUserLogins(state)); +export const getUsers = (state: State) => getUsersByLogins(state, getUserLogins(state)); 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 1fa402c992e..b23f715de55 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -893,8 +893,9 @@ shortcuts.section.rules.deactivate=deactivate selected rule tutorials.onboarding=Analyze a new project tutorials.skip=Skip this tutorial tutorials.finish=Finish this tutorial -tutorials.follow_later=Follow the tutorial later in the Help section +tutorials.follow_later=You can always follow the tutorial later tutorials.find_it_back_in_help=Find it back anytime in the Help section +tutorials.find_it_back_in_plus=Find it back anytime in the "+" menu #------------------------------------------------------------------------------ @@ -2717,3 +2718,23 @@ maintenance.sonarqube_is_up=SonarQube is up maintenance.all_systems_opetational=All systems operational. maintenance.sonarqube_is_offline=SonarQube is offline maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Please contact your system administrator. + + + +#------------------------------------------------------------------------------ +# +# HOMEPAGE +# +#------------------------------------------------------------------------------ +homepage.current=This page is your homepage. Click on the top-left logo to find it anytime. +homepage.check=Check to make the current page your homepage + + + +#------------------------------------------------------------------------------ +# +# FAVORITE +# +#------------------------------------------------------------------------------ +favorite.current=This is your favorite component. Click to unset. +favorite.check=Click to mark this component as favorite. -- 2.39.5