浏览代码

SONAR-10182 Users should be able to choose their homepage

tags/7.0-RC1
Stas Vilchik 6 年前
父节点
当前提交
027514e6f9
共有 53 个文件被更改,包括 792 次插入551 次删除
  1. 5
    1
      server/sonar-web/src/main/js/api/users.ts
  2. 8
    2
      server/sonar-web/src/main/js/app/components/Landing.tsx
  3. 1
    1
      server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
  4. 0
    19
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
  5. 1
    9
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  6. 0
    107
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
  7. 111
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx
  8. 0
    48
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js
  9. 41
    26
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  10. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  11. 20
    10
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx
  12. 11
    3
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
  13. 0
    12
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
  14. 0
    108
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
  15. 98
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap
  16. 37
    47
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
  17. 12
    11
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
  18. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
  19. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
  20. 1
    1
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
  21. 0
    0
      server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap
  22. 3
    1
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
  23. 5
    3
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
  24. 1
    1
      server/sonar-web/src/main/js/app/components/search/Search.css
  25. 1
    1
      server/sonar-web/src/main/js/app/styles/init/forms.css
  26. 5
    5
      server/sonar-web/src/main/js/app/styles/init/icons.css
  27. 14
    3
      server/sonar-web/src/main/js/app/types.ts
  28. 3
    3
      server/sonar-web/src/main/js/apps/explore/Explore.tsx
  29. 8
    0
      server/sonar-web/src/main/js/apps/issues/components/App.js
  30. 7
    1
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  31. 2
    2
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
  32. 22
    24
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
  33. 13
    13
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
  34. 15
    19
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
  35. 12
    0
      server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
  36. 2
    0
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  37. 10
    0
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
  38. 2
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
  39. 4
    0
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
  40. 5
    1
      server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
  41. 1
    1
      server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
  42. 3
    1
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
  43. 3
    3
      server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
  44. 13
    6
      server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
  45. 90
    0
      server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
  46. 26
    16
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
  47. 22
    9
      server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx
  48. 50
    0
      server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx
  49. 13
    12
      server/sonar-web/src/main/js/components/nav/ContextNavBar.css
  50. 21
    1
      server/sonar-web/src/main/js/helpers/urls.ts
  51. 18
    5
      server/sonar-web/src/main/js/store/users/actions.ts
  52. 26
    10
      server/sonar-web/src/main/js/store/users/reducer.ts
  53. 22
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 5
- 1
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);
}

+ 8
- 2
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 {

+ 1
- 1
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)}

+ 0
- 19
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;

+ 1
- 9
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}

+ 0
- 107
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js 查看文件

@@ -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;

+ 111
- 0
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);

+ 0
- 48
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js 查看文件

@@ -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);

+ 41
- 26
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);

+ 2
- 2
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;
}
}));

server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js → 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);
});

+ 11
- 3
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();
});

+ 0
- 12
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 {

+ 0
- 108
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap 查看文件

@@ -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>
`;

+ 98
- 0
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>
`;

+ 37
- 47
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>
`;

+ 12
- 11
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} />

+ 1
- 1
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> {

+ 1
- 1
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[];
}

server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js → 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';


server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap → server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap 查看文件


+ 3
- 1
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()}

+ 5
- 3
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"

+ 1
- 1
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;

+ 1
- 1
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 {

+ 5
- 5
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;
}


+ 14
- 3
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[];
}

+ 3
- 3
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>

+ 8
- 0
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}

+ 7
- 1
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>
);
}

+ 2
- 2
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;

+ 22
- 24
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>
);
}

+ 13
- 13
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>
);
}

+ 15
- 19
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`] = `

+ 12
- 0
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>
`;

+ 2
- 0
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}

+ 10
- 0
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>
);
}

+ 2
- 0
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' }}

+ 4
- 0
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 [

+ 5
- 1
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(

+ 1
- 1
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 =

+ 3
- 1
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' }]
})
)
}));


+ 3
- 3
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

+ 13
- 6
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>
);
}
}

+ 90
- 0
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);

+ 26
- 16
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>
`;

+ 22
- 9
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>
);
}

+ 50
- 0
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>
);
}

+ 13
- 12
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);

+ 21
- 1
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';
}

server/sonar-web/src/main/js/store/users/actions.js → 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 });
},
() => {}
);
};

server/sonar-web/src/main/js/store/users/reducer.js → 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));

+ 22
- 1
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.

正在加载...
取消
保存