]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10080 turn Issues to My Issues
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 28 Nov 2017 13:00:11 +0000 (14:00 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 17:00:33 +0000 (18:00 +0100)
12 files changed:
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/App.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/AppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/routes.ts [deleted file]
server/sonar-web/src/main/js/apps/organizations/routes.js
server/sonar-web/src/main/js/helpers/issues.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 730292aead3915a08d750f92f8eb7407d0346c10..516fe67ce48aab9958d0970e5838dc96eacdae9d 100644 (file)
@@ -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;
index 02b3ab3ce115e8e47b5e4744d2e89f06b00994de..27702bf87f77678a4a755352b3e1abeefd72a98f 100644 (file)
@@ -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}>
index 0350b525fd417d10c5ce8811cfd6554c6b750a73..3488208f5084bfd8a9e362b6426dcd23c509a698 100644 (file)
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx b/server/sonar-web/src/main/js/apps/issues/IssuesPageSelector.tsx
new file mode 100644 (file)
index 0000000..4ff236b
--- /dev/null
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.d.ts b/server/sonar-web/src/main/js/apps/issues/components/App.d.ts
new file mode 100644 (file)
index 0000000..d80df8e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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 { Component, CurrentUser } from '../../../app/types';
+import { RawQuery } from '../../../helpers/query';
+
+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 class App extends React.Component<Props> {}
index 94b226cb89dd19f17eaa6ca643eb9a93728351f6..46cb1d239524464d08263a3a7a258ffc645e0f15 100644 (file)
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
deleted file mode 100644 (file)
index d3af967..0000000
+++ /dev/null
@@ -1,72 +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.
- */
-// @flow
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
-/*:: import type { Dispatch } from 'redux'; */
-import { uniq } from 'lodash';
-import App from './App';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getCurrentUser, areThereCustomOrganizations } 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'; */
-
-const mapStateToProps = state => ({
-  currentUser: getCurrentUser(state)
-});
-
-const fetchIssueOrganizations = issues => dispatch => {
-  if (!issues.length) {
-    return Promise.resolve();
-  }
-
-  const organizationKeys = uniq(issues.map(issue => issue.organization));
-  return getOrganizations({ organizations: organizationKeys.join() }).then(
-    response => dispatch(receiveOrganizations(response.organizations)),
-    throwGlobalError
-  );
-};
-
-const fetchIssues = (query /*: RawQuery */, requestOrganizations /*: boolean */ = true) => (
-  dispatch,
-  getState
-) => {
-  const organizationsEnabled = areThereCustomOrganizations(getState());
-  return searchIssues({ ...query, additionalFields: '_all' })
-    .then(response => {
-      const parsedIssues = response.issues.map(issue =>
-        parseIssueFromResponse(issue, response.components, response.users, response.rules)
-      );
-      return { ...response, issues: parsedIssues };
-    })
-    .then(response => {
-      return organizationsEnabled && requestOrganizations
-        ? dispatch(fetchIssueOrganizations(response.issues)).then(() => response)
-        : response;
-    })
-    .catch(throwGlobalError);
-};
-
-const mapDispatchToProps = { fetchIssues };
-
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
new file mode 100644 (file)
index 0000000..7747e8a
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import { uniq } from 'lodash';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
+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 { RawQuery } from '../../../helpers/query';
+import { CurrentUser } from '../../../app/types';
+import { lazyLoad } from '../../../components/lazyLoad';
+
+interface StateProps {
+  currentUser: CurrentUser;
+  onSonarCloud: boolean;
+}
+
+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();
+  }
+
+  const organizationKeys = uniq(issues.map(issue => issue.organization));
+  return getOrganizations({ organizations: organizationKeys.join() }).then(
+    response => dispatch(receiveOrganizations(response.organizations)),
+    throwGlobalError
+  );
+};
+
+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' })
+    .then(response => {
+      const parsedIssues = response.issues.map(issue =>
+        parseIssueFromResponse(issue, response.components, response.users, response.rules)
+      );
+      return { ...response, issues: parsedIssues };
+    })
+    .then(response => {
+      return organizationsEnabled && requestOrganizations
+        ? dispatch(fetchIssueOrganizations(response.issues)).then(() => response)
+        : response;
+    })
+    .catch(throwGlobalError);
+};
+
+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<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
+  lazyLoad(() => import('./App'))
+);
diff --git a/server/sonar-web/src/main/js/apps/issues/routes.ts b/server/sonar-web/src/main/js/apps/issues/routes.ts
deleted file mode 100644 (file)
index e915d90..0000000
+++ /dev/null
@@ -1,33 +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 { RouterState, IndexRouteProps } from 'react-router';
-import { onEnter } from './redirects';
-
-const routes = [
-  {
-    getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
-      import('./components/AppContainer').then(i =>
-        callback(null, { component: i.default, onEnter })
-      );
-    }
-  }
-];
-
-export default routes;
index b0010c5e416d86b2d8a5067d8788fd7487ff268c..d4cbdaa8a9c509871dd02f2e68657628349af12f 100644 (file)
@@ -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',
index 33f8792bab678535962f50f819c77fdd4242c9cd..c3b6b644c085c3489a81196edbe64d33629f568c 100644 (file)
@@ -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>;
index 671127dca10de03d5ca97d4211f3a17193792741..5618a879e48b3671eb0c9d3d5aee618538051dbc 100644 (file)
@@ -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