]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8362 Display information of favorite projects
authorStas Vilchik <vilchiks@gmail.com>
Wed, 9 Nov 2016 08:41:59 +0000 (09:41 +0100)
committerStas Vilchik <vilchiks@gmail.com>
Wed, 9 Nov 2016 14:51:40 +0000 (15:51 +0100)
13 files changed:
server/sonar-web/src/main/js/app/store/favorites/actions.js [deleted file]
server/sonar-web/src/main/js/app/store/favorites/duck.js [new file with mode: 0644]
server/sonar-web/src/main/js/app/store/favorites/reducer.js [deleted file]
server/sonar-web/src/main/js/app/store/rootReducer.js
server/sonar-web/src/main/js/app/styles/boxed-group.css
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
server/sonar-web/src/main/js/apps/projects/components/ProjectCardQualityGate.js
server/sonar-web/src/main/js/apps/projects/store/actions.js
server/sonar-web/src/main/js/apps/projects/styles.css
server/sonar-web/src/main/js/components/controls/Favorite.js
server/sonar-web/src/main/js/components/controls/FavoriteBase.js
server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/FavoriteContainer.js [new file with mode: 0644]

diff --git a/server/sonar-web/src/main/js/app/store/favorites/actions.js b/server/sonar-web/src/main/js/app/store/favorites/actions.js
deleted file mode 100644 (file)
index 2fc7db4..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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.
- */
-export const RECEIVE_FAVORITES = 'RECEIVE_FAVORITES';
-
-export const receiveFavorites = favorites => ({
-  type: RECEIVE_FAVORITES,
-  favorites
-});
diff --git a/server/sonar-web/src/main/js/app/store/favorites/duck.js b/server/sonar-web/src/main/js/app/store/favorites/duck.js
new file mode 100644 (file)
index 0000000..7c6d4b5
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 uniq from 'lodash/uniq';
+import without from 'lodash/without';
+
+export const actions = {
+  RECEIVE_FAVORITES: 'RECEIVE_FAVORITES',
+  ADD_FAVORITE: 'ADD_FAVORITE',
+  REMOVE_FAVORITE: 'REMOVE_FAVORITE'
+};
+
+export const receiveFavorites = favorites => ({
+  type: actions.RECEIVE_FAVORITES,
+  favorites
+});
+
+export const addFavorite = componentKey => ({
+  type: actions.ADD_FAVORITE,
+  componentKey
+});
+
+export const removeFavorite = componentKey => ({
+  type: actions.REMOVE_FAVORITE,
+  componentKey
+});
+
+export default (state = [], action = {}) => {
+  if (action.type === actions.RECEIVE_FAVORITES) {
+    return uniq([...state, ...action.favorites.map(f => f.key)]);
+  }
+
+  if (action.type === actions.ADD_FAVORITE) {
+    return uniq([...state, action.componentKey]);
+  }
+
+  if (action.type === actions.REMOVE_FAVORITE) {
+    return without(state, action.componentKey);
+  }
+
+  return state;
+};
+
+export const isFavorite = (state, componentKey) => (
+    state.includes(componentKey)
+);
+
diff --git a/server/sonar-web/src/main/js/app/store/favorites/reducer.js b/server/sonar-web/src/main/js/app/store/favorites/reducer.js
deleted file mode 100644 (file)
index 29d19fe..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 { combineReducers } from 'redux';
-import keyBy from 'lodash/keyBy';
-import { RECEIVE_FAVORITES } from './actions';
-
-const favoritesByKey = (state = {}, action = {}) => {
-  if (action.type === RECEIVE_FAVORITES) {
-    const byKey = keyBy(action.favorites, 'key');
-    return { ...state, ...byKey };
-  }
-
-  return state;
-};
-
-const favoriteKeys = (state = null, action = {}) => {
-  if (action.type === RECEIVE_FAVORITES) {
-    return action.favorites.map(f => f.key);
-  }
-
-  return state;
-};
-
-export default combineReducers({ favoritesByKey, favoriteKeys });
-
-export const getFavorites = state => (
-    state.favoriteKeys ?
-        state.favoriteKeys.map(key => state.favoritesByKey[key]) :
-        null
-);
index 3c2ac6abb2b37059d7a19bc3b41c70bc6ae7dad4..40c02dafbf7ff63bb220ee5c1f292243918592c2 100644 (file)
@@ -20,7 +20,7 @@
 import { combineReducers } from 'redux';
 import components, * as fromComponents from './components/reducer';
 import users, * as fromUsers from './users/reducer';
-import favorites, * as fromFavorites from './favorites/reducer';
+import favorites, * as fromFavorites from './favorites/duck';
 import languages, * as fromLanguages from './languages/reducer';
 import measures, * as fromMeasures from './measures/reducer';
 import globalMessages, * as fromGlobalMessages from '../../components/store/globalMessages';
@@ -57,8 +57,8 @@ export const getCurrentUser = state => (
     fromUsers.getCurrentUser(state.users)
 );
 
