@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON, post, postJSON, RequestData } from '../helpers/request'; | |||
import { RawIssue } from '../helpers/issues'; | |||
export interface IssueResponse { | |||
components?: Array<{}>; | |||
@@ -30,7 +31,7 @@ interface IssuesResponse { | |||
components?: Array<{}>; | |||
debtTotal?: number; | |||
facets: Array<{}>; | |||
issues: Array<{}>; | |||
issues: RawIssue[]; | |||
paging: { | |||
pageIndex: number; | |||
pageSize: number; |
@@ -64,11 +64,24 @@ export default class GlobalNavMenu extends React.PureComponent { | |||
} | |||
renderIssuesLink() { | |||
const active = this.props.location.pathname === 'issues'; | |||
if (this.props.sonarCloud) { | |||
return ( | |||
<li> | |||
<Link | |||
to={{ pathname: '/issues', query: { resolved: 'false' } }} | |||
className={active ? 'active' : undefined}> | |||
{translate('my_issues')} | |||
</Link> | |||
</li> | |||
); | |||
} | |||
const query = | |||
this.props.currentUser.isLoggedIn && isMySet() | |||
? { resolved: 'false', myIssues: 'true' } | |||
: { resolved: 'false' }; | |||
const active = this.props.location.pathname === 'issues'; | |||
return ( | |||
<li> | |||
<Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}> |
@@ -47,7 +47,8 @@ import componentRoutes from '../../apps/component/routes'; | |||
import componentMeasuresRoutes from '../../apps/component-measures/routes'; | |||
import customMeasuresRoutes from '../../apps/custom-measures/routes'; | |||
import groupsRoutes from '../../apps/groups/routes'; | |||
import issuesRoutes from '../../apps/issues/routes'; | |||
import Issues from '../../apps/issues/components/AppContainer'; | |||
import IssuesPageSelector from '../../apps/issues/IssuesPageSelector'; | |||
import marketplaceRoutes from '../../apps/marketplace/routes'; | |||
import metricsRoutes from '../../apps/metrics/routes'; | |||
import overviewRoutes from '../../apps/overview/routes'; | |||
@@ -167,7 +168,7 @@ const startReactApp = () => { | |||
path="extension/:pluginKey/:extensionKey" | |||
component={GlobalPageExtension} | |||
/> | |||
<Route path="issues" childRoutes={issuesRoutes} /> | |||
<Route path="issues" component={IssuesPageSelector} /> | |||
<Route path="organizations" childRoutes={organizationsRoutes} /> | |||
<Route path="projects" childRoutes={projectsRoutes} /> | |||
<Route path="quality_gates" childRoutes={qualityGatesRoutes} /> | |||
@@ -187,7 +188,7 @@ const startReactApp = () => { | |||
path="project/extension/:pluginKey/:extensionKey" | |||
component={ProjectPageExtension} | |||
/> | |||
<Route path="project/issues" childRoutes={issuesRoutes} /> | |||
<Route path="project/issues" component={Issues} /> | |||
<Route path="project/quality_gate" childRoutes={projectQualityGateRoutes} /> | |||
<Route | |||
path="project/quality_profiles" |
@@ -0,0 +1,44 @@ | |||
/* | |||
* 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 { connect } from 'react-redux'; | |||
import AppContainer from './components/AppContainer'; | |||
import { CurrentUser, isLoggedIn } from '../../app/types'; | |||
import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer'; | |||
interface StateProps { | |||
currentUser: CurrentUser; | |||
onSonarCloud: boolean; | |||
} | |||
function IssuesPage({ currentUser, onSonarCloud, ...props }: StateProps) { | |||
const myIssues = (isLoggedIn(currentUser) && onSonarCloud) || undefined; | |||
return <AppContainer myIssues={myIssues} {...props} />; | |||
} | |||
const stateToProps = (state: any) => { | |||
const onSonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); | |||
return { | |||
currentUser: getCurrentUser(state), | |||
onSonarCloud: Boolean(onSonarCloudSetting && onSonarCloudSetting.value === 'true') | |||
}; | |||
}; | |||
export default connect<StateProps>(stateToProps)(IssuesPage); |
@@ -1,7 +1,7 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* 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 | |||
@@ -17,17 +17,20 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { RouterState, IndexRouteProps } from 'react-router'; | |||
import { onEnter } from './redirects'; | |||
import * as React from 'react'; | |||
import { Component, CurrentUser } from '../../../app/types'; | |||
import { RawQuery } from '../../../helpers/query'; | |||
const routes = [ | |||
{ | |||
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { | |||
import('./components/AppContainer').then(i => | |||
callback(null, { component: i.default, onEnter }) | |||
); | |||
} | |||
} | |||
]; | |||
interface Props { | |||
branch?: { name: string }; | |||
component?: Component; | |||
currentUser: CurrentUser; | |||
fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<any>; | |||
location: { pathname: string; query: RawQuery }; | |||
myIssues?: boolean; | |||
onBranchesChange: () => void; | |||
onSonarCloud: boolean; | |||
organization?: { key: string }; | |||
} | |||
export default routes; | |||
export default class App extends React.Component<Props> {} |
@@ -22,6 +22,7 @@ import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import key from 'keymaster'; | |||
import { keyBy, without } from 'lodash'; | |||
import PropTypes from 'prop-types'; | |||
import PageActions from './PageActions'; | |||
import FiltersHeader from './FiltersHeader'; | |||
import MyIssuesFilter from './MyIssuesFilter'; | |||
@@ -71,12 +72,10 @@ export type Props = { | |||
currentUser: CurrentUser, | |||
fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<*>, | |||
location: { pathname: string, query: RawQuery }, | |||
myIssues?: bool; | |||
onBranchesChange: () => void, | |||
onSonarCloud: bool, | |||
organization?: { key: string }, | |||
router: { | |||
push: ({ pathname: string, query?: RawQuery }) => void, | |||
replace: ({ pathname: string, query?: RawQuery }) => void | |||
} | |||
}; | |||
*/ | |||
@@ -114,6 +113,10 @@ export default class App extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
static contextTypes = { | |||
router: PropTypes.object.isRequired | |||
}; | |||
constructor(props /*: Props */) { | |||
super(props); | |||
this.state = { | |||
@@ -123,7 +126,7 @@ export default class App extends React.PureComponent { | |||
issues: [], | |||
loading: true, | |||
locationsNavigator: false, | |||
myIssues: areMyIssuesSelected(props.location.query), | |||
myIssues: props.myIssues || areMyIssuesSelected(props.location.query), | |||
openFacets: { severities: true, types: true }, | |||
openIssue: null, | |||
openPopup: null, | |||
@@ -172,7 +175,7 @@ export default class App extends React.PureComponent { | |||
} | |||
this.setState({ | |||
myIssues: areMyIssuesSelected(nextProps.location.query), | |||
myIssues: nextProps.myIssues || areMyIssuesSelected(nextProps.location.query), | |||
openIssue, | |||
query: parseQuery(nextProps.location.query) | |||
}); | |||
@@ -329,15 +332,15 @@ export default class App extends React.PureComponent { | |||
} | |||
}; | |||
if (this.state.openIssue) { | |||
this.props.router.replace(path); | |||
this.context.router.replace(path); | |||
} else { | |||
this.props.router.push(path); | |||
this.context.router.push(path); | |||
} | |||
}; | |||
closeIssue = () => { | |||
if (this.state.query) { | |||
this.props.router.push({ | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery(this.state.query), | |||
@@ -575,7 +578,7 @@ export default class App extends React.PureComponent { | |||
}; | |||
handleFilterChange = (changes /*: {} */) => { | |||
this.props.router.push({ | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, ...changes }), | |||
@@ -591,7 +594,7 @@ export default class App extends React.PureComponent { | |||
if (!this.props.component) { | |||
saveMyIssues(myIssues); | |||
} | |||
this.props.router.push({ | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }), | |||
@@ -618,7 +621,7 @@ export default class App extends React.PureComponent { | |||
}; | |||
handleReset = () => { | |||
this.props.router.push({ | |||
this.context.router.push({ | |||
pathname: this.props.location.pathname, | |||
query: { | |||
...DEFAULT_QUERY, | |||
@@ -754,17 +757,18 @@ export default class App extends React.PureComponent { | |||
} | |||
renderFacets() { | |||
const { component, currentUser } = this.props; | |||
const { component, currentUser, onSonarCloud } = this.props; | |||
const { query } = this.state; | |||
return ( | |||
<div className="layout-page-filters"> | |||
{currentUser.isLoggedIn && ( | |||
<MyIssuesFilter | |||
myIssues={this.state.myIssues} | |||
onMyIssuesChange={this.handleMyIssuesChange} | |||
/> | |||
)} | |||
{currentUser.isLoggedIn && | |||
!onSonarCloud && ( | |||
<MyIssuesFilter | |||
myIssues={this.state.myIssues} | |||
onMyIssuesChange={this.handleMyIssuesChange} | |||
/> | |||
)} | |||
<FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} /> | |||
<Sidebar | |||
component={component} |
@@ -17,25 +17,37 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { connect } from 'react-redux'; | |||
import { withRouter } from 'react-router'; | |||
/*:: import type { Dispatch } from 'redux'; */ | |||
import { Dispatch } from 'redux'; | |||
import { uniq } from 'lodash'; | |||
import App from './App'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer'; | |||
import { | |||
getCurrentUser, | |||
areThereCustomOrganizations, | |||
getGlobalSettingValue | |||
} from '../../../store/rootReducer'; | |||
import { getOrganizations } from '../../../api/organizations'; | |||
import { receiveOrganizations } from '../../../store/organizations/duck'; | |||
import { searchIssues } from '../../../api/issues'; | |||
import { parseIssueFromResponse } from '../../../helpers/issues'; | |||
/*:: import type { RawQuery } from '../../../helpers/query'; */ | |||
import { RawQuery } from '../../../helpers/query'; | |||
import { CurrentUser } from '../../../app/types'; | |||
import { lazyLoad } from '../../../components/lazyLoad'; | |||
const mapStateToProps = state => ({ | |||
currentUser: getCurrentUser(state) | |||
}); | |||
interface StateProps { | |||
currentUser: CurrentUser; | |||
onSonarCloud: boolean; | |||
} | |||
const fetchIssueOrganizations = issues => dispatch => { | |||
const mapStateToProps = (state: any): StateProps => { | |||
const onSonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled'); | |||
return { | |||
currentUser: getCurrentUser(state), | |||
onSonarCloud: Boolean(onSonarCloudSetting && onSonarCloudSetting.value === 'true') | |||
}; | |||
}; | |||
const fetchIssueOrganizations = (issues: any[]) => (dispatch: Dispatch<any>) => { | |||
if (!issues.length) { | |||
return Promise.resolve(); | |||
} | |||
@@ -47,9 +59,10 @@ const fetchIssueOrganizations = issues => dispatch => { | |||
); | |||
}; | |||
const fetchIssues = (query /*: RawQuery */, requestOrganizations /*: boolean */ = true) => ( | |||
dispatch, | |||
getState | |||
const fetchIssues = (query: RawQuery, requestOrganizations = true) => ( | |||
// use `Function` to be able to do `dispatch(...).then(...)` | |||
dispatch: Function, | |||
getState: () => any | |||
) => { | |||
const organizationsEnabled = areThereCustomOrganizations(getState()); | |||
return searchIssues({ ...query, additionalFields: '_all' }) | |||
@@ -67,6 +80,17 @@ const fetchIssues = (query /*: RawQuery */, requestOrganizations /*: boolean */ | |||
.catch(throwGlobalError); | |||
}; | |||
const mapDispatchToProps = { fetchIssues }; | |||
interface DispatchProps { | |||
fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<void>; | |||
} | |||
// have to type cast this, because of async action | |||
const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps; | |||
interface OwnProps { | |||
myIssues?: boolean; | |||
} | |||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App)); | |||
export default connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)( | |||
lazyLoad(() => import('./App')) | |||
); |
@@ -32,7 +32,7 @@ import OrganizationProjectsManagement from './components/OrganizationProjectsMan | |||
import OrganizationDelete from './components/OrganizationDelete'; | |||
import qualityGatesRoutes from '../quality-gates/routes'; | |||
import qualityProfilesRoutes from '../quality-profiles/routes'; | |||
import issuesRoutes from '../issues/routes'; | |||
import Issues from '../issues/components/AppContainer'; | |||
const routes = [ | |||
{ | |||
@@ -55,7 +55,7 @@ const routes = [ | |||
{ | |||
path: 'issues', | |||
component: OrganizationContainer, | |||
childRoutes: issuesRoutes | |||
childRoutes: [{ indexRoute: { component: Issues } }] | |||
}, | |||
{ | |||
path: 'members', |
@@ -50,7 +50,7 @@ interface IssueBase { | |||
[x: string]: any; | |||
} | |||
interface RawIssue extends IssueBase { | |||
export interface RawIssue extends IssueBase { | |||
assignee?: string; | |||
author?: string; | |||
comments?: Array<Comment>; |
@@ -95,6 +95,7 @@ minor=Minor | |||
more=More | |||
more_x={0} more | |||
more_actions=More Actions | |||
my_issues=My Issues | |||
my_favorite=My Favorite | |||
my_favorites=My Favorites | |||
my_projects=My Projects |