@@ -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); | |||
} |
@@ -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 { |
@@ -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)} |
@@ -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; |
@@ -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} |
@@ -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; |
@@ -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); |
@@ -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); |
@@ -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); |
@@ -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; | |||
} | |||
})); |
@@ -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); | |||
}); |
@@ -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(); | |||
}); |
@@ -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 { |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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} /> |
@@ -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> { |
@@ -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[]; | |||
} |
@@ -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'; | |||
@@ -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,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" |
@@ -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; |
@@ -304,7 +304,7 @@ input[type='submit'].button-grey.button-active { | |||
} | |||
.button-small > svg { | |||
margin-top: 2px; | |||
padding-top: 2px; | |||
} | |||
.button-group { |
@@ -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; | |||
} | |||
@@ -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[]; | |||
} |
@@ -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> |
@@ -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} |
@@ -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> | |||
); | |||
} |
@@ -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; |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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`] = ` |
@@ -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> | |||
`; |
@@ -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} |
@@ -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> | |||
); | |||
} |
@@ -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' }} |
@@ -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 [ |
@@ -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( |
@@ -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 = |
@@ -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' }] | |||
}) | |||
) | |||
})); | |||
@@ -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 |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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); |
@@ -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> | |||
`; |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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,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'; | |||
} |
@@ -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 }); | |||
}, | |||
() => {} | |||
); | |||
}; |
@@ -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)); |
@@ -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. |