]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10080 turn Projects to My Projects
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 23 Nov 2017 15:05:14 +0000 (16:05 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 17:00:33 +0000 (18:00 +0100)
42 files changed:
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.tsx
server/sonar-web/src/main/js/apps/organizations/routes.js
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/App.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelector.tsx
server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilter.tsx
server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.tsx
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguages.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganization.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverallMeasures.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/DefaultPageSelector-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/FavoriteFilter-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLanguages-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardLeak-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverall-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCardOverallMeasures-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.tsx
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.tsx
server/sonar-web/src/main/js/apps/projects/routes.ts
server/sonar-web/src/main/js/components/lazyLoad.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/store/languages/reducer.js [deleted file]
server/sonar-web/src/main/js/store/languages/reducer.ts [new file with mode: 0644]
server/sonar-web/src/main/js/store/withCurrentUser.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2a1d784aa1c2abeac45e442b7b1dd2a574b98b33..02b3ab3ce115e8e47b5e4744d2e89f06b00994de 100644 (file)
@@ -30,7 +30,8 @@ export default class GlobalNavMenu extends React.PureComponent {
     currentUser: PropTypes.object.isRequired,
     location: PropTypes.shape({
       pathname: PropTypes.string.isRequired
-    }).isRequired
+    }).isRequired,
+    sonarCloud: PropTypes.bool
   };
 
   static defaultProps = {
@@ -46,7 +47,7 @@ export default class GlobalNavMenu extends React.PureComponent {
     return (
       <li>
         <Link to="/projects" activeClassName="active">
-          {translate('projects.page')}
+          {this.props.sonarCloud ? translate('my_projects') : translate('projects.page')}
         </Link>
       </li>
     );
index 73239aeb4305ceac53d211877653d8c47b915982..f18764601e4e38b877c7a236f1933866e468d7cb 100644 (file)
@@ -133,3 +133,19 @@ export enum Visibility {
   Public = 'public',
   Private = 'private'
 }
+
+export interface CurrentUser {
+  isLoggedIn: boolean;
+  showOnboardingTutorial?: boolean;
+}
+
+export interface LoggedInUser extends CurrentUser {
+  avatar?: string;
+  email?: string;
+  isLoggedIn: true;
+  name: string;
+}
+
+export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
+  return user.isLoggedIn;
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.tsx
deleted file mode 100644 (file)
index d241362..0000000
+++ /dev/null
@@ -1,41 +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 * as React from 'react';
-import App from '../../projects/components/App';
-import AllProjects from '../../projects/components/AllProjects';
-
-interface Props {
-  location: { pathname: string; query: { [x: string]: string } };
-  organization: { key: string };
-}
-
-export default function OrganizationFavoriteProjects(props: Props) {
-  return (
-    <div id="projects-page">
-      <App>
-        <AllProjects
-          isFavorite={true}
-          location={props.location}
-          organization={props.organization}
-        />
-      </App>
-    </div>
-  );
-}
index 07f220f0d964e27f35f413067a3b865dabe01ae6..65169afa93e1e1058c2ce308f45c534e78a61245 100644 (file)
@@ -18,8 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import App from '../../projects/components/App';
-import AllProjects from '../../projects/components/AllProjects';
+import AllProjectsContainer from '../../projects/components/AllProjectsContainer';
 
 interface Props {
   location: { pathname: string; query: { [x: string]: string } };
@@ -28,14 +27,10 @@ interface Props {
 
 export default function OrganizationProjects(props: Props) {
   return (
-    <div id="projects-page">
-      <App>
-        <AllProjects
-          isFavorite={false}
-          location={props.location}
-          organization={props.organization}
-        />
-      </App>
-    </div>
+    <AllProjectsContainer
+      isFavorite={false}
+      location={props.location}
+      organization={props.organization}
+    />
   );
 }
index 072bb955202032831ca6f018afbb637e35448555..b0010c5e416d86b2d8a5067d8788fd7487ff268c 100644 (file)
@@ -21,7 +21,6 @@ import OrganizationPageContainer from './components/OrganizationPage';
 import OrganizationPageExtension from '../../app/components/extensions/OrganizationPageExtension';
 import OrganizationContainer from './components/OrganizationContainer';
 import OrganizationProjects from './components/OrganizationProjects';
-import OrganizationFavoriteProjects from './components/OrganizationFavoriteProjects';
 import OrganizationRules from './components/OrganizationRules';
 import OrganizationAdminContainer from './components/OrganizationAdmin';
 import OrganizationEdit from './components/OrganizationEdit';
@@ -51,17 +50,7 @@ const routes = [
       {
         path: 'projects',
         component: OrganizationContainer,
-        childRoutes: [
-          {
-            indexRoute: {
-              component: OrganizationProjects
-            }
-          },
-          {
-            path: 'favorite',
-            component: OrganizationFavoriteProjects
-          }
-        ]
+        childRoutes: [{ indexRoute: { component: OrganizationProjects } }]
       },
       {
         path: 'issues',
index cd5f60bd1eb1cd3592974e40f97759e7db57a46b..fb4e108e3dbca7627b098019b1ecb441e72326b7 100644 (file)
@@ -24,6 +24,7 @@ import PageHeader from './PageHeader';
 import ProjectsList from './ProjectsList';
 import PageSidebar from './PageSidebar';
 import Visualizations from '../visualizations/Visualizations';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
 import ListFooter from '../../../components/controls/ListFooter';
 import { translate } from '../../../helpers/l10n';
@@ -34,10 +35,13 @@ import { Project, Facets } from '../types';
 import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
 import { parseUrlQuery, Query } from '../query';
 
-interface Props {
+export interface Props {
+  currentUser: CurrentUser;
   isFavorite: boolean;
   location: { pathname: string; query: { [x: string]: string } };
+  onSonarCloud: boolean;
   organization?: { key: string };
+  organizationsEnabled: boolean;
 }
 
 interface State {
@@ -53,8 +57,6 @@ export default class AllProjects extends React.PureComponent<Props, State> {
   mounted: boolean;
 
   static contextTypes = {
-    currentUser: PropTypes.object.isRequired,
-    organizationsEnabled: PropTypes.bool,
     router: PropTypes.object.isRequired
   };
 
@@ -65,7 +67,13 @@ export default class AllProjects extends React.PureComponent<Props, State> {
 
   componentDidMount() {
     this.mounted = true;
-    if (this.props.isFavorite && !this.context.currentUser.isLoggedIn) {
+
+    const html = document.querySelector('html');
+    if (html) {
+      html.classList.add('dashboard-page');
+    }
+
+    if (this.props.isFavorite && !isLoggedIn(this.props.currentUser)) {
       handleRequiredAuthentication();
       return;
     }
@@ -84,6 +92,12 @@ export default class AllProjects extends React.PureComponent<Props, State> {
 
   componentWillUnmount() {
     this.mounted = false;
+
+    const html = document.querySelector('html');
+    if (html) {
+      html.classList.remove('dashboard-page');
+    }
+
     const footer = document.getElementById('footer');
     if (footer) {
       footer.classList.remove('page-footer-with-sidebar');
@@ -231,6 +245,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
               isFavorite={this.props.isFavorite}
               organization={this.props.organization}
               query={this.state.query}
+              showFavoriteFilter={!this.props.onSonarCloud}
               view={this.getView()}
               visualization={this.getVisualization()}
             />
@@ -245,7 +260,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
       <div className="layout-page-header-panel-inner layout-page-main-header-inner">
         <div className="layout-page-main-inner">
           <PageHeader
-            currentUser={this.context.currentUser}
+            currentUser={this.props.currentUser}
             isFavorite={this.props.isFavorite}
             loading={this.state.loading}
             onPerspectiveChange={this.handlePerspectiveChange}
@@ -268,7 +283,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
       <div className="layout-page-main-inner">
         {this.state.projects && (
           <Visualizations
-            displayOrganizations={!this.props.organization && !!this.context.organizationsEnabled}
+            displayOrganizations={!this.props.organization && this.props.organizationsEnabled}
             projects={this.state.projects}
             sort={this.state.query.sort}
             total={this.state.total}
@@ -299,7 +314,7 @@ export default class AllProjects extends React.PureComponent<Props, State> {
 
   render() {
     return (
-      <div className="layout-page projects-page">
+      <div className="layout-page projects-page" id="projects-page">
         <Helmet title={translate('projects.page')} />
 
         {this.renderSide()}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.tsx
new file mode 100644 (file)
index 0000000..5eb3745
--- /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 { connect } from 'react-redux';
+import { CurrentUser } from '../../../app/types';
+import { lazyLoad } from '../../../components/lazyLoad';
+import {
+  getCurrentUser,
+  areThereCustomOrganizations,
+  getGlobalSettingValue
+} from '../../../store/rootReducer';
+
+interface StateProps {
+  currentUser: CurrentUser;
+  onSonarCloud: boolean;
+  organizationsEnabled: boolean;
+}
+
+const stateToProps = (state: any) => {
+  const onSonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
+  return {
+    currentUser: getCurrentUser(state),
+    onSonarCloud: Boolean(onSonarCloudSetting && onSonarCloudSetting.value === 'true'),
+    organizationsEnabled: areThereCustomOrganizations(state)
+  };
+};
+
+export default connect<StateProps, any, any>(stateToProps)(lazyLoad(() => import('./AllProjects')));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.tsx b/server/sonar-web/src/main/js/apps/projects/components/App.tsx
deleted file mode 100644 (file)
index 0e605b9..0000000
+++ /dev/null
@@ -1,75 +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 * as React from 'react';
-import { connect } from 'react-redux';
-import * as PropTypes from 'prop-types';
-import {
-  getCurrentUser,
-  getLanguages,
-  areThereCustomOrganizations
-} from '../../../store/rootReducer';
-
-interface Props {
-  currentUser: { isLoggedIn: boolean };
-  languages: { [key: string]: { key: string; name: string } };
-  organizationsEnabled: boolean;
-}
-
-class App extends React.PureComponent<Props> {
-  static childContextTypes = {
-    currentUser: PropTypes.object.isRequired,
-    languages: PropTypes.object.isRequired,
-    organizationsEnabled: PropTypes.bool
-  };
-
-  getChildContext() {
-    return {
-      currentUser: this.props.currentUser,
-      languages: this.props.languages,
-      organizationsEnabled: this.props.organizationsEnabled
-    };
-  }
-
-  componentDidMount() {
-    const elem = document.querySelector('html');
-    if (elem) {
-      elem.classList.add('dashboard-page');
-    }
-  }
-
-  componentWillUnmount() {
-    const elem = document.querySelector('html');
-    if (elem) {
-      elem.classList.remove('dashboard-page');
-    }
-  }
-
-  render() {
-    return <div id="projects-page">{this.props.children}</div>;
-  }
-}
-
-const mapStateToProps = (state: any) => ({
-  currentUser: getCurrentUser(state),
-  languages: getLanguages(state),
-  organizationsEnabled: areThereCustomOrganizations(state)
-});
-
-export default connect<any, any, any>(mapStateToProps)(App);
index c5d4197f1bed770313b392b5b5e07392dcaded6e..56d148f82b2f51cd55721a68e0400b0a389b9ef1 100644 (file)
  */
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
-import AllProjects from './AllProjects';
+import AllProjectsContainer from './AllProjectsContainer';
 import { isFavoriteSet, isAllSet } from '../../../helpers/storage';
 import { searchProjects } from '../../../api/components';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
 
 interface Props {
+  currentUser: CurrentUser;
   location: { pathname: string; query: { [x: string]: string } };
+  onSonarCloud: boolean;
 }
 
 interface State {
@@ -34,7 +37,6 @@ interface State {
 
 export default class DefaultPageSelector extends React.PureComponent<Props, State> {
   static contextTypes = {
-    currentUser: PropTypes.object.isRequired,
     router: PropTypes.object.isRequired
   };
 
@@ -44,22 +46,26 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
   }
 
   componentDidMount() {
-    this.defineIfShouldBeRedirected();
+    if (!this.props.onSonarCloud) {
+      this.defineIfShouldBeRedirected();
+    }
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (prevProps.location !== this.props.location) {
-      this.defineIfShouldBeRedirected();
-    } else if (this.state.shouldBeRedirected === true) {
-      this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' });
-    } else if (this.state.shouldForceSorting != null) {
-      this.context.router.replace({
-        ...this.props.location,
-        query: {
-          ...this.props.location.query,
-          sort: this.state.shouldForceSorting
-        }
-      });
+    if (!this.props.onSonarCloud) {
+      if (prevProps.location !== this.props.location) {
+        this.defineIfShouldBeRedirected();
+      } else if (this.state.shouldBeRedirected === true) {
+        this.context.router.replace({ ...this.props.location, pathname: '/projects/favorite' });
+      } else if (this.state.shouldForceSorting != null) {
+        this.context.router.replace({
+          ...this.props.location,
+          query: {
+            ...this.props.location.query,
+            sort: this.state.shouldForceSorting
+          }
+        });
+      }
     }
   }
 
@@ -67,7 +73,7 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
     if (Object.keys(this.props.location.query).length > 0) {
       // show ALL projects when there are some filters
       this.setState({ shouldBeRedirected: false, shouldForceSorting: undefined });
-    } else if (!this.context.currentUser.isLoggedIn) {
+    } else if (!isLoggedIn(this.props.currentUser)) {
       // show ALL projects if user is anonymous
       if (!this.props.location.query || !this.props.location.query.sort) {
         // force default sorting to last analysis date
@@ -92,11 +98,15 @@ export default class DefaultPageSelector extends React.PureComponent<Props, Stat
   }
 
   render() {
+    if (this.props.onSonarCloud) {
+      return <AllProjectsContainer isFavorite={true} location={this.props.location} />;
+    }
+
     const { shouldBeRedirected, shouldForceSorting } = this.state;
     if (shouldBeRedirected == null || shouldBeRedirected === true || shouldForceSorting != null) {
       return null;
     } else {
-      return <AllProjects isFavorite={false} location={this.props.location} />;
+      return <AllProjectsContainer isFavorite={false} location={this.props.location} />;
     }
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/DefaultPageSelectorContainer.tsx
new file mode 100644 (file)
index 0000000..d2c993f
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 { connect } from 'react-redux';
+import DefaultPageSelector from './DefaultPageSelector';
+import { CurrentUser } from '../../../app/types';
+import { getCurrentUser, getGlobalSettingValue } from '../../../store/rootReducer';
+
+interface StateProps {
+  currentUser: CurrentUser;
+  onSonarCloud: boolean;
+}
+
+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)(DefaultPageSelector);
index 25c9d92209b6226e308220f165e26c2bd58ddea5..83ccd1a9176af14c99614f8974592def7ebb9e68 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import { IndexLink, Link } from 'react-router';
 import { translate } from '../../../helpers/l10n';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
 import { saveAll, saveFavorite } from '../../../helpers/storage';
 import { RawQuery } from '../../../helpers/query';
 
 interface Props {
+  currentUser: CurrentUser;
   organization?: { key: string };
   query?: RawQuery;
 }
 
 export default class FavoriteFilter extends React.PureComponent<Props> {
-  static contextTypes = {
-    currentUser: PropTypes.object.isRequired
-  };
-
   handleSaveFavorite = () => {
     if (!this.props.organization) {
       saveFavorite();
@@ -47,7 +44,7 @@ export default class FavoriteFilter extends React.PureComponent<Props> {
   };
 
   render() {
-    if (!this.context.currentUser.isLoggedIn) {
+    if (!isLoggedIn(this.props.currentUser)) {
       return null;
     }
 
diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/FavoriteFilterContainer.tsx
new file mode 100644 (file)
index 0000000..2600721
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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 FavoriteFilter from './FavoriteFilter';
+import { withCurrentUser } from '../../../store/withCurrentUser';
+
+export default withCurrentUser(FavoriteFilter);
index 1bfa07b70f21698f85814ce3052247881fd25d5a..bc104756b11e7bf5e0d9f4206154f968883b600e 100644 (file)
@@ -18,8 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import AllProjects from './AllProjects';
+import AllProjectsContainer from './AllProjectsContainer';
 
 export default function FavoriteProjectsContainer(props: any) {
-  return <AllProjects isFavorite={true} {...props} />;
+  return <AllProjectsContainer isFavorite={true} {...props} />;
 }
index c8de3ad390611adcfb977cca6a5f70d666173c45..8b401d2a6272fe21781077cf21ea2d4ddb21040b 100644 (file)
@@ -23,12 +23,13 @@ import SearchFilterContainer from '../filters/SearchFilterContainer';
 import Tooltip from '../../../components/controls/Tooltip';
 import PerspectiveSelect from './PerspectiveSelect';
 import ProjectsSortingSelect from './ProjectsSortingSelect';
+import { CurrentUser, isLoggedIn } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 import { RawQuery } from '../../../helpers/query';
 import { Project } from '../types';
 
 interface Props {
-  currentUser?: { isLoggedIn: boolean };
+  currentUser: CurrentUser;
   isFavorite?: boolean;
   loading: boolean;
   onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
@@ -45,7 +46,7 @@ interface Props {
 export default function PageHeader(props: Props) {
   const { loading, total, projects, currentUser, view } = props;
   const limitReached = projects != null && total != null && projects.length < total;
-  const defaultOption = currentUser && currentUser.isLoggedIn ? 'name' : 'analysis_date';
+  const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';
 
   return (
     <header className="page-header projects-topbar-items">
index 6da09bf1157953a54738efe4ff7364c5a9f9e284..52315fbefa5cbeae1e90527f067e48c87b4c1a23 100644 (file)
@@ -20,8 +20,8 @@
 import * as React from 'react';
 import { Link } from 'react-router';
 import { flatMap } from 'lodash';
-import FavoriteFilter from './FavoriteFilter';
-import LanguagesFilter from '../filters/LanguagesFilter';
+import FavoriteFilterContainer from './FavoriteFilterContainer';
+import LanguagesFilterContainer from '../filters/LanguagesFilterContainer';
 import CoverageFilter from '../filters/CoverageFilter';
 import DuplicationsFilter from '../filters/DuplicationsFilter';
 import MaintainabilityFilter from '../filters/MaintainabilityFilter';
@@ -45,6 +45,7 @@ interface Props {
   isFavorite: boolean;
   organization?: { key: string };
   query: RawQuery;
+  showFavoriteFilter: boolean;
   view: string;
   visualization: string;
 }
@@ -71,7 +72,9 @@ export default function PageSidebar(props: Props) {
 
   return (
     <div>
-      <FavoriteFilter query={linkQuery} organization={organization} />
+      {props.showFavoriteFilter && (
+        <FavoriteFilterContainer query={linkQuery} organization={organization} />
+      )}
 
       <div className="projects-facets-header clearfix">
         {isFiltered && (
@@ -156,7 +159,11 @@ export default function PageSidebar(props: Props) {
           value={query.new_lines}
         />
       ]}
-      <LanguagesFilter {...facetProps} facet={facets && facets.languages} value={query.languages} />
+      <LanguagesFilterContainer
+        {...facetProps}
+        facet={facets && facets.languages}
+        value={query.languages}
+      />
       <TagsFilter {...facetProps} facet={facets && facets.tags} value={query.tags} />
     </div>
   );
index 277a3b6eec83a0e725483def2afc27885aa9bd97..76f581dd53af675565bb91e8f5c829e44b51d3da 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import { sortBy } from 'lodash';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
-
-interface Languages {
-  [key: string]: { key: string; name: string };
-}
+import { Languages } from '../../../store/languages/reducer';
 
 interface Props {
   distribution?: string;
+  languages: Languages;
 }
 
-export default class ProjectCardLanguages extends React.PureComponent<Props> {
-  static contextTypes = {
-    languages: PropTypes.object.isRequired
-  };
-
-  render() {
-    if (this.props.distribution === undefined) {
-      return null;
-    }
+export default function ProjectCardLanguages({ distribution, languages }: Props) {
+  if (distribution === undefined) {
+    return null;
+  }
 
-    const parsedLanguages = this.props.distribution.split(';').map(item => item.split('='));
-    const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l =>
-      getLanguageName(this.context.languages, l[0])
-    );
+  const parsedLanguages = distribution.split(';').map(item => item.split('='));
+  const finalLanguages = sortBy(parsedLanguages, l => -1 * Number(l[1])).map(l =>
+    getLanguageName(languages, l[0])
+  );
 
-    const tooltip = (
-      <span>
-        {finalLanguages.map(language => (
-          <span key={language}>
-            {language}
-            <br />
-          </span>
-        ))}
-      </span>
-    );
+  const tooltip = (
+    <span>
+      {finalLanguages.map(language => (
+        <span key={language}>
+          {language}
+          <br />
+        </span>
+      ))}
+    </span>
+  );
 
-    const languagesText =
-      finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : '');
+  const languagesText =
+    finalLanguages.slice(0, 2).join(', ') + (finalLanguages.length > 2 ? ', ...' : '');
 
-    return (
-      <div className="project-card-languages">
-        <Tooltip placement="bottom" overlay={tooltip}>
-          <span>{languagesText}</span>
-        </Tooltip>
-      </div>
-    );
-  }
+  return (
+    <div className="project-card-languages">
+      <Tooltip placement="bottom" overlay={tooltip}>
+        <span>{languagesText}</span>
+      </Tooltip>
+    </div>
+  );
 }
 
 function getLanguageName(languages: Languages, key: string): string {
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardLanguagesContainer.tsx
new file mode 100644 (file)
index 0000000..8dbdca9
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+* 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 { connect } from 'react-redux';
+import ProjectCardLanguages from './ProjectCardLanguages';
+import { Languages } from '../../../store/languages/reducer';
+import { getLanguages } from '../../../store/rootReducer';
+
+interface StateProps {
+  languages: Languages;
+}
+
+const stateToProps = (state: any) => ({
+  languages: getLanguages(state)
+});
+
+export default connect<StateProps>(stateToProps)(ProjectCardLanguages);
index d8ce816507c5a92f91f99de3d7f7ceaaadb9d3a5..8a5b4f6d216f810527ece956b906487e988611d5 100644 (file)
@@ -23,7 +23,7 @@ import DateFromNow from '../../../components/intl/DateFromNow';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
 import ProjectCardLeakMeasures from './ProjectCardLeakMeasures';
-import ProjectCardOrganization from './ProjectCardOrganization';
+import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer';
 import Favorite from '../../../components/controls/Favorite';
 import TagsList from '../../../components/tags/TagsList';
 import PrivateBadge from '../../../components/common/PrivateBadge';
@@ -52,7 +52,9 @@ export default function ProjectCardLeak({ organization, project }: Props) {
           />
         )}
         <h2 className="project-card-name">
-          {!organization && <ProjectCardOrganization organization={project.organization} />}
+          {!organization && (
+            <ProjectCardOrganizationContainer organization={project.organization} />
+          )}
           <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
         </h2>
         {project.analysisDate && <ProjectCardQualityGate status={measures!['alert_status']} />}
index ce8c98ffd81b7ec13fc0144daf2581a8e3db6728..db72f6cedc40d07e18a6a2150436865fb55d14e4 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import OrganizationLink from '../../../components/ui/OrganizationLink';
 
 interface Props {
   organization?: { key: string; name: string };
+  organizationsEnabled: boolean;
 }
 
-export default class ProjectCardOrganization extends React.PureComponent<Props> {
-  static contextTypes = {
-    organizationsEnabled: PropTypes.bool
-  };
-
-  render() {
-    const { organization } = this.props;
-    const { organizationsEnabled } = this.context;
-
-    if (!organization || !organizationsEnabled) {
-      return null;
-    }
-
-    return (
-      <span className="text-normal">
-        <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
-        <span className="slash-separator" />
-      </span>
-    );
+export default function ProjectCardOrganization({ organization, organizationsEnabled }: Props) {
+  if (!organization || !organizationsEnabled) {
+    return null;
   }
+
+  return (
+    <span className="text-normal">
+      <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+      <span className="slash-separator" />
+    </span>
+  );
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx b/server/sonar-web/src/main/js/apps/projects/components/ProjectCardOrganizationContainer.tsx
new file mode 100644 (file)
index 0000000..099ee54
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+* 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 { connect } from 'react-redux';
+import ProjectCardOrganization from './ProjectCardOrganization';
+import { areThereCustomOrganizations } from '../../../store/rootReducer';
+
+interface StateProps {
+  organizationsEnabled: boolean;
+}
+
+const stateToProps = (state: any) => ({
+  organizationsEnabled: areThereCustomOrganizations(state)
+});
+
+export default connect<StateProps>(stateToProps)(ProjectCardOrganization);
index 7dcae1fe7a2bda05ec74aa5219e7a8366d6289b5..d8ecbe86f66ee5d8bb146933b09a829f02a68fc3 100644 (file)
@@ -22,7 +22,7 @@ import { Link } from 'react-router';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
 import ProjectCardOverallMeasures from './ProjectCardOverallMeasures';
-import ProjectCardOrganization from './ProjectCardOrganization';
+import ProjectCardOrganizationContainer from './ProjectCardOrganizationContainer';
 import Favorite from '../../../components/controls/Favorite';
 import TagsList from '../../../components/tags/TagsList';
 import PrivateBadge from '../../../components/common/PrivateBadge';
@@ -51,7 +51,9 @@ export default function ProjectCardOverall({ organization, project }: Props) {
           />
         )}
         <h2 className="project-card-name">
-          {!organization && <ProjectCardOrganization organization={project.organization} />}
+          {!organization && (
+            <ProjectCardOrganizationContainer organization={project.organization} />
+          )}
           <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
         </h2>
         {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
index 5bba45518c1d0e9033c580e8e7cfd77342fa332a..0fd1b4048efcd07e0698b34b41adee162d33d689 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import ProjectCardLanguages from './ProjectCardLanguages';
+import ProjectCardLanguagesContainer from './ProjectCardLanguagesContainer';
 import Measure from '../../../components/measure/Measure';
 import Rating from '../../../components/ui/Rating';
 import CoverageRating from '../../../components/ui/CoverageRating';
@@ -152,7 +152,9 @@ export default function ProjectCardOverallMeasures({ measures }: Props) {
               </span>
             </div>
             <div className="project-card-measure-label">
-              <ProjectCardLanguages distribution={measures['ncloc_language_distribution']} />
+              <ProjectCardLanguagesContainer
+                distribution={measures['ncloc_language_distribution']}
+              />
             </div>
           </div>
         </div>
index 8a12762240dc405223ac665b854934c873ee1091..f3a02bcc1a762b1221f7affe5c70a19d7fbda72f 100644 (file)
@@ -20,7 +20,7 @@
 /* eslint-disable import/order */
 import * as React from 'react';
 import { mount, shallow } from 'enzyme';
-import AllProjects from '../AllProjects';
+import AllProjects, { Props } from '../AllProjects';
 import { getView, saveSort, saveView, saveVisualization } from '../../../../helpers/storage';
 
 jest.mock('../ProjectsList', () => ({
@@ -168,19 +168,31 @@ it('changes perspective to risk visualization', () => {
 function mountRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) {
   return mount(
     <AllProjects
+      currentUser={{ isLoggedIn: true }}
       fetchProjects={jest.fn()}
       isFavorite={false}
       location={{ pathname: '/projects', query: {} }}
       {...props}
     />,
-    { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } }
+    { context: { router: { push, replace } } }
   );
 }
 
-function shallowRender(props: any = {}, push: Function = jest.fn(), replace: Function = jest.fn()) {
+function shallowRender(
+  props: Partial<Props> = {},
+  push: Function = jest.fn(),
+  replace: Function = jest.fn()
+) {
   const wrapper = shallow(
-    <AllProjects isFavorite={false} location={{ pathname: '/projects', query: {} }} {...props} />,
-    { context: { currentUser: { isLoggedIn: true }, router: { push, replace } } }
+    <AllProjects
+      currentUser={{ isLoggedIn: true }}
+      isFavorite={false}
+      location={{ pathname: '/projects', query: {} }}
+      onSonarCloud={false}
+      organizationsEnabled={false}
+      {...props}
+    />,
+    { context: { router: { push, replace } } }
   );
   wrapper.setState({
     loading: false,
index b5d80b02f7ea16fb45c023e7cbf5ea9dd485e39f..19e0932dd3a06869cf3d59c9b8e98944c045489e 100644 (file)
@@ -18,9 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 /* eslint-disable import/first, import/order */
-jest.mock('../AllProjects', () => ({
+jest.mock('../AllProjectsContainer', () => ({
   // eslint-disable-next-line
-  default: function AllProjects() {
+  default: function AllProjectsContainer() {
     return null;
   }
 }));
@@ -37,6 +37,7 @@ jest.mock('../../../../api/components', () => ({
 import * as React from 'react';
 import { mount } from 'enzyme';
 import DefaultPageSelector from '../DefaultPageSelector';
+import { CurrentUser } from '../../../../app/types';
 import { doAsync } from '../../../../helpers/testUtils';
 
 const isFavoriteSet = require('../../../../helpers/storage').isFavoriteSet as jest.Mock<any>;
@@ -84,8 +85,17 @@ it('fetches favorites', () => {
   });
 });
 
-function mountRender(user: any = { isLoggedIn: true }, query: any = {}, replace: any = jest.fn()) {
-  return mount(<DefaultPageSelector location={{ pathname: '/projects', query }} />, {
-    context: { currentUser: user, router: { replace } }
-  });
+function mountRender(
+  currentUser: CurrentUser = { isLoggedIn: true },
+  query: any = {},
+  replace: any = jest.fn()
+) {
+  return mount(
+    <DefaultPageSelector
+      currentUser={currentUser}
+      location={{ pathname: '/projects', query }}
+      onSonarCloud={false}
+    />,
+    { context: { router: { replace } } }
+  );
 }
index 5b235e0a19362d75f8fab6c9311e628858f37ceb..3f90ede583f597f5d80c32fb770749cdabab76e3 100644 (file)
@@ -38,11 +38,11 @@ beforeEach(() => {
 });
 
 it('renders for logged in user', () => {
-  expect(shallow(<FavoriteFilter query={query} />, { context: { currentUser } })).toMatchSnapshot();
+  expect(shallow(<FavoriteFilter currentUser={currentUser} query={query} />)).toMatchSnapshot();
 });
 
 it('saves last selection', () => {
-  const wrapper = shallow(<FavoriteFilter query={query} />, { context: { currentUser } });
+  const wrapper = shallow(<FavoriteFilter currentUser={currentUser} query={query} />);
   click(wrapper.find('#favorite-projects'));
   expect(saveFavorite).toBeCalled();
   click(wrapper.find('#all-projects'));
@@ -51,16 +51,16 @@ it('saves last selection', () => {
 
 it('handles organization', () => {
   expect(
-    shallow(<FavoriteFilter organization={{ key: 'org' }} query={query} />, {
-      context: { currentUser }
-    })
+    shallow(
+      <FavoriteFilter currentUser={currentUser} organization={{ key: 'org' }} query={query} />
+    )
   ).toMatchSnapshot();
 });
 
 it('does not save last selection with organization', () => {
-  const wrapper = shallow(<FavoriteFilter organization={{ key: 'org' }} query={query} />, {
-    context: { currentUser }
-  });
+  const wrapper = shallow(
+    <FavoriteFilter currentUser={currentUser} organization={{ key: 'org' }} query={query} />
+  );
   click(wrapper.find('#favorite-projects'));
   expect(saveFavorite).not.toBeCalled();
   click(wrapper.find('#all-projects'));
@@ -69,8 +69,6 @@ it('does not save last selection with organization', () => {
 
 it('does not render for anonymous', () => {
   expect(
-    shallow(<FavoriteFilter query={query} />, {
-      context: { currentUser: { isLoggedIn: false } }
-    }).type()
+    shallow(<FavoriteFilter currentUser={{ isLoggedIn: false }} query={query} />).type()
   ).toBeNull();
 });
index 86ef27cc3a5b5f5347df9489cbf6b6c1e81b3d37..279297daf13a2a1083c3f69f670a1f7fddcae0b9 100644 (file)
@@ -70,6 +70,7 @@ it('should render switch the default sorting option for anonymous users', () =>
 function shallowRender(props?: {}) {
   return shallow(
     <PageHeader
+      currentUser={{ isLoggedIn: false }}
       loading={false}
       onPerspectiveChange={jest.fn()}
       onSortChange={jest.fn()}
index b6ba776c0da2b12c34f430f98afa9e793fdf1f29..e19492bd40e6c7cf0b9f985c064cbdd7836e5e81 100644 (file)
@@ -23,14 +23,26 @@ import PageSidebar from '../PageSidebar';
 
 it('should render correctly', () => {
   const sidebar = shallow(
-    <PageSidebar query={{ size: '3' }} view="overall" visualization="risk" isFavorite={true} />
+    <PageSidebar
+      isFavorite={true}
+      query={{ size: '3' }}
+      showFavoriteFilter={true}
+      view="overall"
+      visualization="risk"
+    />
   );
   expect(sidebar).toMatchSnapshot();
 });
 
 it('should render `leak` view correctly', () => {
   const sidebar = shallow(
-    <PageSidebar query={{ view: 'leak' }} view="leak" visualization="risk" isFavorite={false} />
+    <PageSidebar
+      isFavorite={false}
+      query={{ view: 'leak' }}
+      showFavoriteFilter={true}
+      view="leak"
+      visualization="risk"
+    />
   );
   expect(sidebar).toMatchSnapshot();
 });
@@ -38,10 +50,11 @@ it('should render `leak` view correctly', () => {
 it('reset function should work correctly with view and visualizations', () => {
   const sidebar = shallow(
     <PageSidebar
+      isFavorite={false}
       query={{ view: 'visualizations', visualization: 'bugs' }}
+      showFavoriteFilter={true}
       view="visualizations"
       visualization="bugs"
-      isFavorite={false}
     />
   );
   expect(sidebar.find('.projects-facets-reset').exists()).toBeFalsy();
index c81dffce5e4b5b55cc4484b1e890c7916588bba0..ec986854e76e94eed1d3997fb60cb86da4d00f1d 100644 (file)
@@ -28,26 +28,26 @@ const languages = {
 
 it('renders', () => {
   expect(
-    shallow(<ProjectCardLanguages distribution="java=137;js=15" />, { context: { languages } })
+    shallow(<ProjectCardLanguages distribution="java=137;js=15" languages={languages} />)
   ).toMatchSnapshot();
 });
 
 it('sorts languages', () => {
   expect(
-    shallow(<ProjectCardLanguages distribution="java=13;js=152" />, { context: { languages } })
+    shallow(<ProjectCardLanguages distribution="java=13;js=152" languages={languages} />)
   ).toMatchSnapshot();
 });
 
 it('handles unknown languages', () => {
   expect(
-    shallow(<ProjectCardLanguages distribution="java=13;cpp=18" />, { context: { languages } })
+    shallow(<ProjectCardLanguages distribution="java=13;cpp=18" languages={languages} />)
   ).toMatchSnapshot();
 
   expect(
-    shallow(<ProjectCardLanguages distribution="java=13;<null>=18" />, { context: { languages } })
+    shallow(<ProjectCardLanguages distribution="java=13;<null>=18" languages={languages} />)
   ).toMatchSnapshot();
 });
 
 it('does not render', () => {
-  expect(shallow(<ProjectCardLanguages />, { context: { languages } }).type()).toBeNull();
+  expect(shallow(<ProjectCardLanguages languages={languages} />).type()).toBeNull();
 });
index ef7225c06eb7f2f589abf4967cb2ecdbe22451f5..14e6af9b4e5d6aacd6329e91ea2d1b14490c59bd 100644 (file)
@@ -3,6 +3,7 @@
 exports[`renders 1`] = `
 <div
   className="layout-page projects-page"
+  id="projects-page"
 >
   <HelmetWrapper
     defer={true}
@@ -51,6 +52,7 @@ exports[`renders 1`] = `
                 "visualization": undefined,
               }
             }
+            showFavoriteFilter={true}
             view="overall"
             visualization="risk"
           />
@@ -174,6 +176,7 @@ exports[`renders 1`] = `
 exports[`renders 2`] = `
 <div
   className="layout-page projects-page"
+  id="projects-page"
 >
   <HelmetWrapper
     defer={true}
@@ -204,6 +207,7 @@ exports[`renders 2`] = `
                 "view": "visualizations",
               }
             }
+            showFavoriteFilter={true}
             view="visualizations"
             visualization="risk"
           />
index 78961fde19f5d08e789c82998035527712c4a370..cabe4555b867d71c43f2d096c857fc9036546fdc 100644 (file)
@@ -25,7 +25,7 @@ exports[`reset function should work correctly with view and visualizations 1`] =
 
 exports[`should render \`leak\` view correctly 1`] = `
 <div>
-  <FavoriteFilter
+  <Connect(FavoriteFilter)
     query={
       Object {
         "view": "leak",
@@ -101,7 +101,7 @@ exports[`should render \`leak\` view correctly 1`] = `
       }
     }
   />
-  <LanguagesFilter
+  <Connect(LanguagesFilter)
     isFavorite={false}
     query={
       Object {
@@ -122,7 +122,7 @@ exports[`should render \`leak\` view correctly 1`] = `
 
 exports[`should render correctly 1`] = `
 <div>
-  <FavoriteFilter />
+  <Connect(FavoriteFilter) />
   <div
     className="projects-facets-header clearfix"
   >
@@ -210,7 +210,7 @@ exports[`should render correctly 1`] = `
     }
     value="3"
   />
-  <LanguagesFilter
+  <Connect(LanguagesFilter)
     isFavorite={true}
     query={
       Object {
index e293ca6efc96b7081d7c522f498ebf61f393bce0..e53e1df3fe9927e0d41d1f0ffd88da9864f730d6 100644 (file)
@@ -11,7 +11,7 @@ exports[`should display the leak measures and quality gate 1`] = `
     <h2
       className="project-card-name"
     >
-      <ProjectCardOrganization
+      <Connect(ProjectCardOrganization)
         organization={
           Object {
             "key": "org",
index 7fdfbd65dbe840d7d17b37e4814fd463233c5dc7..115f01c75337ea7e662b0b23d1b655daefbe93bd 100644 (file)
@@ -11,7 +11,7 @@ exports[`should display the overall measures and quality gate 1`] = `
     <h2
       className="project-card-name"
     >
-      <ProjectCardOrganization
+      <Connect(ProjectCardOrganization)
         organization={
           Object {
             "key": "org",
index ed38e592644559af2b80b7e47477e16c611387d8..74ef1f08cfeeaeeb580a5c5868c3f3ab94ae5bf7 100644 (file)
@@ -280,7 +280,7 @@ exports[`should render correctly with all data 1`] = `
       <div
         className="project-card-measure-label"
       >
-        <ProjectCardLanguages />
+        <Connect(ProjectCardLanguages) />
       </div>
     </div>
   </div>
@@ -320,7 +320,7 @@ exports[`should render ncloc correctly 1`] = `
     <div
       className="project-card-measure-label"
     >
-      <ProjectCardLanguages />
+      <Connect(ProjectCardLanguages) />
     </div>
   </div>
 </div>
index 385425c48f6692bb22647086ffeb50e02d50e9fa..fce64b5de4c323712450750c0e64746f46f162b0 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import { difference, sortBy } from 'lodash';
 import Filter from './Filter';
 import FilterHeader from './FilterHeader';
 import SearchableFilterFooter from './SearchableFilterFooter';
 import SearchableFilterOption from './SearchableFilterOption';
-import { getLanguageByKey } from '../../../store/languages/reducer';
+import { getLanguageByKey, Languages } from '../../../store/languages/reducer';
 import { translate } from '../../../helpers/l10n';
 import { Facet } from '../types';
 
 interface Props {
   facet?: Facet;
   isFavorite?: boolean;
+  languages: Languages;
   maxFacetValue?: number;
   organization?: { key: string };
   property?: string;
@@ -41,18 +41,14 @@ interface Props {
 const LIST_SIZE = 10;
 
 export default class LanguagesFilter extends React.Component<Props> {
-  static contextTypes = {
-    languages: PropTypes.object.isRequired
-  };
-
   getSearchOptions = () => {
-    let languageKeys = Object.keys(this.context.languages);
+    let languageKeys = Object.keys(this.props.languages);
     if (this.props.facet) {
       languageKeys = difference(languageKeys, Object.keys(this.props.facet));
     }
     return languageKeys
       .slice(0, LIST_SIZE)
-      .map(key => ({ label: this.context.languages[key].name, value: key }));
+      .map(key => ({ label: this.props.languages[key].name, value: key }));
   };
 
   getSortedOptions = (facet: Facet = {}) =>
@@ -63,7 +59,7 @@ export default class LanguagesFilter extends React.Component<Props> {
   renderOption = (option: string) => (
     <SearchableFilterOption
       optionKey={option}
-      option={getLanguageByKey(this.context.languages, option)}
+      option={getLanguageByKey(this.props.languages, option)}
     />
   );
 
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.tsx
new file mode 100644 (file)
index 0000000..f6de241
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+* 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 { connect } from 'react-redux';
+import LanguagesFilter from './LanguagesFilter';
+import { Languages } from '../../../store/languages/reducer';
+import { getLanguages } from '../../../store/rootReducer';
+
+interface StateProps {
+  languages: Languages;
+}
+
+const stateToProps = (state: any) => ({
+  languages: getLanguages(state)
+});
+
+export default connect<StateProps>(stateToProps)(LanguagesFilter);
index 27ea25d17dd3e43d0f351918817e2411ebd03d0c..ff0bf4f8b37cad8976dc4a79759706a1d215c558 100644 (file)
@@ -33,21 +33,21 @@ const languages = {
 const languagesFacet = { java: 39, cs: 4, js: 1 };
 
 it('should render the languages without the ones in the facet', () => {
-  const wrapper = shallow(<LanguagesFilter query={{ languages: null }} facet={languagesFacet} />, {
-    context: { languages }
-  });
+  const wrapper = shallow(
+    <LanguagesFilter facet={languagesFacet} languages={languages} query={{ languages: null }} />
+  );
   expect(wrapper).toMatchSnapshot();
 });
 
 it('should render the languages facet with the selected languages', () => {
   const wrapper = shallow(
     <LanguagesFilter
-      query={{ languages: ['java', 'cs'] }}
-      value={['java', 'cs']}
       facet={languagesFacet}
       isFavorite={true}
-    />,
-    { context: { languages } }
+      languages={languages}
+      query={{ languages: ['java', 'cs'] }}
+      value={['java', 'cs']}
+    />
   );
   expect(wrapper).toMatchSnapshot();
   expect(wrapper.find('Filter').shallow()).toMatchSnapshot();
@@ -68,12 +68,12 @@ it('should render maximum 10 languages in the searchbox results', () => {
   };
   const wrapper = shallow(
     <LanguagesFilter
-      query={{ languages: ['java', 'g'] }}
-      value={['java', 'g']}
       facet={{ ...languagesFacet, g: 1 }}
       isFavorite={true}
-    />,
-    { context: { languages: manyLanguages } }
+      languages={manyLanguages}
+      query={{ languages: ['java', 'g'] }}
+      value={['java', 'g']}
+    />
   );
   expect(wrapper).toMatchSnapshot();
 });
index c6f3df309f5bedf42f329a6d8ac5b80a421ebff8..8d260c75965c4bc5fac62eae143a80b83292cfa2 100644 (file)
  * 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, RouteComponent, RedirectFunction } from 'react-router';
+import { RouterState, RedirectFunction } from 'react-router';
+import DefaultPageSelectorContainer from './components/DefaultPageSelectorContainer';
+import FavoriteProjectsContainer from './components/FavoriteProjectsContainer';
 import { saveAll } from '../../helpers/storage';
 
 const routes = [
+  { indexRoute: { component: DefaultPageSelectorContainer } },
   {
-    getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
-      import('./components/App').then(i => callback(null, i.default));
-    },
-    childRoutes: [
-      {
-        getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
-          import('./components/DefaultPageSelector').then(i =>
-            callback(null, { component: i.default })
-          );
-        }
-      },
-      {
-        path: 'all',
-        onEnter(_: RouterState, replace: RedirectFunction) {
-          saveAll();
-          replace('/projects');
-        }
-      },
-      {
-        path: 'favorite',
-        getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
-          import('./components/FavoriteProjectsContainer').then(i => callback(null, i.default));
-        }
-      }
-    ]
-  }
+    path: 'all',
+    onEnter(_: RouterState, replace: RedirectFunction) {
+      saveAll();
+      replace('/projects');
+    }
+  },
+  { path: 'favorite', component: FavoriteProjectsContainer }
 ];
 
 export default routes;
diff --git a/server/sonar-web/src/main/js/components/lazyLoad.tsx b/server/sonar-web/src/main/js/components/lazyLoad.tsx
new file mode 100644 (file)
index 0000000..dfc83a1
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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';
+
+interface Loader {
+  (): Promise<{ default: React.ComponentClass }>;
+}
+
+export function lazyLoad(loader: Loader) {
+  interface State {
+    Component?: React.ComponentClass;
+  }
+
+  // use `React.Component`, not `React.PureComponent` to always re-render
+  // and let the child component decide if it needs to change
+  return class LazyLoader extends React.Component<any, State> {
+    mounted: boolean;
+    state: State = {};
+
+    componentDidMount() {
+      this.mounted = true;
+      loader().then(i => this.receiveComponent(i.default), () => {});
+    }
+
+    componentWillUnmount() {
+      this.mounted = false;
+    }
+
+    receiveComponent = (Component: React.ComponentClass) => {
+      if (this.mounted) {
+        this.setState({ Component });
+      }
+    };
+
+    render() {
+      const { Component } = this.state;
+
+      if (!Component) {
+        return null;
+      }
+
+      return <Component {...this.props} />;
+    }
+  };
+}
diff --git a/server/sonar-web/src/main/js/store/languages/reducer.js b/server/sonar-web/src/main/js/store/languages/reducer.js
deleted file mode 100644 (file)
index 5217724..0000000
+++ /dev/null
@@ -1,35 +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 { keyBy } from 'lodash';
-import { RECEIVE_LANGUAGES } from './actions';
-
-const reducer = (state = {}, action = {}) => {
-  if (action.type === RECEIVE_LANGUAGES) {
-    return keyBy(action.languages, 'key');
-  }
-
-  return state;
-};
-
-export default reducer;
-
-export const getLanguages = state => state;
-
-export const getLanguageByKey = (state, key) => state[key];
diff --git a/server/sonar-web/src/main/js/store/languages/reducer.ts b/server/sonar-web/src/main/js/store/languages/reducer.ts
new file mode 100644 (file)
index 0000000..a137ef7
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 { keyBy } from 'lodash';
+import { RECEIVE_LANGUAGES } from './actions';
+
+export interface Languages {
+  [key: string]: { key: string; name: string };
+}
+
+const reducer = (state: Languages = {}, action: any = {}) => {
+  if (action.type === RECEIVE_LANGUAGES) {
+    return keyBy(action.languages, 'key');
+  }
+
+  return state;
+};
+
+export default reducer;
+
+export const getLanguages = (state: Languages) => state;
+
+export const getLanguageByKey = (state: Languages, key: string) => state[key];
diff --git a/server/sonar-web/src/main/js/store/withCurrentUser.tsx b/server/sonar-web/src/main/js/store/withCurrentUser.tsx
new file mode 100644 (file)
index 0000000..1a2d815
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 { connect } from 'react-redux';
+import * as React from 'react';
+import { getCurrentUser } from './rootReducer';
+import { CurrentUser } from '../app/types';
+
+interface StateProps {
+  currentUser: CurrentUser;
+}
+
+export function withCurrentUser<P extends StateProps>(Component: React.ComponentClass<P>) {
+  function mapStateToProps(state: any): StateProps {
+    return { currentUser: getCurrentUser(state) };
+  }
+
+  return connect<StateProps>(mapStateToProps)(Component);
+}
index f12b6c3bcb01a51bf0d23a6ed7ef81d9d4572f81..671127dca10de03d5ca97d4211f3a17193792741 100644 (file)
@@ -97,6 +97,7 @@ more_x={0} more
 more_actions=More Actions
 my_favorite=My Favorite
 my_favorites=My Favorites
+my_projects=My Projects
 name=Name
 navigation=Navigation
 never=Never