-export const getFavorites = state => (
-    fromFavorites.getFavorites(state.favorites)
+export const isFavorite = (state, componentKey) => (
+    fromFavorites.isFavorite(state.favorites, componentKey)
 );
 
 export const getComponentMeasure = (state, componentKey, metricKey) => (
index 6709d6523817236301927fdd98f8f1d24b6e1d09..3b3836cb6cc44419ea96dddffd022bcad72e1022 100644 (file)
   margin: 15px -20px;
 }
 
+.boxed-group-header {
+  padding: 15px 20px 0;
+}
+
+.boxed-group-header > h2 {
+  display: inline-block;
+  vertical-align: middle;
+  line-height: 24px;
+}
+
+.boxed-group-header > [class^="icon-"] {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.boxed-group-header > .icon-star {
+  position: relative;
+  top: 1px;
+}
+
 .boxed-group-actions {
   float: right;
   margin-top: 15px;
index 9b9ec7fc11f0d813cb20e1349f0c2b20f8c8ec9c..79a4b28d9f624d60e487de06992396e03a8d5a30 100644 (file)
@@ -21,6 +21,7 @@ import React from 'react';
 import classNames from 'classnames';
 import ProjectCardQualityGate from './ProjectCardQualityGate';
 import ProjectCardMeasures from './ProjectCardMeasures';
+import FavoriteContainer from '../../../components/controls/FavoriteContainer';
 import { getComponentUrl } from '../../../helpers/urls';
 
 export default class ProjectCard extends React.Component {
@@ -44,9 +45,14 @@ export default class ProjectCard extends React.Component {
                 <ProjectCardQualityGate status={this.props.measures['alert_status']}/>
               </div>
           )}
-          <h2 className="project-card-name">
-            <a className="link-base-color" href={getComponentUrl(project.key)}>{project.name}</a>
-          </h2>
+          <div className="boxed-group-header">
+            {project.isFavorite != null && (
+                <FavoriteContainer className="spacer-right" componentKey={project.key}/>
+            )}
+            <h2 className="project-card-name">
+              <a className="link-base-color" href={getComponentUrl(project.key)}>{project.name}</a>
+            </h2>
+          </div>
           <div className="boxed-group-inner">
             <ProjectCardMeasures measures={this.props.measures}/>
           </div>
index 1f60437b534fc62564a00c2e6216913bde738a36..0b81e2ceb8afbafeecebf677b3515ec98d566add 100644 (file)
@@ -34,7 +34,7 @@ export default class ProjectCardQualityGate extends React.Component {
     }
 
     return (
-        <div className="project-card-measure">
+        <div className="project-card-measure project-card-quality-gate">
           <div className="project-card-measure-inner">
             <span className="small spacer-right">
               {translate('overview.quality_gate')}
index 9582b7a7dee7e9fa1b94cc1968a13fb73d93c2f0..922400ead5993488d7e7dbe83b9ace91d4a3cadc 100644 (file)
@@ -28,8 +28,7 @@ import { getProjectsAppState } from '../../../app/store/rootReducer';
 import { getMeasuresForProjects } from '../../../api/measures';
 import { receiveComponentsMeasures } from '../../../app/store/measures/actions';
 import { convertToFilter } from './utils';
-import { getFavorites } from '../../../api/favorites';
-import { receiveFavorites } from '../../../app/store/favorites/actions';
+import { receiveFavorites } from '../../../app/store/favorites/duck';
 
 const PAGE_SIZE = 50;
 
@@ -84,9 +83,17 @@ const fetchProjectMeasures = projects => dispatch => {
   return getMeasuresForProjects(projectKeys, METRICS).then(onReceiveMeasures(dispatch), onFail(dispatch));
 };
 
+const handleFavorites = (dispatch, projects) => {
+  const favorites = projects.filter(project => project.isFavorite);
+  if (favorites.length) {
+    dispatch(receiveFavorites(favorites));
+  }
+};
+
 const onReceiveProjects = dispatch => response => {
   dispatch(receiveComponents(response.components));
   dispatch(receiveProjects(response.components, response.facets));
+  handleFavorites(dispatch, response.components);
   dispatch(fetchProjectMeasures(response.components)).then(() => {
     dispatch(updateState({ loading: false }));
   });
@@ -99,6 +106,7 @@ const onReceiveProjects = dispatch => response => {
 const onReceiveMoreProjects = dispatch => response => {
   dispatch(receiveComponents(response.components));
   dispatch(receiveMoreProjects(response.components));
+  handleFavorites(dispatch, response.components);
   dispatch(fetchProjectMeasures(response.components)).then(() => {
     dispatch(updateState({ loading: false }));
   });
@@ -126,23 +134,3 @@ export const fetchMoreProjects = query => (dispatch, getState) => {
   }
   return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch));
 };
-
-export const fetchFavoriteProjects = () => dispatch => {
-  dispatch(updateState({ loading: true }));
-
-  return getFavorites().then(favorites => {
-    dispatch(receiveFavorites(favorites));
-
-    const projects = favorites.filter(component => component.qualifier === 'TRK');
-
-    dispatch(receiveComponents(projects));
-    dispatch(receiveProjects(projects, []));
-    dispatch(fetchProjectMeasures(projects)).then(() => {
-      dispatch(updateState({ loading: false }));
-    });
-    dispatch(updateState({
-      total: projects.length,
-      pageIndex: 1
-    }));
-  }, onFail(dispatch));
-};
index a71aceb70d25bf007c21fecc183311245200a047..d9ac7cbd9c5b2a41804b4be64b586adea344589b 100644 (file)
   font-size: 12px;
 }
 
+.project-card-quality-gate {
+  line-height: 24px;
+}
+
 .projects-facet-header {
   padding-top: 10px;
   padding-bottom: 10px;
index 5e35ba2f1b26205f9a7b87fe83fe9b4f3dba0720..92767a8655b04ec5500dfe6927c193f54790d1d3 100644 (file)
@@ -24,15 +24,19 @@ import { addFavorite, removeFavorite } from '../../api/favorites';
 export default class Favorite extends React.Component {
   static propTypes = {
     favorite: React.PropTypes.bool.isRequired,
-    component: React.PropTypes.string.isRequired
+    component: React.PropTypes.string.isRequired,
+    className: React.PropTypes.string
   };
 
   render () {
+    const { favorite, component, ...other } = this.props;
+
     return (
         <FavoriteBase
-            favorite={this.props.favorite}
-            addFavorite={() => addFavorite(this.props.component)}
-            removeFavorite={() => removeFavorite(this.props.component)}/>
+            {...other}
+            favorite={favorite}
+            addFavorite={() => addFavorite(component)}
+            removeFavorite={() => removeFavorite(component)}/>
     );
   }
 }
index 04ee1fc464089544f7a7e5114b4506a9479b6f74..8749b2ebd778cc2149fb1542c8e8457ba1fb7e5d 100644 (file)
@@ -24,7 +24,8 @@ export default class FavoriteBase extends React.Component {
   static propTypes = {
     favorite: React.PropTypes.bool.isRequired,
     addFavorite: React.PropTypes.func.isRequired,
-    removeFavorite: React.PropTypes.func.isRequired
+    removeFavorite: React.PropTypes.func.isRequired,
+    className: React.PropTypes.string
   };
 
   constructor (props) {
@@ -79,7 +80,7 @@ export default class FavoriteBase extends React.Component {
   render () {
     const className = classNames('icon-star', {
       'icon-star-favorite': this.state.favorite
-    });
+    }, this.props.className);
 
     return (
         <a className={className}
diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js b/server/sonar-web/src/main/js/components/controls/FavoriteBaseStateless.js
new file mode 100644 (file)
index 0000000..d648e4f
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 React from 'react';
+import classNames from 'classnames';
+
+export default class FavoriteBaseStateless extends React.Component {
+  static propTypes = {
+    favorite: React.PropTypes.bool.isRequired,
+    addFavorite: React.PropTypes.func.isRequired,
+    removeFavorite: React.PropTypes.func.isRequired,
+    className: React.PropTypes.string
+  };
+
+  toggleFavorite = e => {
+    e.preventDefault();
+    if (this.props.favorite) {
+      this.props.removeFavorite();
+    } else {
+      this.props.addFavorite();
+    }
+  };
+
+  renderSVG () {
+    /* eslint max-len: 0 */
+    return (
+        <svg width="16" height="16">
+          <path
+              d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z"/>
+        </svg>
+    );
+  }
+
+  render () {
+    const className = classNames('icon-star', {
+      'icon-star-favorite': this.props.favorite
+    }, this.props.className);
+
+    return (
+        <a className={className}
+           href="#"
+           onClick={this.toggleFavorite}>
+          {this.renderSVG()}
+        </a>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteContainer.js b/server/sonar-web/src/main/js/components/controls/FavoriteContainer.js
new file mode 100644 (file)
index 0000000..b2157ce
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 FavoriteBaseStateless from './FavoriteBaseStateless';
+import { isFavorite } from '../../app/store/rootReducer';
+import * as actionCreators from '../../app/store/favorites/duck';
+import * as api from '../../api/favorites';
+import { addGlobalErrorMessage } from '../store/globalMessages';
+import { parseError } from '../../apps/code/utils';
+
+const addFavorite = componentKey => dispatch => {
+  // optimistic update
+  dispatch(actionCreators.addFavorite(componentKey));
+  api.addFavorite(componentKey).catch(error => {
+    dispatch(actionCreators.removeFavorite(componentKey));
+    parseError(error).then(message => dispatch(addGlobalErrorMessage(message)));
+  });
+};
+
+const removeFavorite = componentKey => dispatch => {
+  // optimistic update
+  dispatch(actionCreators.removeFavorite(componentKey));
+  api.removeFavorite(componentKey).catch(error => {
+    dispatch(actionCreators.addFavorite(componentKey));
+    parseError(error).then(message => dispatch(addGlobalErrorMessage(message)));
+  });
+};
+
+const mapStateToProps = (state, ownProps) => ({
+  favorite: isFavorite(state, ownProps.componentKey)
+});
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+  addFavorite: () => dispatch(addFavorite(ownProps.componentKey)),
+  removeFavorite: () => dispatch(removeFavorite(ownProps.componentKey))
+});
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(FavoriteBaseStateless);