+++ /dev/null
-/*
- * 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
-});
--- /dev/null
+/*
+ * 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)
+);
+
+++ /dev/null
-/*
- * 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
-);
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';
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) => (
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;
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 {
<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>
}
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')}
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;
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 }));
});
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 }));
});
}
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));
-};
font-size: 12px;
}
+.project-card-quality-gate {
+ line-height: 24px;
+}
+
.projects-facet-header {
padding-top: 10px;
padding-bottom: 10px;
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)}/>
);
}
}
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) {
render () {
const className = classNames('icon-star', {
'icon-star-favorite': this.state.favorite
- });
+ }, this.props.className);
return (
<a className={className}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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);