diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-12-13 13:44:12 +0100 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-01-02 10:38:10 +0100 |
commit | 027514e6f94607fcd7df8e69e668fe32aeb2873e (patch) | |
tree | b73db22c100713df85f1a790c4a772300b41a7f5 /server/sonar-web/src | |
parent | 492cd3de03d14aaf91b54e96531d77079c0db7f1 (diff) | |
download | sonarqube-027514e6f94607fcd7df8e69e668fe32aeb2873e.tar.gz sonarqube-027514e6f94607fcd7df8e69e668fe32aeb2873e.zip |
SONAR-10182 Users should be able to choose their homepage
Diffstat (limited to 'server/sonar-web/src')
52 files changed, 770 insertions, 550 deletions
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<User> { export function skipOnboarding(): Promise<void | Response> { return post('/api/users/skip_onboarding_tutorial').catch(throwGlobalError); } + +export function setHomePage(homepage: HomePage): Promise<void | Response> { + 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<Props> { 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 = () => ( <ul className="side-tabs-menu"> - {(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<Props, State> { id="context-navigation" height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - <ComponentNavBreadcrumbs - component={this.props.component} - breadcrumbs={this.props.component.breadcrumbs} - /> - <ComponentNavFavorite - component={this.props.component.key} - favorite={this.props.component.isFavorite} - /> + <ComponentNavBreadcrumbs component={this.props.component} /> {this.props.currentBranch && ( <ComponentNavBranch branches={this.props.branches} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js deleted file mode 100644 index 23138af7f82..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js +++ /dev/null @@ -1,107 +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 { Link } from 'react-router'; -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'; - -class ComponentNavBreadcrumbs extends React.PureComponent { - static propTypes = { - breadcrumbs: PropTypes.array, - component: PropTypes.shape({ - visibility: PropTypes.string - }).isRequired - }; - - render() { - const { breadcrumbs, component, organization, shouldOrganizationBeDisplayed } = this.props; - - if (!breadcrumbs) { - return null; - } - - const displayOrganization = organization != null && shouldOrganizationBeDisplayed; - - const lastItem = breadcrumbs[breadcrumbs.length - 1]; - - const items = breadcrumbs.map((item, index) => { - const isPath = item.qualifier === 'DIR'; - const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name); - return ( - <span key={item.key}> - {index === 0 && ( - <span className="navbar-context-title-qualifier spacer-right"> - <QualifierIcon qualifier={lastItem.qualifier} /> - </span> - )} - <Link - className="link-base-color link-no-underline" - title={item.name} - to={getProjectUrl(item.key)}> - {itemName} - </Link> - {index < breadcrumbs.length - 1 && <span className="slash-separator" />} - </span> - ); - }); - - return ( - <h1 className="navbar-context-header"> - <OrganizationHelmet - title={component.name} - organization={displayOrganization ? organization : null} - /> - {displayOrganization && ( - <span> - <OrganizationAvatar organization={organization} /> - <OrganizationLink - organization={organization} - className="link-base-color link-no-underline spacer-left"> - {organization.name} - </OrganizationLink> - <span className="slash-separator" /> - </span> - )} - {items} - {component.visibility === 'private' && ( - <PrivateBadge className="spacer-left" qualifier={component.qualifier} /> - )} - </h1> - ); - } -} - -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( + <QualifierIcon + className="spacer-right" + key={`qualifier-${item.key}`} + qualifier={lastItem.qualifier} + /> + ); + } + + items.push( + <Link + className="link-base-color link-no-underline" + key={`name-${item.key}`} + title={item.name} + to={getProjectUrl(item.key)}> + {itemName} + </Link> + ); + + if (index < breadcrumbs.length - 1) { + items.push(<span className="slash-separator" key={`separator-${item.key}`} />); + } + }); + + return ( + <header className="navbar-context-header"> + <OrganizationHelmet + title={component.name} + organization={organization && shouldOrganizationBeDisplayed ? organization : undefined} + /> + {organization && + shouldOrganizationBeDisplayed && <OrganizationAvatar organization={organization} />} + {organization && + shouldOrganizationBeDisplayed && ( + <OrganizationLink + organization={organization} + className="link-base-color link-no-underline spacer-left"> + {organization.name} + </OrganizationLink> + )} + {organization && shouldOrganizationBeDisplayed && <span className="slash-separator" />} + {items} + {component.visibility === 'private' && ( + <PrivateBadge className="spacer-left" qualifier={component.qualifier} /> + )} + </header> + ); +} + +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 ( - <div className="navbar-context-favorite"> - <Favorite component={this.props.component} favorite={this.props.favorite} /> - </div> - ); - } -} - -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 ( <div className="navbar-context-meta"> - <ul className="list-inline"> - {props.component.analysisDate && ( - <li> - <DateTimeFormatter date={props.component.analysisDate} /> - </li> - )} - {showVersion && ( - <li> - <Tooltip - overlay={`${translate('version')} ${props.component.version}`} - mouseEnterDelay={0.5}> - <span className="text-limited"> - {translate('version')} {props.component.version} - </span> - </Tooltip> - </li> - )} - </ul> - {shortBranch && ( - <div className="navbar-context-meta-branch"> - <BranchStatus branch={props.branch!} /> + {component.analysisDate && ( + <div className="spacer-left"> + <DateTimeFormatter date={component.analysisDate} /> </div> )} + {component.version && + !shortBranch && ( + <Tooltip overlay={`${translate('version')} ${component.version}`} mouseEnterDelay={0.5}> + <div className="spacer-left text-limited"> + {translate('version')} {component.version} + </div> + </Tooltip> + )} + {isLoggedIn(currentUser) && + mainBranch && ( + <div className="navbar-context-meta-secondary"> + <Favorite component={component.key} favorite={Boolean(component.isFavorite)} /> + <HomePageSelect + className="spacer-left" + currentPage={{ type: 'project', key: component.key }} + /> + </div> + )} + {shortBranch && <BranchStatus branch={branch!} />} </div> ); } + +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 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(<Unconnected breadcrumbs={breadcrumbs} component={component} />); + const result = shallow( + <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} /> + ); 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( - <Unconnected - breadcrumbs={breadcrumbs} + <ComponentNavBreadcrumbs component={component} organization={organization} shouldOrganizationBeDisplayed={true} @@ -56,12 +63,15 @@ it('should render organization', () => { 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(<Unconnected breadcrumbs={breadcrumbs} component={component} />); + const result = shallow( + <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} /> + ); 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(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot(); + expect( + shallow( + <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} /> + ) + ).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(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot(); + expect( + shallow( + <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} /> + ) + ).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`] = ` } > <ComponentNavBreadcrumbs - breadcrumbs={ - Array [ - Object { - "key": "component", - "name": "component", - "qualifier": "TRK", - }, - ] - } component={ Object { "breadcrumbs": Array [ @@ -53,9 +44,6 @@ exports[`renders 1`] = ` } } /> - <ComponentNavFavorite - component="component" - /> <ComponentNavMeta component={ Object { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap deleted file mode 100644 index 99cf4b7c00f..00000000000 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap +++ /dev/null @@ -1,108 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not render breadcrumbs with one element 1`] = ` -<h1 - className="navbar-context-header" -> - <OrganizationHelmet - organization={null} - title="My Project" - /> - <span - key="my-project" - > - <span - className="navbar-context-title-qualifier spacer-right" - > - <QualifierIcon - qualifier="TRK" - /> - </span> - <Link - className="link-base-color link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} - title="My Project" - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "my-project", - }, - } - } - > - My Project - </Link> - </span> -</h1> -`; - -exports[`should render organization 1`] = ` -<h1 - className="navbar-context-header" -> - <OrganizationHelmet - organization={ - Object { - "key": "foo", - "name": "The Foo Organization", - } - } - title="My Project" - /> - <span> - <OrganizationAvatar - organization={ - Object { - "key": "foo", - "name": "The Foo Organization", - } - } - /> - <OrganizationLink - className="link-base-color link-no-underline spacer-left" - organization={ - Object { - "key": "foo", - "name": "The Foo Organization", - } - } - > - The Foo Organization - </OrganizationLink> - <span - className="slash-separator" - /> - </span> - <span - key="my-project" - > - <span - className="navbar-context-title-qualifier spacer-right" - > - <QualifierIcon - qualifier="TRK" - /> - </span> - <Link - className="link-base-color link-no-underline" - onlyActiveOnIndex={false} - style={Object {}} - title="My Project" - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "branch": undefined, - "id": "my-project", - }, - } - } - > - My Project - </Link> - </span> -</h1> -`; 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`] = ` +<header + className="navbar-context-header" +> + <OrganizationHelmet + title="My Project" + /> + <QualifierIcon + className="spacer-right" + key="qualifier-my-project" + qualifier="TRK" + /> + <Link + className="link-base-color link-no-underline" + key="name-my-project" + onlyActiveOnIndex={false} + style={Object {}} + title="My Project" + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "my-project", + }, + } + } + > + My Project + </Link> +</header> +`; + +exports[`should render organization 1`] = ` +<header + className="navbar-context-header" +> + <OrganizationHelmet + organization={ + Object { + "key": "foo", + "name": "The Foo Organization", + "projectVisibility": "public", + } + } + title="My Project" + /> + <OrganizationAvatar + organization={ + Object { + "key": "foo", + "name": "The Foo Organization", + "projectVisibility": "public", + } + } + /> + <OrganizationLink + className="link-base-color link-no-underline spacer-left" + organization={ + Object { + "key": "foo", + "name": "The Foo Organization", + "projectVisibility": "public", + } + } + > + The Foo Organization + </OrganizationLink> + <span + className="slash-separator" + /> + <QualifierIcon + className="spacer-right" + key="qualifier-my-project" + qualifier="TRK" + /> + <Link + className="link-base-color link-no-underline" + key="name-my-project" + onlyActiveOnIndex={false} + style={Object {}} + title="My Project" + to={ + Object { + "pathname": "/dashboard", + "query": Object { + "branch": undefined, + "id": "my-project", + }, + } + } + > + My Project + </Link> +</header> +`; 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`] = ` <div className="navbar-context-meta" > - <ul - className="list-inline" + <div + className="spacer-left" + > + <DateTimeFormatter + date="2017-01-02T00:00:00.000Z" + /> + </div> + <Tooltip + mouseEnterDelay={0.5} + overlay="version 0.0.1" + placement="bottom" > - <li> - <DateTimeFormatter - date="2017-01-02T00:00:00.000Z" - /> - </li> - <li> - <Tooltip - mouseEnterDelay={0.5} - overlay="version 0.0.1" - placement="bottom" - > - <span - className="text-limited" - > - version - - 0.0.1 - </span> - </Tooltip> - </li> - </ul> + <div + className="spacer-left text-limited" + > + version + + 0.0.1 + </div> + </Tooltip> </div> `; @@ -35,33 +31,27 @@ exports[`renders status of short-living branch 1`] = ` <div className="navbar-context-meta" > - <ul - className="list-inline" - > - <li> - <DateTimeFormatter - date="2017-01-02T00:00:00.000Z" - /> - </li> - </ul> <div - className="navbar-context-meta-branch" + className="spacer-left" > - <BranchStatus - branch={ - Object { - "isMain": false, - "mergeBranch": "master", - "name": "feature", - "status": Object { - "bugs": 0, - "codeSmells": 2, - "vulnerabilities": 3, - }, - "type": "SHORT", - } - } + <DateTimeFormatter + date="2017-01-02T00:00:00.000Z" /> </div> + <BranchStatus + branch={ + Object { + "isMain": false, + "mergeBranch": "master", + "name": "feature", + "status": Object { + "bugs": 0, + "codeSmells": 2, + "vulnerabilities": 3, + }, + "type": "SHORT", + } + } + /> </div> `; 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<Props, State> { }, 3000); }; + withTutorialTooltip = (element: React.ReactNode) => + this.state.onboardingTutorialTooltip ? ( + <Tooltip defaultVisible={true} overlay={translate('tutorials.follow_later')} trigger="manual"> + {element} + </Tooltip> + ) : ( + element + ); + render() { return ( <NavBar className="navbar-global" id="global-navigation" height={theme.globalNavHeightRaw}> @@ -121,21 +130,13 @@ class GlobalNav extends React.PureComponent<Props, State> { <GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} /> <li> <a className="navbar-help" onClick={this.handleHelpClick} href="#"> - {this.state.onboardingTutorialTooltip ? ( - <Tooltip - defaultVisible={true} - overlay={translate('tutorials.follow_later')} - trigger="manual"> - <HelpIcon /> - </Tooltip> - ) : ( - <HelpIcon /> - )} + {this.props.onSonarCloud ? <HelpIcon /> : this.withTutorialTooltip(<HelpIcon />)} </a> </li> <Search appState={this.props.appState} currentUser={this.props.currentUser} /> {isLoggedIn(this.props.currentUser) && - this.props.onSonarCloud && ( + this.props.onSonarCloud && + this.withTutorialTooltip( <GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} /> )} <GlobalNavUserContainer {...this.props} /> 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<Props> { 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 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 index 34dd8f91396..34dd8f91396 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.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<Props> { id="context-navigation" height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - <h1 className="navbar-context-header">{translate('layout.settings')}</h1> + <header className="navbar-context-header"> + <h1>{translate('layout.settings')}</h1> + </header> <NavBarTabs> {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" > - <h1 + <header className="navbar-context-header" > - layout.settings - </h1> + <h1> + layout.settings + </h1> + </header> <NavBarTabs> <li className="dropdown" diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css index 028707705cf..033eda0c07a 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.css +++ b/server/sonar-web/src/main/js/app/components/search/Search.css @@ -81,7 +81,7 @@ left: 0; } -.navbar-search-item-icons > .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 ( <div id="explore"> <ContextNavBar id="explore-navigation" height={theme.contextNavHeightRaw}> - <div className="navbar-context-header"> - <h1 className="display-inline-block">{translate('explore')}</h1> - </div> + <header className="navbar-context-header"> + <h1>{translate('explore')}</h1> + </header> <NavBarTabs> <li> 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 { </div> ) : ( <PageActions + canSetHome={ + this.props.onSonarCloud && + isLoggedIn(this.props.currentUser) && + this.props.myIssues && + !this.props.organization && + !this.props.component + } loading={this.state.loading} onReload={this.handleReload} paging={paging} diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js index e939e60869f..2aaca8517e8 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js @@ -19,14 +19,16 @@ */ // @flow import React from 'react'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; import IssuesCounter from './IssuesCounter'; import ReloadButton from './ReloadButton'; /*:: import type { Paging } from '../utils'; */ +import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import HomePageSelect from '../../../components/controls/HomePageSelect'; import { translate } from '../../../helpers/l10n'; /*:: type Props = {| + canSetHome: bool, loading: boolean, onReload: () => void, paging: ?Paging, @@ -70,6 +72,10 @@ export default class PageActions extends React.PureComponent { <IssuesCounter className="spacer-left" current={selectedIndex} total={paging.total} /> )} </div> + + {this.props.canSetHome && ( + <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-issues' }} /> + )} </div> ); } 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 ( - <div className="navbar-context-header"> - <h1 className="display-inline-block"> - <OrganizationAvatar organization={organization} /> - {other.length ? ( - <Dropdown> - {({ onToggleClick, open }) => ( - <div className={classNames('organization-switch', 'dropdown', { open })}> - <a className="dropdown-toggle" href="#" onClick={onToggleClick}> - {organization.name} - <DropdownIcon className="little-spacer-left" /> - </a> - <ul className="dropdown-menu"> - {sortBy(other, org => org.name.toLowerCase()).map(organization => ( - <OrganizationListItem key={organization.key} organization={organization} /> - ))} - </ul> - </div> - )} - </Dropdown> - ) : ( - <span className="spacer-left">{organization.name}</span> - )} - </h1> + <header className="navbar-context-header"> + <OrganizationAvatar organization={organization} /> + {other.length ? ( + <Dropdown> + {({ onToggleClick, open }) => ( + <div className={classNames('organization-switch', 'dropdown', { open })}> + <a className="dropdown-toggle" href="#" onClick={onToggleClick}> + {organization.name} + <DropdownIcon className="little-spacer-left" /> + </a> + <ul className="dropdown-menu"> + {sortBy(other, org => org.name.toLowerCase()).map(organization => ( + <OrganizationListItem key={organization.key} organization={organization} /> + ))} + </ul> + </div> + )} + </Dropdown> + ) : ( + <span className="spacer-left">{organization.name}</span> + )} {organization.description != null && ( <div className="navbar-context-description"> <p className="text-limited text-top" title={organization.description}> @@ -65,6 +63,6 @@ export default function OrganizationNavigationHeader({ organization, organizatio </p> </div> )} - </div> + </header> ); } 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 ( <div className="navbar-context-meta"> + {organization.url != null && ( + <a + className="spacer-right text-limited" + href={organization.url} + title={organization.url} + rel="nofollow"> + {organization.url} + </a> + )} <div className="text-muted"> <strong>{translate('organization.key')}:</strong> {organization.key} </div> - {organization.url != null && ( - <div> - <p className="text-limited text-top"> - <a - className="link-underline" - href={organization.url} - title={organization.url} - rel="nofollow"> - {organization.url} - </a> - </p> - </div> - )} + <div className="navbar-context-meta-secondary"> + <HomePageSelect currentPage={{ type: 'organization', key: organization.key }} /> + </div> </div> ); } 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`] = ` -<div +<header className="navbar-context-header" > - <h1 - className="display-inline-block" - > - <OrganizationAvatar - organization={ - Object { - "key": "foo", - "name": "Foo", - "projectVisibility": "public", - } + <OrganizationAvatar + organization={ + Object { + "key": "foo", + "name": "Foo", + "projectVisibility": "public", } - /> - <span - className="spacer-left" - > - Foo - </span> - </h1> -</div> + } + /> + <span + className="spacer-left" + > + Foo + </span> +</header> `; 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 </div> + <div + className="navbar-context-meta-secondary" + > + <Connect(HomePageSelect) + currentPage={ + Object { + "key": "foo", + "type": "organization", + } + } + /> + </div> </div> `; 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<Props, State> { <div className="layout-page-main-inner"> <PageHeader currentUser={this.props.currentUser} + isFavorite={this.props.isFavorite} loading={this.state.loading} onPerspectiveChange={this.handlePerspectiveChange} onQueryChange={this.updateLocationQuery} + onSonarCloud={this.props.onSonarCloud} onSortChange={this.handleSortChange} organization={this.props.organization} projects={this.state.projects} diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx index d8de26017fe..3f347339ad0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx @@ -24,15 +24,18 @@ import Tooltip from '../../../components/controls/Tooltip'; import PerspectiveSelect from './PerspectiveSelect'; import ProjectsSortingSelect from './ProjectsSortingSelect'; import { CurrentUser, isLoggedIn } from '../../../app/types'; +import HomePageSelect from '../../../components/controls/HomePageSelect'; import { translate } from '../../../helpers/l10n'; import { RawQuery } from '../../../helpers/query'; import { Project } from '../types'; interface Props { currentUser: CurrentUser; + isFavorite: boolean; loading: boolean; onPerspectiveChange: (x: { view: string; visualization?: string }) => 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) { </span> )} </div> + + {props.onSonarCloud && + isLoggedIn(currentUser) && + props.isFavorite && + !props.organization && ( + <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-projects' }} /> + )} </header> ); } 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( <PageHeader currentUser={{ isLoggedIn: false }} + isFavorite={false} loading={false} onPerspectiveChange={jest.fn()} onQueryChange={jest.fn()} + onSonarCloud={false} onSortChange={jest.fn()} projects={[]} query={{ search: 'test' }} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index 40a5ea5b647..3dd798e4094 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -31,9 +31,11 @@ exports[`renders 1`] = ` "isLoggedIn": true, } } + isFavorite={false} loading={false} onPerspectiveChange={[Function]} onQueryChange={[Function]} + onSonarCloud={false} onSortChange={[Function]} projects={ Array [ @@ -158,9 +160,11 @@ exports[`renders 2`] = ` "isLoggedIn": true, } } + isFavorite={false} loading={false} onPerspectiveChange={[Function]} onQueryChange={[Function]} + onSonarCloud={false} onSortChange={[Function]} projects={ Array [ diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js index 5a1ea7dcaa6..c3c52bb74cb 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js @@ -165,7 +165,11 @@ export default class Onboarding extends React.PureComponent { {translate('tutorials.skip')} </a> )} - <p className="note">{translate('tutorials.find_it_back_in_help')}</p> + <p className="note"> + {translate( + sonarCloud ? 'tutorials.find_it_back_in_plus' : 'tutorials.find_it_back_in_help' + )} + </p> </div> <div className="page-description"> {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`] = ` <p className="note" > - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus </p> </div> <div @@ -249,7 +249,7 @@ exports[`guides for sonarcloud 2`] = ` <p className="note" > - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus </p> </div> <div @@ -330,7 +330,7 @@ exports[`guides for sonarcloud 3`] = ` <p className="note" > - tutorials.find_it_back_in_help + tutorials.find_it_back_in_plus </p> </div> <div diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx index fc76186117b..f802e0885f7 100644 --- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx +++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx @@ -19,7 +19,9 @@ */ import * as React from 'react'; import * as classNames from 'classnames'; +import Tooltip from './Tooltip'; import FavoriteIcon from '../icons-components/FavoriteIcon'; +import { translate } from '../../helpers/l10n'; interface Props { addFavorite: () => Promise<void>; @@ -80,13 +82,18 @@ export default class FavoriteBase extends React.PureComponent<Props, State> { } render() { + const tooltip = this.state.favorite + ? translate('favorite.current') + : translate('favorite.check'); return ( - <a - className={classNames('link-no-underline', this.props.className)} - href="#" - onClick={this.toggleFavorite}> - <FavoriteIcon favorite={this.state.favorite} /> - </a> + <Tooltip overlay={tooltip}> + <a + className={classNames('display-inline-block', 'link-no-underline', this.props.className)} + href="#" + onClick={this.toggleFavorite}> + <FavoriteIcon favorite={this.state.favorite} /> + </a> + </Tooltip> ); } } 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<Props> { + handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + 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 ( + <Tooltip overlay={tooltip}> + {checked ? ( + <span className={classNames('display-inline-block', this.props.className)}> + <HomeIcon filled={checked} /> + </span> + ) : ( + <a + className={classNames( + 'link-no-underline', + 'display-inline-block', + this.props.className + )} + href="#" + onClick={this.handleClick}> + <HomeIcon filled={checked} /> + </a> + )} + </Tooltip> + ); + } +} + +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`] = ` -<a - className="link-no-underline" - href="#" - onClick={[Function]} +<Tooltip + overlay="favorite.current" + placement="bottom" > - <FavoriteIcon - favorite={true} - /> -</a> + <a + className="display-inline-block link-no-underline" + href="#" + onClick={[Function]} + > + <FavoriteIcon + favorite={true} + /> + </a> +</Tooltip> `; exports[`should render not favorite 1`] = ` -<a - className="link-no-underline" - href="#" - onClick={[Function]} +<Tooltip + overlay="favorite.check" + placement="bottom" > - <FavoriteIcon - favorite={false} - /> -</a> + <a + className="display-inline-block link-no-underline" + href="#" + onClick={[Function]} + > + <FavoriteIcon + favorite={false} + /> + </a> +</Tooltip> `; 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 ( - <span className={classNames('icon-star', { 'icon-star-favorite': favorite }, className)}> - <svg width={size} height={size} viewBox="0 0 16 16"> - <path d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z" /> - </svg> - </span> + <svg + className={classNames('icon-outline', { 'is-filled': favorite }, className)} + style={{ color: fill }} + width={size} + height={size} + viewBox="0 0 16 16" + version="1.1" + xmlnsXlink="http://www.w3.org/1999/xlink" + xmlSpace="preserve"> + <g transform="matrix(0.988024,0,0,0.988024,0.0957953,0.717719)"> + <path d="M15.428,5.777C15.428,5.908 15.35,6.051 15.195,6.205L11.954,9.366L12.722,13.83C12.728,13.872 12.731,13.932 12.731,14.009C12.731,14.134 12.7,14.24 12.637,14.326C12.575,14.412 12.484,14.455 12.365,14.455C12.252,14.455 12.133,14.42 12.008,14.348L7.999,12.241L3.99,14.348C3.859,14.42 3.74,14.455 3.633,14.455C3.508,14.455 3.414,14.412 3.352,14.326C3.289,14.24 3.258,14.134 3.258,14.009C3.258,13.973 3.264,13.914 3.276,13.83L4.044,9.366L0.794,6.205C0.645,6.045 0.57,5.902 0.57,5.777C0.57,5.557 0.737,5.42 1.07,5.366L5.552,4.714L7.561,0.652C7.674,0.408 7.82,0.286 7.999,0.286C8.177,0.286 8.323,0.408 8.436,0.652L10.445,4.714L14.927,5.366C15.261,5.42 15.427,5.557 15.427,5.777L15.428,5.777Z" /> + </g> + </svg> ); } 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 ( + <svg + className={classNames(className, 'icon-outline', { 'is-filled': filled })} + style={{ color: fill }} + width={size} + height={size} + viewBox="0 0 16 16" + version="1.1" + xmlnsXlink="http://www.w3.org/1999/xlink" + xmlSpace="preserve"> + <g transform="matrix(0.870918,0,0,0.870918,0.978227,0.978227)"> + <path d="M15.9,7.8L8.2,0.1C8.1,0 7.9,0 7.8,0.1L0.1,7.8C0,7.9 0,8.1 0.1,8.2C0.2,8.3 0.2,8.3 0.3,8.3L2.2,8.3L2.2,15.8C2.2,15.9 2.2,15.9 2.3,16C2.3,16 2.4,16.1 2.5,16.1L6.2,16.1C6.3,16.1 6.5,16 6.5,15.8L6.5,10.5L9.7,10.5L9.7,15.8C9.7,15.9 9.8,16.1 10,16.1L13.7,16.1C13.8,16.1 14,16 14,15.8L14,8.2L15.9,8.2C16,8.2 16,8.2 16.1,8.1C16,8 16.1,7.9 15.9,7.8Z" /> + </g> + </svg> + ); +} 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 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<any>) => { + return api.getCurrentUser().then(user => dispatch(receiveCurrentUser(user))); +}; + +export const setHomePage = (homepage: HomePage) => (dispatch: Dispatch<any>) => { + 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 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)); |