@@ -51,6 +51,7 @@ | |||
"jsdom": "6.5.1", | |||
"lodash": "4.6.1", | |||
"mocha": "2.3.4", | |||
"mockery": "1.7.0", | |||
"moment": "2.10.6", | |||
"numeral": "1.5.3", | |||
"postcss-loader": "0.8.0", |
@@ -17,13 +17,16 @@ | |||
* 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 { post, requestDelete } from '../helpers/request'; | |||
import { getComponentIssuesUrl } from '../../helpers/urls'; | |||
export function addFavorite (componentKey) { | |||
const url = window.baseUrl + '/api/favourites'; | |||
const data = { key: componentKey }; | |||
return post(url, data); | |||
} | |||
export const IssuesLink = React.createClass({ | |||
render() { | |||
const url = getComponentIssuesUrl(this.props.component, this.props.params); | |||
return <a className={this.props.className} href={url}>{this.props.children}</a>; | |||
} | |||
}); | |||
export function removeFavorite (componentKey) { | |||
const url = window.baseUrl + | |||
'/api/favourites/' + encodeURIComponent(componentKey); | |||
return requestDelete(url); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import FavoriteIssueFilter from '../../../components/shared/FavoriteIssueFilter'; | |||
import FavoriteIssueFilter from '../../../components/controls/FavoriteIssueFilter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const FavoriteIssueFilters = ({ issueFilters }) => ( |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import FavoriteMeasureFilter from '../../../components/shared/FavoriteMeasureFilter'; | |||
import FavoriteMeasureFilter from '../../../components/controls/FavoriteMeasureFilter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
const FavoriteMeasureFilters = ({ measureFilters }) => ( |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import Favorite from '../../../components/shared/favorite'; | |||
import Favorite from '../../../components/controls/Favorite'; | |||
import QualifierIcon from '../../../components/shared/qualifier-icon'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getComponentUrl } from '../../../helpers/urls'; |
@@ -21,7 +21,7 @@ import React from 'react'; | |||
import { IndexLink } from 'react-router'; | |||
import UserExternalIdentity from './UserExternalIdentity'; | |||
import Avatar from '../../../components/shared/avatar'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
const UserCard = ({ user }) => { | |||
return ( |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import React from 'react'; | |||
import Checkbox from '../../../components/shared/checkbox'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
import { CURRENTS } from '../constants'; | |||
const CurrentsFilter = ({ value, onChange }) => { | |||
@@ -40,7 +40,7 @@ const CurrentsFilter = ({ value, onChange }) => { | |||
return ( | |||
<div className="bt-search-form-field"> | |||
<Checkbox | |||
initiallyChecked={checked} | |||
checked={checked} | |||
onCheck={handleChange}/> | |||
| |||
<label |
@@ -24,7 +24,7 @@ import Components from './Components'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import SourceViewer from './../../../components/source-viewer/SourceViewer'; | |||
import Search from './Search'; | |||
import ListFooter from '../../../components/shared/list-footer'; | |||
import ListFooter from '../../../components/controls/ListFooter'; | |||
import { retrieveComponentBase, retrieveComponent, loadMoreChildren, parseError } from '../utils'; | |||
import { addComponentBreadcrumbs } from '../bucket'; | |||
import { selectCoverageMetric } from '../../../helpers/measures'; |
@@ -19,8 +19,8 @@ | |||
*/ | |||
import React from 'react'; | |||
import { Rating } from '../../../components/shared/rating'; | |||
import Level from '../../../components/shared/Level'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import Level from '../../../components/ui/Level'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { formatLeak, isDiffMetric } from '../utils'; | |||
@@ -24,7 +24,7 @@ import ComponentsList from './ComponentsList'; | |||
import ListHeader from './ListHeader'; | |||
import Spinner from '../../components/Spinner'; | |||
import SourceViewer from '../../../../components/source-viewer/SourceViewer'; | |||
import ListFooter from '../../../../components/shared/list-footer'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
export default class ListView extends React.Component { | |||
static contextTypes = { |
@@ -23,7 +23,7 @@ import ComponentsList from './ComponentsList'; | |||
import ListHeader from './ListHeader'; | |||
import Spinner from '../../components/Spinner'; | |||
import SourceViewer from '../../../../components/source-viewer/SourceViewer'; | |||
import ListFooter from '../../../../components/shared/list-footer'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
export default class TreeView extends React.Component { | |||
componentDidMount () { |
@@ -19,12 +19,11 @@ | |||
*/ | |||
import moment from 'moment'; | |||
import React from 'react'; | |||
import enhance from './enhance'; | |||
import { IssuesLink } from '../../../components/shared/issues-link'; | |||
import { getMetricName } from '../helpers/metrics'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
class CodeSmells extends React.Component { | |||
renderHeader () { | |||
@@ -47,11 +46,11 @@ class CodeSmells extends React.Component { | |||
const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate); | |||
return ( | |||
<IssuesLink component={component.key} params={params}> | |||
<a href={getComponentIssuesUrl(component.key, params)}> | |||
<span title={tooltip} data-toggle="tooltip"> | |||
{formatMeasure(value, 'SHORT_WORK_DUR')} | |||
</span> | |||
</IssuesLink> | |||
</a> | |||
); | |||
} | |||
@@ -20,11 +20,9 @@ | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import shallowCompare from 'react-addons-shallow-compare'; | |||
import { DrilldownLink } from '../../../components/shared/drilldown-link'; | |||
import { IssuesLink } from '../../../components/shared/issues-link'; | |||
import { DonutChart } from '../../../components/charts/donut-chart'; | |||
import { Rating } from './../../../components/shared/rating'; | |||
import Rating from './../../../components/ui/Rating'; | |||
import Timeline from '../components/Timeline'; | |||
import { | |||
formatMeasure, | |||
@@ -35,6 +33,7 @@ import { | |||
} from '../../../helpers/measures'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { getPeriodDate } from '../../../helpers/periods'; | |||
import { getComponentIssuesUrl } from '../../../helpers/urls'; | |||
export default function enhance (ComposedComponent) { | |||
return class extends React.Component { | |||
@@ -163,11 +162,11 @@ export default function enhance (ComposedComponent) { | |||
const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate); | |||
return ( | |||
<IssuesLink component={component.key} params={params}> | |||
<a href={getComponentIssuesUrl(component.key, params)}> | |||
<span title={tooltip} data-toggle="tooltip"> | |||
{formatMeasure(value, 'SHORT_INT')} | |||
</span> | |||
</IssuesLink> | |||
</a> | |||
); | |||
} | |||
@@ -18,9 +18,8 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { QualityGateLink } from '../../../components/shared/quality-gate-link'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getQualityGateUrl } from '../../../helpers/urls'; | |||
const MetaQualityGate = ({ gate }) => { | |||
return ( | |||
@@ -36,9 +35,9 @@ const MetaQualityGate = ({ gate }) => { | |||
{'(' + translate('default') + ')'} | |||
</span> | |||
)} | |||
<QualityGateLink gate={gate.key}> | |||
<a href={getQualityGateUrl(gate.key)}> | |||
{gate.name} | |||
</QualityGateLink> | |||
</a> | |||
</li> | |||
</ul> | |||
</div> |
@@ -18,9 +18,8 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { QualityProfileLink } from '../../../components/shared/quality-profile-link'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getQualityProfileUrl } from '../../../helpers/urls'; | |||
const MetaQualityProfiles = ({ profiles }) => { | |||
return ( | |||
@@ -35,9 +34,9 @@ const MetaQualityProfiles = ({ profiles }) => { | |||
<span className="note spacer-right"> | |||
{'(' + profile.language + ')'} | |||
</span> | |||
<QualityProfileLink profile={profile.key}> | |||
<a href={getQualityProfileUrl(profile.key)}> | |||
{profile.name} | |||
</QualityProfileLink> | |||
</a> | |||
</li> | |||
))} | |||
</ul> |
@@ -20,7 +20,7 @@ | |||
import React from 'react'; | |||
import { translate } from '../../helpers/l10n'; | |||
import RadioToggle from '../../components/shared/radio-toggle'; | |||
import RadioToggle from '../../components/controls/RadioToggle'; | |||
const rootQualifiersToOptions = (qualifiers) => { | |||
return qualifiers.map(q => { |
@@ -24,7 +24,7 @@ import Search from './search'; | |||
import Projects from './projects'; | |||
import { PAGE_SIZE, TYPE } from './constants'; | |||
import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components'; | |||
import ListFooter from '../../components/shared/list-footer'; | |||
import ListFooter from '../../components/controls/ListFooter'; | |||
export default React.createClass({ | |||
propTypes: { |
@@ -20,7 +20,7 @@ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import { getComponentUrl } from '../../helpers/urls'; | |||
import Checkbox from '../../components/shared/checkbox'; | |||
import Checkbox from '../../components/controls/Checkbox'; | |||
import QualifierIcon from '../../components/shared/qualifier-icon'; | |||
export default React.createClass({ | |||
@@ -46,8 +46,9 @@ export default React.createClass({ | |||
return ( | |||
<tr key={project.id}> | |||
<td className="thin"> | |||
<Checkbox onCheck={this.onProjectCheck.bind(this, project)} | |||
initiallyChecked={this.isProjectSelected(project)}/> | |||
<Checkbox | |||
checked={this.isProjectSelected(project)} | |||
onCheck={this.onProjectCheck.bind(this, project)}/> | |||
</td> | |||
<td className="thin"> | |||
<QualifierIcon qualifier={project.qualifier}/> |
@@ -21,8 +21,8 @@ import _ from 'underscore'; | |||
import React from 'react'; | |||
import { TYPE, QUALIFIERS_ORDER } from './constants'; | |||
import DeleteView from './delete-view'; | |||
import RadioToggle from '../../components/shared/radio-toggle'; | |||
import Checkbox from '../../components/shared/checkbox'; | |||
import RadioToggle from '../../components/controls/RadioToggle'; | |||
import Checkbox from '../../components/controls/Checkbox'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default React.createClass({ | |||
@@ -63,8 +63,13 @@ export default React.createClass({ | |||
const thirdState = this.props.projects.length > 0 && | |||
this.props.selection.length > 0 && | |||
this.props.selection.length < this.props.projects.length; | |||
const isChecked = isAllChecked || thirdState; | |||
return <Checkbox onCheck={this.onCheck} initiallyChecked={isChecked} thirdState={thirdState}/>; | |||
const checked = isAllChecked || thirdState; | |||
return ( | |||
<Checkbox | |||
checked={checked} | |||
thirdState={thirdState} | |||
onCheck={this.onCheck}/> | |||
); | |||
}, | |||
renderSpinner() { | |||
@@ -99,8 +104,11 @@ export default React.createClass({ | |||
} | |||
return ( | |||
<td className="thin nowrap text-middle"> | |||
<RadioToggle options={this.getQualifierOptions()} value={this.props.qualifiers} | |||
name="projects-qualifier" onCheck={this.props.onQualifierChanged}/> | |||
<RadioToggle | |||
options={this.getQualifierOptions()} | |||
value={this.props.qualifiers} | |||
name="projects-qualifier" | |||
onCheck={this.props.onQualifierChanged}/> | |||
</td> | |||
); | |||
}, | |||
@@ -117,8 +125,11 @@ export default React.createClass({ | |||
</td> | |||
{this.renderQualifierFilter()} | |||
<td className="thin nowrap text-middle"> | |||
<RadioToggle options={this.getTypeOptions()} value={this.props.type} | |||
name="projects-type" onCheck={this.props.onTypeChanged}/> | |||
<RadioToggle | |||
options={this.getTypeOptions()} | |||
value={this.props.type} | |||
name="projects-type" | |||
onCheck={this.props.onTypeChanged}/> | |||
</td> | |||
<td className="text-middle"> | |||
<form onSubmit={this.onSubmit} className="search-box"> |
@@ -21,7 +21,7 @@ import React, { Component } from 'react'; | |||
import Select from 'react-select'; | |||
import DeleteConditionView from '../views/gate-conditions-delete-view'; | |||
import Checkbox from '../../../components/shared/checkbox'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
import { createCondition, updateCondition } from '../../../api/quality-gates'; | |||
import { translate, getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
@@ -173,7 +173,7 @@ export default class Condition extends Component { | |||
<td className="thin text-middle nowrap"> | |||
{(edit && !isDiffMetric) ? ( | |||
<Checkbox | |||
initiallyChecked={isLeakSelected} | |||
checked={isLeakSelected} | |||
onCheck={this.handlePeriodChange.bind(this)}/> | |||
) : this.renderPeriodValue()} | |||
</td> |
@@ -36,7 +36,7 @@ export default Marionette.LayoutView.extend({ | |||
}, | |||
modelEvents: { | |||
'change': 'onChange', | |||
'change': 'handleChange', | |||
'flashChangelog': 'flashChangelog' | |||
}, | |||
@@ -32,7 +32,7 @@ export default Marionette.ItemView.extend({ | |||
}, | |||
events: { | |||
'click': 'onClick' | |||
'click': 'handleClick' | |||
}, | |||
onRender () { |
@@ -20,7 +20,7 @@ | |||
import _ from 'underscore'; | |||
import React from 'react'; | |||
import Checkbox from '../../../components/shared/checkbox'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -60,7 +60,7 @@ export default class Search extends React.Component { | |||
<TooltipsContainer> | |||
<div className="big-spacer-top"> | |||
<Checkbox | |||
initiallyChecked={showInternal} | |||
checked={showInternal} | |||
onCheck={onToggleInternal}/> | |||
{' '} | |||
<span | |||
@@ -79,7 +79,7 @@ export default class Search extends React.Component { | |||
<TooltipsContainer> | |||
<div className="spacer-top"> | |||
<Checkbox | |||
initiallyChecked={showOnlyDeprecated} | |||
checked={showOnlyDeprecated} | |||
onCheck={onToggleDeprecated}/> | |||
{' '} | |||
<span |
@@ -18,39 +18,39 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
export default React.createClass({ | |||
propTypes: { | |||
export default class Checkbox extends React.Component { | |||
static propTypes = { | |||
onCheck: React.PropTypes.func.isRequired, | |||
initiallyChecked: React.PropTypes.bool, | |||
checked: React.PropTypes.bool.isRequired, | |||
thirdState: React.PropTypes.bool | |||
}, | |||
}; | |||
getInitialState() { | |||
return { checked: this.props.initiallyChecked || false }; | |||
}, | |||
static defaultProps = { | |||
thirdState: false | |||
}; | |||
componentWillReceiveProps(nextProps) { | |||
if (nextProps.initiallyChecked != null) { | |||
this.setState({ checked: nextProps.initiallyChecked }); | |||
} | |||
}, | |||
componentWillMount () { | |||
this.handleClick = this.handleClick.bind(this); | |||
} | |||
toggle(e) { | |||
handleClick (e) { | |||
e.preventDefault(); | |||
this.props.onCheck(!this.state.checked); | |||
this.setState({ checked: !this.state.checked }); | |||
}, | |||
e.target.blur(); | |||
this.props.onCheck(!this.props.checked); | |||
} | |||
render () { | |||
const className = classNames('icon-checkbox', { | |||
'icon-checkbox-checked': this.props.checked, | |||
'icon-checkbox-single': this.props.thirdState | |||
}); | |||
render() { | |||
const classNames = ['icon-checkbox']; | |||
if (this.state.checked) { | |||
classNames.push('icon-checkbox-checked'); | |||
} | |||
if (this.props.thirdState) { | |||
classNames.push('icon-checkbox-single'); | |||
} | |||
const className = classNames.join(' '); | |||
return <a onClick={this.toggle} className={className} href="#"/>; | |||
return ( | |||
<a className={className} | |||
href="#" | |||
onClick={this.handleClick}/> | |||
); | |||
} | |||
}); | |||
} |
@@ -18,25 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import StatusIcon from './status-icon'; | |||
import { translate } from '../../helpers/l10n'; | |||
import FavoriteBase from './FavoriteBase'; | |||
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 | |||
}; | |||
export default React.createClass({ | |||
render () { | |||
if (!this.props.status) { | |||
return null; | |||
} | |||
let resolution; | |||
if (this.props.resolution) { | |||
resolution = ' (' + translate('issue.resolution', this.props.resolution) + ')'; | |||
} | |||
return ( | |||
<span> | |||
<StatusIcon status={this.props.status}/> | |||
| |||
{translate('issue.status', this.props.status)} | |||
{resolution} | |||
</span> | |||
<FavoriteBase | |||
favorite={this.props.favorite} | |||
addFavorite={() => addFavorite(this.props.component)} | |||
removeFavorite={() => removeFavorite(this.props.component)}/> | |||
); | |||
} | |||
}); | |||
} |
@@ -0,0 +1,92 @@ | |||
/* | |||
* 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 FavoriteBase extends React.Component { | |||
static propTypes = { | |||
favorite: React.PropTypes.bool.isRequired, | |||
addFavorite: React.PropTypes.func.isRequired, | |||
removeFavorite: React.PropTypes.func.isRequired | |||
}; | |||
constructor (props) { | |||
super(props); | |||
this.state = { favorite: this.props.favorite }; | |||
} | |||
componentWillMount () { | |||
this.mounted = true; | |||
this.toggleFavorite = this.toggleFavorite.bind(this); | |||
} | |||
componentWillUnmount () { | |||
this.mounted = false; | |||
} | |||
toggleFavorite (e) { | |||
e.preventDefault(); | |||
if (this.state.favorite) { | |||
this.removeFavorite(); | |||
} else { | |||
this.addFavorite(); | |||
} | |||
} | |||
addFavorite () { | |||
this.props.addFavorite().then(() => { | |||
if (this.mounted) { | |||
this.setState({ favorite: true }); | |||
} | |||
}); | |||
} | |||
removeFavorite () { | |||
this.props.removeFavorite().then(() => { | |||
if (this.mounted) { | |||
this.setState({ favorite: false }); | |||
} | |||
}); | |||
} | |||
renderSVG () { | |||
/* jscs:disable maximumLineLength */ | |||
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.state.favorite | |||
}); | |||
return ( | |||
<a className={className} | |||
href="#" | |||
onClick={this.toggleFavorite}> | |||
{this.renderSVG()} | |||
</a> | |||
); | |||
} | |||
} |
@@ -18,14 +18,23 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import Avatar from './avatar'; | |||
import { translate } from '../../helpers/l10n'; | |||
import FavoriteBase from './FavoriteBase'; | |||
import { toggleIssueFilter } from '../../api/issue-filters'; | |||
export default class FavoriteIssueFilter extends React.Component { | |||
static propTypes = { | |||
favorite: React.PropTypes.bool.isRequired, | |||
filter: React.PropTypes.shape({ | |||
id: React.PropTypes.string.isRequired | |||
}).isRequired | |||
}; | |||
export default class Assignee extends React.Component { | |||
render () { | |||
const avatar = this.props.user ? | |||
<span className="spacer-right"><Avatar email={this.props.user.email} size={16}/></span> : null; | |||
const name = this.props.user ? this.props.user.name : translate('unassigned'); | |||
return <span>{avatar}{name}</span>; | |||
return ( | |||
<FavoriteBase | |||
favorite={this.props.favorite} | |||
addFavorite={() => toggleIssueFilter(this.props.filter.id)} | |||
removeFavorite={() => toggleIssueFilter(this.props.filter.id)}/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* 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 FavoriteBase from './FavoriteBase'; | |||
import { toggleMeasureFilter } from '../../api/measure-filters'; | |||
export default class FavoriteMeasureFilter extends React.Component { | |||
static propTypes = { | |||
favorite: React.PropTypes.bool.isRequired, | |||
filter: React.PropTypes.shape({ | |||
id: React.PropTypes.number.isRequired | |||
}).isRequired | |||
}; | |||
render () { | |||
return ( | |||
<FavoriteBase | |||
favorite={this.props.favorite} | |||
addFavorite={() => toggleMeasureFilter(this.props.filter.id)} | |||
removeFavorite={() => toggleMeasureFilter(this.props.filter.id)}/> | |||
); | |||
} | |||
} |
@@ -19,48 +19,56 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
export default React.createClass({ | |||
propTypes: { | |||
export default class ListFooter extends React.Component { | |||
static propTypes = { | |||
count: React.PropTypes.number.isRequired, | |||
total: React.PropTypes.number.isRequired, | |||
loadMore: React.PropTypes.func, | |||
ready: React.PropTypes.bool | |||
}, | |||
}; | |||
getDefaultProps() { | |||
return { ready: true }; | |||
}, | |||
static defaultProps = { | |||
ready: true | |||
}; | |||
canLoadMore() { | |||
componentWillMount () { | |||
this.handleLoadMore = this.handleLoadMore.bind(this); | |||
} | |||
canLoadMore () { | |||
return typeof this.props.loadMore === 'function'; | |||
}, | |||
} | |||
handleLoadMore(e) { | |||
handleLoadMore (e) { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
if (this.canLoadMore()) { | |||
this.props.loadMore(); | |||
} | |||
}, | |||
renderLoading() { | |||
return <footer className="spacer-top note text-center"> | |||
{translate('loading')} | |||
</footer>; | |||
}, | |||
this.props.loadMore(); | |||
} | |||
render() { | |||
render () { | |||
const hasMore = this.props.total > this.props.count; | |||
const loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>; | |||
const className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready }); | |||
const loadMoreLink = ( | |||
<a className="spacer-left" href="#" onClick={this.handleLoadMore}> | |||
{translate('show_more')} | |||
</a> | |||
); | |||
const className = classNames('spacer-top note text-center', { | |||
'new-loading': !this.props.ready | |||
}); | |||
return ( | |||
<footer className={className}> | |||
{formatMeasure(this.props.count, 'INT')} of {formatMeasure(this.props.total, 'INT')} shown | |||
{translateWithParameters( | |||
'x_of_y_shown', | |||
formatMeasure(this.props.count, 'INT'), | |||
formatMeasure(this.props.total, 'INT') | |||
)} | |||
{this.canLoadMore() && hasMore ? loadMoreLink : null} | |||
</footer> | |||
); | |||
} | |||
}); | |||
} |
@@ -19,44 +19,55 @@ | |||
*/ | |||
import React from 'react'; | |||
export default React.createClass({ | |||
propTypes: { | |||
export default class RadioToggle extends React.Component { | |||
static propTypes = { | |||
value: React.PropTypes.string, | |||
options: React.PropTypes.array.isRequired, | |||
options: React.PropTypes.arrayOf(React.PropTypes.shape({ | |||
value: React.PropTypes.string.isRequired, | |||
label: React.PropTypes.string.isRequired | |||
})).isRequired, | |||
name: React.PropTypes.string.isRequired, | |||
onCheck: React.PropTypes.func.isRequired | |||
}, | |||
}; | |||
getDefaultProps () { | |||
return { disabled: false, value: null }; | |||
}, | |||
static defaultProps = { | |||
disabled: false, | |||
value: null | |||
}; | |||
onChange(e) { | |||
componentWillMount () { | |||
this.renderOption = this.renderOption.bind(this); | |||
this.handleChange = this.handleChange.bind(this); | |||
} | |||
handleChange (e) { | |||
const newValue = e.currentTarget.value; | |||
this.props.onCheck(newValue); | |||
}, | |||
} | |||
renderOption(option) { | |||
renderOption (option) { | |||
const checked = option.value === this.props.value; | |||
const htmlId = this.props.name + '__' + option.value; | |||
return ( | |||
<li key={option.value}> | |||
<input onChange={this.onChange} | |||
type="radio" | |||
name={this.props.name} | |||
value={option.value} | |||
id={htmlId} | |||
checked={checked} | |||
disabled={this.props.disabled}/> | |||
<input | |||
type="radio" | |||
name={this.props.name} | |||
value={option.value} | |||
id={htmlId} | |||
checked={checked} | |||
onChange={this.handleChange}/> | |||
<label htmlFor={htmlId}>{option.label}</label> | |||
</li> | |||
); | |||
}, | |||
} | |||
render() { | |||
const options = this.props.options.map(this.renderOption); | |||
render () { | |||
return ( | |||
<ul className="radio-toggle">{options}</ul> | |||
<ul className="radio-toggle"> | |||
{this.props.options.map(this.renderOption)} | |||
</ul> | |||
); | |||
} | |||
}); | |||
} |
@@ -0,0 +1,75 @@ | |||
/* | |||
* 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 chai, { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import sinon from 'sinon'; | |||
import sinonChai from 'sinon-chai'; | |||
import React from 'react'; | |||
import Checkbox from '../Checkbox'; | |||
chai.use(sinonChai); | |||
function click (element) { | |||
return element.simulate('click', { | |||
target: { blur () {} }, | |||
preventDefault () {} | |||
}); | |||
} | |||
describe('Components :: Controls :: Checkbox', () => { | |||
it('should render unchecked', () => { | |||
const checkbox = shallow( | |||
<Checkbox checked={false} onCheck={() => true}/> | |||
); | |||
expect(checkbox.is('.icon-checkbox-checked')).to.equal(false); | |||
}); | |||
it('should render checked', () => { | |||
const checkbox = shallow( | |||
<Checkbox checked={true} onCheck={() => true}/> | |||
); | |||
expect(checkbox.is('.icon-checkbox-checked')).to.equal(true); | |||
}); | |||
it('should render unchecked third state', () => { | |||
const checkbox = shallow( | |||
<Checkbox checked={false} thirdState={true} onCheck={() => true}/> | |||
); | |||
expect(checkbox.is('.icon-checkbox-single')).to.equal(true); | |||
expect(checkbox.is('.icon-checkbox-checked')).to.equal(false); | |||
}); | |||
it('should render checked third state', () => { | |||
const checkbox = shallow( | |||
<Checkbox checked={true} thirdState={true} onCheck={() => true}/> | |||
); | |||
expect(checkbox.is('.icon-checkbox-single')).to.equal(true); | |||
expect(checkbox.is('.icon-checkbox-checked')).to.equal(true); | |||
}); | |||
it('should call onCheck', () => { | |||
const onCheck = sinon.spy(); | |||
const checkbox = shallow( | |||
<Checkbox checked={false} onCheck={onCheck}/> | |||
); | |||
click(checkbox); | |||
expect(onCheck).to.have.been.calledWith(true); | |||
}); | |||
}); |
@@ -0,0 +1,70 @@ | |||
/* | |||
* 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 chai, { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import sinon from 'sinon'; | |||
import sinonChai from 'sinon-chai'; | |||
import React from 'react'; | |||
import FavoriteBase from '../FavoriteBase'; | |||
chai.use(sinonChai); | |||
function click (element) { | |||
return element.simulate('click', { | |||
target: { blur () {} }, | |||
preventDefault () {} | |||
}); | |||
} | |||
function renderFavoriteBase (props) { | |||
return shallow( | |||
<FavoriteBase | |||
favorite={true} | |||
addFavorite={sinon.stub().throws()} | |||
removeFavorite={sinon.stub().throws()} | |||
{...props}/> | |||
); | |||
} | |||
describe('Components :: Controls :: FavoriteBase', () => { | |||
it('should render favorite', () => { | |||
const favorite = renderFavoriteBase({ favorite: true }); | |||
expect(favorite.is('.icon-star-favorite')).to.equal(true); | |||
}); | |||
it('should render not favorite', () => { | |||
const favorite = renderFavoriteBase({ favorite: false }); | |||
expect(favorite.is('.icon-star-favorite')).to.equal(false); | |||
}); | |||
it('should add favorite', () => { | |||
const addFavorite = sinon.stub().returns(Promise.resolve()); | |||
const favorite = renderFavoriteBase({ favorite: false, addFavorite }); | |||
click(favorite.find('a')); | |||
expect(addFavorite).to.have.been.called; | |||
}); | |||
it('should remove favorite', () => { | |||
const removeFavorite = sinon.stub().returns(Promise.resolve()); | |||
const favorite = renderFavoriteBase({ favorite: true, removeFavorite }); | |||
click(favorite.find('a')); | |||
expect(removeFavorite).to.have.been.called; | |||
}); | |||
}); |
@@ -0,0 +1,61 @@ | |||
/* | |||
* 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 chai, { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import sinon from 'sinon'; | |||
import sinonChai from 'sinon-chai'; | |||
import React from 'react'; | |||
import ListFooter from '../ListFooter'; | |||
chai.use(sinonChai); | |||
function click (element) { | |||
return element.simulate('click', { | |||
target: { blur () {} }, | |||
preventDefault () {} | |||
}); | |||
} | |||
describe('Components :: Controls :: ListFooter', () => { | |||
it('should render "3 of 5 shown"', () => { | |||
const listFooter = shallow( | |||
<ListFooter count={3} total={5}/> | |||
); | |||
expect(listFooter.text()).to.contain('x_of_y_shown.3.5'); | |||
}); | |||
it('should not render "show more"', () => { | |||
const listFooter = shallow( | |||
<ListFooter count={3} total={5}/> | |||
); | |||
expect(listFooter.find('a')).to.have.length(0); | |||
}); | |||
it('should "show more"', () => { | |||
const loadMore = sinon.spy(); | |||
const listFooter = shallow( | |||
<ListFooter count={3} total={5} loadMore={loadMore}/> | |||
); | |||
const link = listFooter.find('a'); | |||
expect(link).to.have.length(1); | |||
click(link); | |||
expect(loadMore).to.have.been.called; | |||
}); | |||
}); |
@@ -0,0 +1,60 @@ | |||
/* | |||
* 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 chai, { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import sinon from 'sinon'; | |||
import sinonChai from 'sinon-chai'; | |||
import React from 'react'; | |||
import RadioToggle from '../RadioToggle'; | |||
chai.use(sinonChai); | |||
function getSample (props) { | |||
const options = [ | |||
{ value: 'one', label: 'first' }, | |||
{ value: 'two', label: 'second' } | |||
]; | |||
return ( | |||
<RadioToggle | |||
options={options} | |||
name="sample" | |||
onCheck={() => true} | |||
{...props}/> | |||
); | |||
} | |||
function change (element, value) { | |||
return element.simulate('change', { currentTarget: { value } }); | |||
} | |||
describe('Components :: Controls :: RadioToggle', () => { | |||
it('should render', () => { | |||
const radioToggle = shallow(getSample()); | |||
expect(radioToggle.find('input[type="radio"]')).to.have.length(2); | |||
expect(radioToggle.find('label')).to.have.length(2); | |||
}); | |||
it('should call onCheck', () => { | |||
const onCheck = sinon.spy(); | |||
const radioToggle = shallow(getSample({ onCheck })); | |||
change(radioToggle.find('input[value="two"]'), 'two'); | |||
expect(onCheck).to.have.been.calledWith('two'); | |||
}); | |||
}); |
@@ -33,7 +33,7 @@ export default PopupView.extend({ | |||
}, | |||
events: { | |||
'click': 'onClick', | |||
'click': 'handleClick', | |||
'keydown @ui.textarea': 'onKeydown', | |||
'keyup @ui.textarea': 'toggleSubmit', | |||
'click @ui.cancelButton': 'cancel', |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import _ from 'underscore'; | |||
import React from 'react'; | |||
import RadioToggle from '../shared/radio-toggle'; | |||
import RadioToggle from '../controls/RadioToggle'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default React.createClass({ | |||
@@ -46,8 +46,6 @@ export default React.createClass({ | |||
}, | |||
render() { | |||
const selectionDisabled = !!this.props.query; | |||
const selectionOptions = [ | |||
{ value: 'selected', label: 'Selected' }, | |||
{ value: 'deselected', label: 'Not Selected' }, | |||
@@ -61,8 +59,7 @@ export default React.createClass({ | |||
name="select-list-selection" | |||
options={selectionOptions} | |||
onCheck={this.onCheck} | |||
value={this.props.selection} | |||
disabled={selectionDisabled}/> | |||
value={this.props.selection}/> | |||
</div> | |||
<div className="pull-right"> | |||
<input |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import Checkbox from '../shared/checkbox'; | |||
import Checkbox from '../controls/Checkbox'; | |||
export default React.createClass({ | |||
propTypes: { | |||
@@ -41,7 +41,9 @@ export default React.createClass({ | |||
return ( | |||
<li className="panel panel-vertical"> | |||
<div className="display-inline-block text-middle spacer-right"> | |||
<Checkbox onCheck={this.onCheck} initiallyChecked={!!this.props.item.selected}/> | |||
<Checkbox | |||
checked={!!this.props.item.selected} | |||
onCheck={this.onCheck}/> | |||
</div> | |||
<div className="display-inline-block text-middle" dangerouslySetInnerHTML={{ __html: renderedItem }}/> | |||
</li> |
@@ -1,65 +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 React from 'react'; | |||
import { toggleIssueFilter } from '../../api/issue-filters'; | |||
export default React.createClass({ | |||
propTypes: { | |||
filter: React.PropTypes.object.isRequired, | |||
favorite: React.PropTypes.bool.isRequired | |||
}, | |||
getInitialState() { | |||
return { favorite: this.props.favorite }; | |||
}, | |||
toggleFavorite(e) { | |||
e.preventDefault(); | |||
if (this.state.favorite) { | |||
this.removeFavorite(); | |||
} else { | |||
this.addFavorite(); | |||
} | |||
}, | |||
addFavorite() { | |||
toggleIssueFilter(this.props.filter.id).then(() => this.setState({ favorite: true })); | |||
}, | |||
removeFavorite() { | |||
toggleIssueFilter(this.props.filter.id).then(() => this.setState({ favorite: false })); | |||
}, | |||
renderSVG() { | |||
/* jscs:disable maximumLineLength */ | |||
return ( | |||
<svg width="16" height="16" style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 1.41421 }}> | |||
<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" | |||
style={{ fillRule: 'nonzero' }}/> | |||
</svg> | |||
); | |||
}, | |||
render() { | |||
const className = this.state.favorite ? 'icon-star icon-star-favorite' : 'icon-star'; | |||
return <a onClick={this.toggleFavorite} className={className} href="#">{this.renderSVG()}</a>; | |||
} | |||
}); |
@@ -1,65 +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 React from 'react'; | |||
import { toggleMeasureFilter } from '../../api/measure-filters'; | |||
export default React.createClass({ | |||
propTypes: { | |||
filter: React.PropTypes.object.isRequired, | |||
favorite: React.PropTypes.bool.isRequired | |||
}, | |||
getInitialState() { | |||
return { favorite: this.props.favorite }; | |||
}, | |||
toggleFavorite(e) { | |||
e.preventDefault(); | |||
if (this.state.favorite) { | |||
this.removeFavorite(); | |||
} else { | |||
this.addFavorite(); | |||
} | |||
}, | |||
addFavorite() { | |||
toggleMeasureFilter(this.props.filter.id).then(() => this.setState({ favorite: true })); | |||
}, | |||
removeFavorite() { | |||
toggleMeasureFilter(this.props.filter.id).then(() => this.setState({ favorite: false })); | |||
}, | |||
renderSVG() { | |||
/* jscs:disable maximumLineLength */ | |||
return ( | |||
<svg width="16" height="16" style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 1.41421 }}> | |||
<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" | |||
style={{ fillRule: 'nonzero' }}/> | |||
</svg> | |||
); | |||
}, | |||
render() { | |||
const className = this.state.favorite ? 'icon-star icon-star-favorite' : 'icon-star'; | |||
return <a onClick={this.toggleFavorite} className={className} href="#">{this.renderSVG()}</a>; | |||
} | |||
}); |
@@ -20,9 +20,10 @@ | |||
import _ from 'underscore'; | |||
import moment from 'moment'; | |||
import React from 'react'; | |||
import { IssuesLink } from './issues-link'; | |||
import { getComponentDrilldownUrl } from '../../helpers/urls'; | |||
import { | |||
getComponentDrilldownUrl, | |||
getComponentIssuesUrl | |||
} from '../../helpers/urls'; | |||
const ISSUE_MEASURES = [ | |||
'violations', | |||
@@ -113,9 +114,13 @@ export const DrilldownLink = React.createClass({ | |||
}, | |||
renderIssuesLink() { | |||
return <IssuesLink component={this.props.component} params={this.propsToIssueParams()}> | |||
{this.props.children} | |||
</IssuesLink>; | |||
const url = getComponentIssuesUrl( | |||
this.props.component, | |||
this.propsToIssueParams()); | |||
return ( | |||
<a href={url}>{this.props.children}</a> | |||
); | |||
}, | |||
render() { |
@@ -1,67 +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 $ from 'jquery'; | |||
import React from 'react'; | |||
export default React.createClass({ | |||
propTypes: { | |||
component: React.PropTypes.string.isRequired, | |||
favorite: React.PropTypes.bool.isRequired | |||
}, | |||
getInitialState() { | |||
return { favorite: this.props.favorite }; | |||
}, | |||
toggleFavorite(e) { | |||
e.preventDefault(); | |||
if (this.state.favorite) { | |||
this.removeFavorite(); | |||
} else { | |||
this.addFavorite(); | |||
} | |||
}, | |||
addFavorite() { | |||
const url = window.baseUrl + '/api/favourites'; | |||
const data = { key: this.props.component }; | |||
$.ajax({ type: 'POST', url, data }).done(() => this.setState({ favorite: true })); | |||
}, | |||
removeFavorite() { | |||
const url = `${window.baseUrl}/api/favourites/${encodeURIComponent(this.props.component)}`; | |||
$.ajax({ type: 'DELETE', url }).done(() => this.setState({ favorite: false })); | |||
}, | |||
renderSVG() { | |||
/* jscs:disable maximumLineLength */ | |||
return ( | |||
<svg width="16" height="16" style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 1.41421 }}> | |||
<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" | |||
style={{ fillRule: 'nonzero' }}/> | |||
</svg> | |||
); | |||
}, | |||
render() { | |||
const className = this.state.favorite ? 'icon-star icon-star-favorite' : 'icon-star'; | |||
return <a onClick={this.toggleFavorite} className={className} href="#">{this.renderSVG()}</a>; | |||
} | |||
}); |
@@ -1,30 +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 React from 'react'; | |||
export default React.createClass({ | |||
render() { | |||
if (!this.props.status) { | |||
return null; | |||
} | |||
const className = 'icon-status-' + this.props.status.toLowerCase(); | |||
return <i className={className}></i>; | |||
} | |||
}); |
@@ -24,7 +24,7 @@ export default Popup.extend({ | |||
template: Template, | |||
events: { | |||
'click': 'onClick' | |||
'click': 'handleClick' | |||
}, | |||
onRender () { |
@@ -20,25 +20,29 @@ | |||
import React from 'react'; | |||
import md5 from 'blueimp-md5'; | |||
export default React.createClass({ | |||
propTypes: { | |||
export default class Avatar extends React.Component { | |||
static propTypes = { | |||
email: React.PropTypes.string, | |||
size: React.PropTypes.number.isRequired | |||
}, | |||
}; | |||
render() { | |||
render () { | |||
const shouldShowAvatar = window.SS && window.SS.lf && window.SS.lf.enableGravatar; | |||
if (!shouldShowAvatar) { | |||
return null; | |||
} | |||
const emailHash = md5.md5(this.props.email || '').trim(); | |||
const url = ('' + window.SS.lf.gravatarServerUrl) | |||
.replace('{EMAIL_MD5}', emailHash) | |||
.replace('{SIZE}', this.props.size * 2); | |||
return <img className="rounded" | |||
src={url} | |||
width={this.props.size} | |||
height={this.props.size} | |||
alt={this.props.email}/>; | |||
.replace('{EMAIL_MD5}', emailHash) | |||
.replace('{SIZE}', this.props.size * 2); | |||
return ( | |||
<img className="rounded" | |||
src={url} | |||
width={this.props.size} | |||
height={this.props.size} | |||
alt={this.props.email}/> | |||
); | |||
} | |||
}); | |||
} |
@@ -18,16 +18,16 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
export const Rating = React.createClass({ | |||
render() { | |||
if (this.props.value == null || isNaN(this.props.value)) { | |||
return null; | |||
} | |||
const formatted = formatMeasure(this.props.value, 'RATING'); | |||
const className = 'rating rating-' + formatted; | |||
export default class Level extends React.Component { | |||
static propTypes = { | |||
level: React.PropTypes.oneOf(['ERROR', 'WARN', 'OK']).isRequired | |||
}; | |||
render () { | |||
const formatted = formatMeasure(this.props.level, 'LEVEL'); | |||
const className = 'level level-' + this.props.level; | |||
return <span className={className}>{formatted}</span>; | |||
} | |||
}); | |||
} |
@@ -18,17 +18,23 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
const Level = ({ level }) => { | |||
const formatted = formatMeasure(level, 'LEVEL'); | |||
const className = 'level level-' + level; | |||
return ( | |||
<span className={className}> | |||
{formatted} | |||
</span> | |||
); | |||
}; | |||
export default class Rating extends React.Component { | |||
static propTypes = { | |||
value: (props, propName, componentName) => { | |||
// allow both numbers and strings | |||
const numberValue = Number(props[propName]); | |||
if (numberValue < 1 || numberValue > 5) { | |||
throw new Error( | |||
`Invalid prop "${propName}" passed to "${componentName}".`); | |||
} | |||
} | |||
}; | |||
export default Level; | |||
render () { | |||
const formatted = formatMeasure(this.props.value, 'RATING'); | |||
const className = 'rating rating-' + formatted; | |||
return <span className={className}>{formatted}</span>; | |||
} | |||
} |
@@ -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 { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import Avatar from '../Avatar'; | |||
describe('Components :: UI :: Avatar', () => { | |||
beforeEach(() => { | |||
window.SS = { | |||
lf: { | |||
enableGravatar: true, | |||
gravatarServerUrl: 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}' | |||
} | |||
}; | |||
}); | |||
afterEach(() => { | |||
window.SS = undefined; | |||
}); | |||
it('should render', () => { | |||
const avatar = shallow( | |||
<Avatar email="mail@example.com" size={20}/> | |||
); | |||
expect(avatar.is('img')).to.equal(true); | |||
expect(avatar.prop('width')).to.equal(20); | |||
expect(avatar.prop('height')).to.equal(20); | |||
expect(avatar.prop('alt')).to.equal('mail@example.com'); | |||
expect(avatar.prop('src')).to.equal( | |||
'http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=40'); | |||
}); | |||
it('should not render', () => { | |||
window.SS.lf.enableGravatar = false; | |||
const avatar = shallow( | |||
<Avatar email="mail@example.com" size={20}/> | |||
); | |||
expect(avatar.is('img')).to.equal(false); | |||
}); | |||
}); |
@@ -17,11 +17,16 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import Level from '../Level'; | |||
export const QualityGateLink = React.createClass({ | |||
render() { | |||
const url = `${window.baseUrl}/quality_gates/show/${this.props.gate}`; | |||
return <a href={url}>{this.props.children}</a>; | |||
} | |||
describe('Components :: UI :: Level', () => { | |||
it('should render', () => { | |||
const rating = shallow( | |||
<Level level="ERROR"/> | |||
); | |||
expect(rating.is('.level-ERROR')).to.equal(true); | |||
}); | |||
}); |
@@ -17,11 +17,23 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { expect } from 'chai'; | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import Rating from '../Rating'; | |||
export const QualityProfileLink = React.createClass({ | |||
render() { | |||
const url = `${window.baseUrl}/profiles/show?key=${encodeURIComponent(this.props.profile)}`; | |||
return <a href={url}>{this.props.children}</a>; | |||
} | |||
describe('Components :: UI :: Rating', () => { | |||
it('should render with numeric value', () => { | |||
const rating = shallow( | |||
<Rating value={2}/> | |||
); | |||
expect(rating.is('.rating-B')).to.equal(true); | |||
}); | |||
it('should render with string value', () => { | |||
const rating = shallow( | |||
<Rating value="2.0"/> | |||
); | |||
expect(rating.is('.rating-B')).to.equal(true); | |||
}); | |||
}); |
@@ -32,7 +32,7 @@ export default Marionette.ItemView.extend({ | |||
}, | |||
events: { | |||
'click': 'onClick', | |||
'click': 'handleClick', | |||
'click .js-close': 'onCloseClick' | |||
}, | |||
@@ -156,6 +156,19 @@ export function post (url, data) { | |||
.then(checkStatus); | |||
} | |||
/** | |||
* Shortcut to do a POST request and return response json | |||
* @param url | |||
* @param data | |||
*/ | |||
export function requestDelete (url, data) { | |||
return request(url) | |||
.setMethod('DELETE') | |||
.setData(data) | |||
.submit() | |||
.then(checkStatus); | |||
} | |||
/** | |||
* Delay promise for testing purposes | |||
* @param response |
@@ -83,3 +83,21 @@ export function getComponentFixedDashboardUrl (componentKey, dashboardKey) { | |||
export function getComponentDashboardManagementUrl (componentKey) { | |||
return window.baseUrl + '/dashboards?resource=' + encodeURIComponent(componentKey); | |||
} | |||
/** | |||
* Generate URL for a quality profile | |||
* @param {string} key | |||
* @returns {string} | |||
*/ | |||
export function getQualityProfileUrl (key) { | |||
return window.baseUrl + '/profiles/show?key=' + encodeURIComponent(key); | |||
} | |||
/** | |||
* Generate URL for a quality gate | |||
* @param {string} key | |||
* @returns {string} | |||
*/ | |||
export function getQualityGateUrl (key) { | |||
return window.baseUrl + '/quality_gates/show/' + encodeURIComponent(key); | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import Favorite from '../../../components/shared/favorite'; | |||
import Favorite from '../../../components/controls/Favorite'; | |||
export default React.createClass({ | |||
render() { | |||
@@ -27,7 +27,9 @@ export default React.createClass({ | |||
} | |||
return ( | |||
<div className="navbar-context-favorite"> | |||
<Favorite component={this.props.component} favorite={this.props.favorite}/> | |||
<Favorite | |||
component={this.props.component} | |||
favorite={this.props.favorite}/> | |||
</div> | |||
); | |||
} |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import Avatar from '../../../components/shared/avatar'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import RecentHistory from '../component/RecentHistory'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -149,6 +149,7 @@ severity=Severity | |||
severity_abbreviated=Se. | |||
shared=Shared | |||
show_verb=Show | |||
x_of_y_shown={0} of {1} shown | |||
size=Size | |||
status=Status | |||
status_abbreviated=St. |