@@ -18,7 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import { getJSON, post } from '../helpers/request'; | |||
import { getJSON, post, postJSON } from '../helpers/request'; | |||
export type IssueResponse = { | |||
components?: Array<*>, | |||
issue: {}, | |||
rules?: Array<*>, | |||
users?: Array<*> | |||
}; | |||
type IssuesResponse = { | |||
components?: Array<*>, | |||
@@ -34,6 +41,15 @@ type IssuesResponse = { | |||
users?: Array<*> | |||
}; | |||
export type Transition = | |||
| 'confirm' | |||
| 'unconfirm' | |||
| 'reopen' | |||
| 'resolve' | |||
| 'falsepositive' | |||
| 'wontfix' | |||
| 'close'; | |||
export const searchIssues = (query: {}): Promise<IssuesResponse> => | |||
getJSON('/api/issues/search', query); | |||
@@ -83,11 +99,60 @@ export function getIssuesCount(query: {}): Promise<*> { | |||
export const searchIssueTags = (ps: number = 500) => getJSON('/api/issues/tags', { ps }); | |||
export function getIssueChangelog(issue: string): Promise<*> { | |||
const url = '/api/issues/changelog'; | |||
return getJSON(url, { issue }).then(r => r.changelog); | |||
} | |||
export function getIssueFilters() { | |||
const url = '/api/issue_filters/search'; | |||
return getJSON(url).then(r => r.issueFilters); | |||
} | |||
export function addIssueComment(data: { issue: string, text: string }): Promise<IssueResponse> { | |||
const url = '/api/issues/add_comment'; | |||
return postJSON(url, data); | |||
} | |||
export function deleteIssueComment(data: { comment: string }): Promise<IssueResponse> { | |||
const url = '/api/issues/delete_comment'; | |||
return postJSON(url, data); | |||
} | |||
export function editIssueComment(data: { comment: string, text: string }): Promise<IssueResponse> { | |||
const url = '/api/issues/edit_comment'; | |||
return postJSON(url, data); | |||
} | |||
export function setIssueAssignee( | |||
data: { issue: string, assignee?: string } | |||
): Promise<IssueResponse> { | |||
const url = '/api/issues/assign'; | |||
return postJSON(url, data); | |||
} | |||
export function setIssueSeverity(data: { issue: string, severity: string }): Promise<*> { | |||
const url = '/api/issues/set_severity'; | |||
return postJSON(url, data); | |||
} | |||
export function setIssueTags(data: { issue: string, tags: string }): Promise<IssueResponse> { | |||
const url = '/api/issues/set_tags'; | |||
return postJSON(url, data); | |||
} | |||
export function setIssueTransition( | |||
data: { issue: string, transition: Transition } | |||
): Promise<IssueResponse> { | |||
const url = '/api/issues/do_transition'; | |||
return postJSON(url, data); | |||
} | |||
export function setIssueType(data: { issue: string, type: string }): Promise<IssueResponse> { | |||
const url = '/api/issues/set_type'; | |||
return postJSON(url, data); | |||
} | |||
export const bulkChangeIssues = (issueKeys: Array<string>, query: {}) => | |||
post('/api/issues/bulk_change', { | |||
issues: issueKeys.join(), |
@@ -0,0 +1,94 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { orderBy, uniq, without } from 'lodash'; | |||
import FacetBox from './components/FacetBox'; | |||
import FacetHeader from './components/FacetHeader'; | |||
import FacetItem from './components/FacetItem'; | |||
import FacetItemsList from './components/FacetItemsList'; | |||
import SeverityHelper from '../../../components/shared/SeverityHelper'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = {| | |||
onChange: (changes: { [string]: Array<string> }) => void, | |||
onToggle: (property: string) => void, | |||
open: boolean, | |||
severities: Array<string>, | |||
stats?: { [string]: number } | |||
|}; | |||
export default class SeverityFacet extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
open: true | |||
}; | |||
property = 'severities'; | |||
handleItemClick = (itemValue: string) => { | |||
const { severities } = this.props; | |||
const newValue = orderBy( | |||
severities.includes(itemValue) | |||
? without(severities, itemValue) | |||
: uniq([...severities, itemValue]) | |||
); | |||
this.props.onChange({ [this.property]: newValue }); | |||
}; | |||
handleHeaderClick = () => { | |||
this.props.onToggle(this.property); | |||
}; | |||
getStat(severity: string): ?number { | |||
const { stats } = this.props; | |||
return stats ? stats[severity] : null; | |||
} | |||
render() { | |||
const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR']; | |||
return ( | |||
<FacetBox property={this.property}> | |||
<FacetHeader | |||
hasValue={this.props.severities.length > 0} | |||
name={translate('issues.facet', this.property)} | |||
onClick={this.handleHeaderClick} | |||
open={this.props.open} | |||
/> | |||
<FacetItemsList open={this.props.open}> | |||
{severities.map(severity => ( | |||
<FacetItem | |||
active={this.props.severities.includes(severity)} | |||
halfWidth={true} | |||
key={severity} | |||
name={<SeverityHelper severity={severity} />} | |||
onClick={this.handleItemClick} | |||
stat={this.getStat(severity)} | |||
value={severity} | |||
/> | |||
))} | |||
</FacetItemsList> | |||
</FacetBox> | |||
); | |||
} | |||
} |
@@ -123,11 +123,7 @@ export default class MetaTags extends React.PureComponent { | |||
} else { | |||
return ( | |||
<div className="overview-meta-card overview-meta-tags"> | |||
<TagsList | |||
tags={tags.length ? tags : [translate('no_tags')]} | |||
allowUpdate={false} | |||
allowMultiLine={true} | |||
/> | |||
<TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={false} /> | |||
</div> | |||
); | |||
} |
@@ -5,7 +5,6 @@ exports[`test should open the tag selector on click 1`] = ` | |||
className="button-link" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
@@ -24,7 +23,6 @@ exports[`test should open the tag selector on click 2`] = ` | |||
className="button-link" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
@@ -59,7 +57,6 @@ exports[`test should open the tag selector on click 3`] = ` | |||
className="button-link" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
@@ -78,7 +75,6 @@ exports[`test should render with tags and admin rights 1`] = ` | |||
className="button-link" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
@@ -94,7 +90,6 @@ exports[`test should render without tags and admin rights 1`] = ` | |||
<div | |||
className="overview-meta-card overview-meta-tags"> | |||
<TagsList | |||
allowMultiLine={true} | |||
allowUpdate={false} | |||
tags={ | |||
Array [ |
@@ -26,7 +26,6 @@ import { searchProjectTags } from '../../../api/components'; | |||
import { setProjectTags } from '../store/actions'; | |||
type Props = { | |||
open: boolean, | |||
position: {}, | |||
project: string, | |||
selectedTags: Array<string>, | |||
@@ -75,7 +74,6 @@ class ProjectTagsSelectorContainer extends React.PureComponent { | |||
render() { | |||
return ( | |||
<TagsSelector | |||
open={this.props.open} | |||
position={this.props.position} | |||
tags={this.state.searchResult} | |||
selectedTags={this.props.selectedTags} |
@@ -60,7 +60,6 @@ exports[`test should display tags 1`] = ` | |||
</Link> | |||
</h2> | |||
<TagsList | |||
allowMultiLine={false} | |||
allowUpdate={false} | |||
customClass="spacer-left" | |||
tags={ |
@@ -19,7 +19,7 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import SeverityHelper from '../../../components/shared/severity-helper'; | |||
import SeverityHelper from '../../../components/shared/SeverityHelper'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SeverityChange from '../SeverityChange'; | |||
import SeverityHelper from '../../../../components/shared/severity-helper'; | |||
import SeverityHelper from '../../../../components/shared/SeverityHelper'; | |||
it('should render SeverityHelper', () => { | |||
const output = shallow(<SeverityChange severity="BLOCKER" />).find(SeverityHelper); |
@@ -21,7 +21,7 @@ | |||
import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import ComparisonEmpty from './ComparisonEmpty'; | |||
import SeverityIcon from '../../../components/shared/severity-icon'; | |||
import SeverityIcon from '../../../components/shared/SeverityIcon'; | |||
import { translateWithParameters } from '../../../helpers/l10n'; | |||
import { getRulesUrl } from '../../../helpers/urls'; | |||
@@ -22,7 +22,7 @@ import React from 'react'; | |||
import { Link } from 'react-router'; | |||
import ComparisonResults from '../ComparisonResults'; | |||
import ComparisonEmpty from '../ComparisonEmpty'; | |||
import SeverityIcon from '../../../../components/shared/severity-icon'; | |||
import SeverityIcon from '../../../../components/shared/SeverityIcon'; | |||
it('should render ComparisonEmpty', () => { | |||
const output = shallow( |
@@ -20,7 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import SeverityIcon from '../../shared/severity-icon'; | |||
import SeverityIcon from '../../shared/SeverityIcon'; | |||
import { sortBySeverity } from '../../../helpers/issues'; | |||
import type { SourceLine } from '../types'; | |||
@@ -11,7 +11,7 @@ exports[`test render highest severity 1`] = ` | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex="0"> | |||
<severity-icon | |||
<SeverityIcon | |||
severity="CRITICAL" /> | |||
<span | |||
className="source-line-issues-counter"> | |||
@@ -27,7 +27,7 @@ exports[`test render highest severity 2`] = ` | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex="0"> | |||
<severity-icon | |||
<SeverityIcon | |||
severity="MINOR" /> | |||
<span | |||
className="source-line-issues-counter"> |
@@ -1,11 +1,11 @@ | |||
exports[`test render issues list 1`] = ` | |||
<div | |||
className="issue-list"> | |||
<Connect(Connect(Issue)) | |||
<Connect(BaseIssue) | |||
issueKey="foo" | |||
onClick={[Function]} | |||
selected={true} /> | |||
<Connect(Connect(Issue)) | |||
<Connect(BaseIssue) | |||
issueKey="bar" | |||
onClick={[Function]} | |||
selected={false} /> |
@@ -0,0 +1,109 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
type Props = { | |||
className?: string, | |||
children: React.Component<*>, | |||
isOpen: boolean, | |||
offset?: { | |||
vertical: number, | |||
horizontal: number | |||
}, | |||
popup: React.Component<*>, | |||
position: 'bottomleft' | 'bottomright', | |||
togglePopup: (?boolean) => void | |||
}; | |||
type State = { | |||
position: { top: number, right: number } | |||
}; | |||
export default class BubblePopupHelper extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
position: { | |||
top: 0, | |||
right: 0 | |||
} | |||
}; | |||
componentDidMount() { | |||
this.setState({ position: this.getPosition(this.props) }); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (!this.props.isOpen && nextProps.isOpen) { | |||
window.addEventListener('keydown', this.handleKey, false); | |||
window.addEventListener('click', this.handleOutsideClick, false); | |||
} else if (this.props.isOpen && !nextProps.isOpen) { | |||
window.removeEventListener('keydown', this.handleKey); | |||
window.removeEventListener('click', this.handleOutsideClick); | |||
} | |||
} | |||
handleKey = (evt: KeyboardEvent) => { | |||
// Escape key | |||
if (evt.keyCode === 27) { | |||
this.props.togglePopup(false); | |||
} | |||
}; | |||
handleOutsideClick = (evt: SyntheticInputEvent) => { | |||
if (!this.popupContainer || !this.popupContainer.contains(evt.target)) { | |||
this.props.togglePopup(false); | |||
} | |||
}; | |||
handleClick(evt: SyntheticInputEvent) { | |||
evt.stopPropagation(); | |||
} | |||
getPosition(props: Props) { | |||
const containerPos = this.container.getBoundingClientRect(); | |||
const { position } = props; | |||
const offset = props.offset || { vertical: 0, horizontal: 0 }; | |||
if (position === 'bottomleft') { | |||
return { top: containerPos.height + offset.vertical, left: offset.horizontal }; | |||
} else if (position === 'bottomright') { | |||
return { top: containerPos.height + offset.vertical, right: offset.horizontal }; | |||
} | |||
} | |||
render() { | |||
return ( | |||
<div | |||
className={classNames(this.props.className, 'bubble-popup-helper')} | |||
ref={container => this.container = container} | |||
onClick={this.handleClick} | |||
tabIndex={0} | |||
role="tooltip"> | |||
{this.props.children} | |||
{this.props.isOpen && | |||
<div ref={popupContainer => this.popupContainer = popupContainer}> | |||
{React.cloneElement(this.props.popup, { | |||
popupPosition: this.state.position | |||
})} | |||
</div>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { getMarkdownHelpUrl } from '../../helpers/urls'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default class MarkdownTips extends React.PureComponent { | |||
handleClick(evt: MouseEvent) { | |||
evt.preventDefault(); | |||
window.open(getMarkdownHelpUrl(), 'height=300,width=600,scrollbars=1,resizable=1'); | |||
} | |||
render() { | |||
return ( | |||
<div className="markdown-tips"> | |||
<a className="little-spacer-right" href="#" onClick={this.handleClick}> | |||
{translate('markdown.helplink')} | |||
</a> | |||
{':'} | |||
<span className="spacer-left">*{translate('bold')}*</span> | |||
<span className="spacer-left">``{translate('code')}``</span> | |||
<span className="spacer-left">* {translate('bulleted_point')}</span> | |||
</div> | |||
); | |||
} | |||
} |
@@ -112,12 +112,18 @@ export default class MultiSelect extends React.PureComponent { | |||
switch (evt.keyCode) { | |||
case 40: // down | |||
this.setState(this.selectNextElement); | |||
evt.stopPropagation(); | |||
evt.preventDefault(); | |||
break; | |||
case 38: // up | |||
this.setState(this.selectPreviousElement); | |||
evt.stopPropagation(); | |||
evt.preventDefault(); | |||
break; | |||
case 37: // left | |||
case 39: // right | |||
evt.stopPropagation(); | |||
break; | |||
case 13: // return | |||
if (this.state.activeIdx >= 0) { | |||
this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]); |
@@ -0,0 +1,133 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import SelectListItem from './SelectListItem'; | |||
type Props = { | |||
children?: SelectListItem, | |||
items: Array<string>, | |||
currentItem: string, | |||
onSelect: (string) => void | |||
}; | |||
type State = { | |||
active: string | |||
}; | |||
export default class SelectList extends React.PureComponent { | |||
list: HTMLElement; | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
active: props.currentItem | |||
}; | |||
} | |||
componentDidMount() { | |||
this.list.focus(); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if ( | |||
nextProps.currentItem !== this.props.currentItem && | |||
!nextProps.items.includes(this.state.active) | |||
) { | |||
this.setState({ active: nextProps.currentItem }); | |||
} | |||
} | |||
handleKeyboard = (evt: KeyboardEvent) => { | |||
switch (evt.keyCode) { | |||
case 40: // down | |||
this.setState(this.selectNextElement); | |||
break; | |||
case 38: // up | |||
this.setState(this.selectPreviousElement); | |||
break; | |||
case 13: // return | |||
if (this.state.active) { | |||
this.handleSelect(this.state.active); | |||
} | |||
break; | |||
default: | |||
return; | |||
} | |||
evt.preventDefault(); | |||
evt.stopPropagation(); | |||
}; | |||
handleSelect = (item: string) => { | |||
this.props.onSelect(item); | |||
}; | |||
handleHover = (item: string) => { | |||
this.setState({ active: item }); | |||
}; | |||
selectNextElement = (state: State, props: Props) => { | |||
const idx = props.items.indexOf(state.active); | |||
if (idx < 0) { | |||
return { active: props.items[0] }; | |||
} | |||
return { active: props.items[(idx + 1) % props.items.length] }; | |||
}; | |||
selectPreviousElement = (state: State, props: Props) => { | |||
const idx = props.items.indexOf(state.active); | |||
if (idx <= 0) { | |||
return { active: props.items[props.items.length - 1] }; | |||
} | |||
return { active: props.items[idx - 1] }; | |||
}; | |||
render() { | |||
const { children } = this.props; | |||
const hasChildren = React.Children.count(children) > 0; | |||
return ( | |||
<ul | |||
className="menu" | |||
onKeyDown={this.handleKeyboard} | |||
ref={list => this.list = list} | |||
tabIndex={0}> | |||
{hasChildren && | |||
React.Children.map(children, child => | |||
React.cloneElement(child, { | |||
active: this.state.active, | |||
onHover: this.handleHover, | |||
onSelect: this.handleSelect | |||
}))} | |||
{!hasChildren && | |||
this.props.items.map(item => ( | |||
<SelectListItem | |||
active={this.state.active} | |||
item={item} | |||
key={item} | |||
onHover={this.handleHover} | |||
onSelect={this.handleSelect} | |||
/> | |||
))} | |||
</ul> | |||
); | |||
} | |||
} |
@@ -0,0 +1,76 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Tooltip from '../controls/Tooltip'; | |||
type Props = { | |||
active?: string, | |||
children?: React.Component<*>, | |||
item: string, | |||
onSelect?: (string) => void, | |||
onHover?: (string) => void, | |||
title?: string | |||
}; | |||
export default class SelectListItem extends React.PureComponent { | |||
props: Props; | |||
handleSelect = (evt: SyntheticInputEvent) => { | |||
evt.preventDefault(); | |||
this.props.onSelect && this.props.onSelect(this.props.item); | |||
}; | |||
handleHover = () => { | |||
this.props.onHover && this.props.onHover(this.props.item); | |||
}; | |||
renderLink() { | |||
let children = this.props.item; | |||
if (this.props.hasOwnProperty('children')) { | |||
children = this.props.children; | |||
} | |||
return ( | |||
<li> | |||
<a | |||
href="#" | |||
className={classNames({ active: this.props.active === this.props.item })} | |||
onClick={this.handleSelect} | |||
onMouseOver={this.handleHover} | |||
onFocus={this.handleHover}> | |||
{children} | |||
</a> | |||
</li> | |||
); | |||
} | |||
render() { | |||
if (this.props.title) { | |||
return ( | |||
<Tooltip placement="right" overlay={this.props.title}> | |||
{this.renderLink()} | |||
</Tooltip> | |||
); | |||
} else { | |||
return this.renderLink(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,139 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow, mount } from 'enzyme'; | |||
import React from 'react'; | |||
import BubblePopupHelper from '../BubblePopupHelper'; | |||
import BubblePopup from '../BubblePopup'; | |||
import { click } from '../../../helpers/testUtils'; | |||
it('should render an open popup on the right', () => { | |||
const toggle = jest.fn(); | |||
const popup = shallow( | |||
<BubblePopupHelper | |||
isOpen={true} | |||
position="bottomright" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup).toMatchSnapshot(); | |||
}); | |||
it('should render the popup helper with a closed popup', () => { | |||
const toggle = jest.fn(); | |||
const popup = shallow( | |||
<BubblePopupHelper | |||
isOpen={false} | |||
position="bottomright" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup).toMatchSnapshot(); | |||
}); | |||
it('should render with custom classes', () => { | |||
const toggle = jest.fn(); | |||
const popup = shallow( | |||
<BubblePopupHelper | |||
customClass="myhelperclass" | |||
isOpen={true} | |||
position="bottomright" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup customClass="mypopupclass"> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup).toMatchSnapshot(); | |||
}); | |||
it('should render the popup with offset', () => { | |||
const toggle = jest.fn(); | |||
const popup = mount( | |||
<BubblePopupHelper | |||
isOpen={true} | |||
offset={{ vertical: 5, horizontal: 2 }} | |||
position="bottomright" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup.find('BubblePopup')).toMatchSnapshot(); | |||
}); | |||
it('should render an open popup on the left', () => { | |||
const toggle = jest.fn(); | |||
const popup = mount( | |||
<BubblePopupHelper | |||
isOpen={true} | |||
offset={{ vertical: 0, horizontal: 2 }} | |||
position="bottomleft" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup.find('BubblePopup')).toMatchSnapshot(); | |||
}); | |||
it('should correctly handle clicks on the button', () => { | |||
const toggle = jest.fn(() => popup.setProps({ isOpen: !popup.props().isOpen })); | |||
const popup = shallow( | |||
<BubblePopupHelper | |||
isOpen={false} | |||
offset={{ vertical: 0, horizontal: 2 }} | |||
position="bottomleft" | |||
togglePopup={toggle} | |||
popup={ | |||
<BubblePopup> | |||
<span>test</span> | |||
</BubblePopup> | |||
}> | |||
<button onClick={toggle}>open</button> | |||
</BubblePopupHelper> | |||
); | |||
expect(popup).toMatchSnapshot(); | |||
click(popup.find('button')); | |||
expect(toggle.mock.calls.length).toBe(1); | |||
expect(popup).toMatchSnapshot(); | |||
}); |
@@ -17,14 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import MarkdownTips from '../MarkdownTips'; | |||
export default React.createClass({ | |||
render() { | |||
if (!this.props.severity) { | |||
return null; | |||
} | |||
const className = 'icon-severity-' + this.props.severity.toLowerCase(); | |||
return <i className={className} />; | |||
} | |||
it('should render the tips', () => { | |||
expect(shallow(<MarkdownTips />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,75 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow, mount } from 'enzyme'; | |||
import React from 'react'; | |||
import SelectList from '../SelectList'; | |||
import SelectListItem from '../SelectListItem'; | |||
import { click, keydown } from '../../../helpers/testUtils'; | |||
it('should render correctly without children', () => { | |||
const onSelect = jest.fn(); | |||
expect( | |||
shallow( | |||
<SelectList | |||
items={['item', 'seconditem', 'third']} | |||
currentItem="seconditem" | |||
onSelect={onSelect} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with children', () => { | |||
const onSelect = jest.fn(); | |||
const items = ['item', 'seconditem', 'third']; | |||
expect( | |||
shallow( | |||
<SelectList items={items} currentItem="seconditem" onSelect={onSelect}> | |||
{items.map(item => ( | |||
<SelectListItem key={item} item={item}> | |||
<i className="myicon" />item | |||
</SelectListItem> | |||
))} | |||
</SelectList> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should correclty handle user actions', () => { | |||
const onSelect = jest.fn(); | |||
const items = ['item', 'seconditem', 'third']; | |||
const list = mount( | |||
<SelectList items={items} currentItem="seconditem" onSelect={onSelect}> | |||
{items.map(item => ( | |||
<SelectListItem key={item} item={item}> | |||
<i className="myicon" />item | |||
</SelectListItem> | |||
))} | |||
</SelectList> | |||
); | |||
keydown(list.find('ul'), 40); | |||
expect(list.state()).toMatchSnapshot(); | |||
keydown(list.find('ul'), 40); | |||
expect(list.state()).toMatchSnapshot(); | |||
keydown(list.find('ul'), 38); | |||
expect(list.state()).toMatchSnapshot(); | |||
click(list.childAt(2).find('a')); | |||
expect(onSelect.mock.calls).toMatchSnapshot(); // eslint-disable-linelist | |||
}); |
@@ -0,0 +1,44 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SelectListItem from '../SelectListItem'; | |||
it('should render correctly without children', () => { | |||
expect(shallow(<SelectListItem item="myitem" />)).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with children', () => { | |||
expect( | |||
shallow( | |||
<SelectListItem active="myitem" item="seconditem"> | |||
<i className="custom-icon" /><p>seconditem</p> | |||
</SelectListItem> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with a tooltip', () => { | |||
expect(shallow(<SelectListItem item="myitem" title="my custom tooltip" />)).toMatchSnapshot(); | |||
}); | |||
it('should render with the active class', () => { | |||
expect(shallow(<SelectListItem active="myitem" item="myitem" />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,148 @@ | |||
exports[`test should correctly handle clicks on the button 1`] = ` | |||
<div | |||
className="bubble-popup-helper" | |||
onClick={[Function]} | |||
role="tooltip" | |||
tabIndex={0}> | |||
<button | |||
onClick={[Function]}> | |||
open | |||
</button> | |||
</div> | |||
`; | |||
exports[`test should correctly handle clicks on the button 2`] = ` | |||
<div | |||
className="bubble-popup-helper" | |||
onClick={[Function]} | |||
role="tooltip" | |||
tabIndex={0}> | |||
<button | |||
onClick={[Function]}> | |||
open | |||
</button> | |||
<div> | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 0, | |||
"top": 0, | |||
} | |||
}> | |||
<span> | |||
test | |||
</span> | |||
</BubblePopup> | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should render an open popup on the left 1`] = ` | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"left": 2, | |||
"top": 0, | |||
} | |||
}> | |||
<div | |||
className="bubble-popup" | |||
style={Object {}}> | |||
<span> | |||
test | |||
</span> | |||
<div | |||
className="bubble-popup-arrow" /> | |||
</div> | |||
</BubblePopup> | |||
`; | |||
exports[`test should render an open popup on the right 1`] = ` | |||
<div | |||
className="bubble-popup-helper" | |||
onClick={[Function]} | |||
role="tooltip" | |||
tabIndex={0}> | |||
<button | |||
onClick={[Function]}> | |||
open | |||
</button> | |||
<div> | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 0, | |||
"top": 0, | |||
} | |||
}> | |||
<span> | |||
test | |||
</span> | |||
</BubblePopup> | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should render the popup helper with a closed popup 1`] = ` | |||
<div | |||
className="bubble-popup-helper" | |||
onClick={[Function]} | |||
role="tooltip" | |||
tabIndex={0}> | |||
<button | |||
onClick={[Function]}> | |||
open | |||
</button> | |||
</div> | |||
`; | |||
exports[`test should render the popup with offset 1`] = ` | |||
<BubblePopup | |||
customClass="" | |||
popupPosition={ | |||
Object { | |||
"right": 2, | |||
"top": 5, | |||
} | |||
}> | |||
<div | |||
className="bubble-popup" | |||
style={Object {}}> | |||
<span> | |||
test | |||
</span> | |||
<div | |||
className="bubble-popup-arrow" /> | |||
</div> | |||
</BubblePopup> | |||
`; | |||
exports[`test should render with custom classes 1`] = ` | |||
<div | |||
className="bubble-popup-helper" | |||
onClick={[Function]} | |||
role="tooltip" | |||
tabIndex={0}> | |||
<button | |||
onClick={[Function]}> | |||
open | |||
</button> | |||
<div> | |||
<BubblePopup | |||
customClass="mypopupclass" | |||
popupPosition={ | |||
Object { | |||
"right": 0, | |||
"top": 0, | |||
} | |||
}> | |||
<span> | |||
test | |||
</span> | |||
</BubblePopup> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,29 @@ | |||
exports[`test should render the tips 1`] = ` | |||
<div | |||
className="markdown-tips"> | |||
<a | |||
className="little-spacer-right" | |||
href="#" | |||
onClick={[Function]}> | |||
markdown.helplink | |||
</a> | |||
: | |||
<span | |||
className="spacer-left"> | |||
* | |||
bold | |||
* | |||
</span> | |||
<span | |||
className="spacer-left"> | |||
\`\` | |||
code | |||
\`\` | |||
</span> | |||
<span | |||
className="spacer-left"> | |||
* | |||
bulleted_point | |||
</span> | |||
</div> | |||
`; |
@@ -0,0 +1,83 @@ | |||
exports[`test should correclty handle user actions 1`] = ` | |||
Object { | |||
"active": "third", | |||
} | |||
`; | |||
exports[`test should correclty handle user actions 2`] = ` | |||
Object { | |||
"active": "item", | |||
} | |||
`; | |||
exports[`test should correclty handle user actions 3`] = ` | |||
Object { | |||
"active": "third", | |||
} | |||
`; | |||
exports[`test should correclty handle user actions 4`] = ` | |||
Array [ | |||
Array [ | |||
"third", | |||
], | |||
] | |||
`; | |||
exports[`test should render correctly with children 1`] = ` | |||
<ul | |||
className="menu" | |||
onKeyDown={[Function]} | |||
tabIndex={0}> | |||
<SelectListItem | |||
active="seconditem" | |||
item="item" | |||
onHover={[Function]} | |||
onSelect={[Function]}> | |||
<i | |||
className="myicon" /> | |||
item | |||
</SelectListItem> | |||
<SelectListItem | |||
active="seconditem" | |||
item="seconditem" | |||
onHover={[Function]} | |||
onSelect={[Function]}> | |||
<i | |||
className="myicon" /> | |||
item | |||
</SelectListItem> | |||
<SelectListItem | |||
active="seconditem" | |||
item="third" | |||
onHover={[Function]} | |||
onSelect={[Function]}> | |||
<i | |||
className="myicon" /> | |||
item | |||
</SelectListItem> | |||
</ul> | |||
`; | |||
exports[`test should render correctly without children 1`] = ` | |||
<ul | |||
className="menu" | |||
onKeyDown={[Function]} | |||
tabIndex={0}> | |||
<SelectListItem | |||
active="seconditem" | |||
item="item" | |||
onHover={[Function]} | |||
onSelect={[Function]} /> | |||
<SelectListItem | |||
active="seconditem" | |||
item="seconditem" | |||
onHover={[Function]} | |||
onSelect={[Function]} /> | |||
<SelectListItem | |||
active="seconditem" | |||
item="third" | |||
onHover={[Function]} | |||
onSelect={[Function]} /> | |||
</ul> | |||
`; |
@@ -0,0 +1,59 @@ | |||
exports[`test should render correctly with a tooltip 1`] = ` | |||
<Tooltip | |||
overlay="my custom tooltip" | |||
placement="right"> | |||
<li> | |||
<a | |||
className="" | |||
href="#" | |||
onClick={[Function]} | |||
onFocus={[Function]} | |||
onMouseOver={[Function]}> | |||
myitem | |||
</a> | |||
</li> | |||
</Tooltip> | |||
`; | |||
exports[`test should render correctly with children 1`] = ` | |||
<li> | |||
<a | |||
className="" | |||
href="#" | |||
onClick={[Function]} | |||
onFocus={[Function]} | |||
onMouseOver={[Function]}> | |||
<i | |||
className="custom-icon" /> | |||
<p> | |||
seconditem | |||
</p> | |||
</a> | |||
</li> | |||
`; | |||
exports[`test should render correctly without children 1`] = ` | |||
<li> | |||
<a | |||
className="" | |||
href="#" | |||
onClick={[Function]} | |||
onFocus={[Function]} | |||
onMouseOver={[Function]}> | |||
myitem | |||
</a> | |||
</li> | |||
`; | |||
exports[`test should render with the active class 1`] = ` | |||
<li> | |||
<a | |||
className="active" | |||
href="#" | |||
onClick={[Function]} | |||
onFocus={[Function]} | |||
onMouseOver={[Function]}> | |||
myitem | |||
</a> | |||
</li> | |||
`; |
@@ -25,7 +25,8 @@ export default class Checkbox extends React.Component { | |||
id: React.PropTypes.string, | |||
onCheck: React.PropTypes.func.isRequired, | |||
checked: React.PropTypes.bool.isRequired, | |||
thirdState: React.PropTypes.bool | |||
thirdState: React.PropTypes.bool, | |||
className: React.PropTypes.string | |||
}; | |||
static defaultProps = { | |||
@@ -43,7 +44,7 @@ export default class Checkbox extends React.Component { | |||
} | |||
render() { | |||
const className = classNames('icon-checkbox', { | |||
const className = classNames(this.props.className, 'icon-checkbox', { | |||
'icon-checkbox-checked': this.props.checked, | |||
'icon-checkbox-single': this.props.thirdState | |||
}); |
@@ -57,3 +57,10 @@ it('should call onCheck with id as second parameter', () => { | |||
click(checkbox); | |||
expect(onCheck).toBeCalledWith(true, 'foo'); | |||
}); | |||
it('should apply custom class', () => { | |||
const checkbox = shallow( | |||
<Checkbox className="customclass" checked={true} onCheck={() => true} /> | |||
); | |||
expect(checkbox.is('.customclass')).toBe(true); | |||
}); |
@@ -0,0 +1,153 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import IssueView from './IssueView'; | |||
import { setIssueAssignee } from '../../api/issues'; | |||
import type { Issue } from './types'; | |||
type Props = { | |||
checked?: boolean, | |||
issue: Issue, | |||
onCheck?: () => void, | |||
onClick: (string) => void, | |||
onFail: (Error) => void, | |||
onFilterClick?: () => void, | |||
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, | |||
selected: boolean | |||
}; | |||
type State = { | |||
currentPopup: string | |||
}; | |||
export default class BaseIssue extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State; | |||
static defaultProps = { | |||
selected: false | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
currentPopup: '' | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (this.props.selected) { | |||
this.bindShortcuts(); | |||
} | |||
} | |||
componentWillUpdate(nextProps: Props) { | |||
if (!nextProps.selected && this.props.selected) { | |||
this.unbindShortcuts(); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (!prevProps.selected && this.props.selected) { | |||
this.bindShortcuts(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
if (this.props.selected) { | |||
this.unbindShortcuts(); | |||
} | |||
} | |||
bindShortcuts() { | |||
document.addEventListener('keypress', this.handleKeyPress); | |||
} | |||
unbindShortcuts() { | |||
document.removeEventListener('keypress', this.handleKeyPress); | |||
} | |||
togglePopup = (popupName: string, open?: boolean) => { | |||
if (this.mounted) { | |||
this.setState((prevState: State) => { | |||
if (prevState.currentPopup !== popupName && open !== false) { | |||
return { currentPopup: popupName }; | |||
} else if (prevState.currentPopup === popupName && open !== true) { | |||
return { currentPopup: '' }; | |||
} | |||
return prevState; | |||
}); | |||
} | |||
}; | |||
handleAssignement = (login: string) => { | |||
const { issue } = this.props; | |||
if (issue.assignee !== login) { | |||
this.props.onIssueChange(setIssueAssignee({ issue: issue.key, assignee: login })); | |||
} | |||
this.togglePopup('assign', false); | |||
}; | |||
handleKeyPress = (e: Object) => { | |||
const tagName = e.target.tagName.toUpperCase(); | |||
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; | |||
if (shouldHandle) { | |||
switch (e.key) { | |||
case 'f': | |||
return this.togglePopup('transition'); | |||
case 'a': | |||
return this.togglePopup('assign'); | |||
case 'm': | |||
return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me'); | |||
case 'p': | |||
return this.togglePopup('plan'); | |||
case 'i': | |||
return this.togglePopup('set-severity'); | |||
case 'c': | |||
return this.togglePopup('comment'); | |||
case 't': | |||
return this.togglePopup('edit-tags'); | |||
} | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<IssueView | |||
issue={this.props.issue} | |||
checked={this.props.checked} | |||
onAssign={this.handleAssignement} | |||
onCheck={this.props.onCheck} | |||
onClick={this.props.onClick} | |||
onFail={this.props.onFail} | |||
onFilterClick={this.props.onFilterClick} | |||
onIssueChange={this.props.onIssueChange} | |||
togglePopup={this.togglePopup} | |||
currentPopup={this.state.currentPopup} | |||
selected={this.props.selected} | |||
/> | |||
); | |||
} | |||
} |
@@ -19,11 +19,18 @@ | |||
*/ | |||
// @flow | |||
import { connect } from 'react-redux'; | |||
import Issue from './Issue'; | |||
import BaseIssue from './BaseIssue'; | |||
import { getIssueByKey } from '../../store/rootReducer'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { updateIssue } from './actions'; | |||
const mapStateToProps = (state, ownProps) => ({ | |||
issue: getIssueByKey(state, ownProps.issueKey) | |||
}); | |||
export default connect(mapStateToProps)(Issue); | |||
const mapDispatchToProps = { | |||
onIssueChange: updateIssue, | |||
onFail: error => dispatch => onFail(dispatch)(error) | |||
}; | |||
export default connect(mapStateToProps, mapDispatchToProps)(BaseIssue); |
@@ -18,134 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import IssueView from './issue-view'; | |||
import IssueModel from './models/issue'; | |||
import { receiveIssues } from '../../store/issues/duck'; | |||
import type { Issue as IssueType } from './types'; | |||
import BaseIssue from './BaseIssue'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { updateIssue } from './actions'; | |||
type Model = { toJSON: () => {} }; | |||
type Props = { | |||
checked?: boolean, | |||
issue: IssueType | Model, | |||
onCheck?: () => void, | |||
onClick: () => void, | |||
onFilterClick?: () => void, | |||
onIssueChange: ({}) => void, | |||
selected: boolean | |||
const mapDispatchToProps = { | |||
onIssueChange: updateIssue, | |||
onFail: error => dispatch => onFail(dispatch)(error) | |||
}; | |||
class Issue extends React.PureComponent { | |||
issueView: Object; | |||
node: HTMLElement; | |||
props: Props; | |||
static defaultProps = { | |||
selected: false | |||
}; | |||
componentDidMount() { | |||
this.renderIssueView(); | |||
if (this.props.selected) { | |||
this.bindShortcuts(); | |||
} | |||
} | |||
componentWillUpdate(nextProps: Props) { | |||
if (!nextProps.selected && this.props.selected) { | |||
this.unbindShortcuts(); | |||
} | |||
this.destroyIssueView(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
this.renderIssueView(); | |||
if (!prevProps.selected && this.props.selected) { | |||
this.bindShortcuts(); | |||
} | |||
// $FlowFixMe resolution doesn't exist in type `Model` | |||
const { resolution } = this.props.issue; | |||
if (!prevProps.issue.resolution && ['FALSE-POSITIVE', 'WONTFIX'].includes(resolution)) { | |||
this.issueView.comment({ fromTransition: true }); | |||
} | |||
} | |||
componentWillUnmount() { | |||
if (this.props.selected) { | |||
this.unbindShortcuts(); | |||
} | |||
this.destroyIssueView(); | |||
} | |||
bindShortcuts() { | |||
document.addEventListener('keypress', this.handleKeyPress); | |||
} | |||
unbindShortcuts() { | |||
document.removeEventListener('keypress', this.handleKeyPress); | |||
} | |||
doIssueAction(action: string) { | |||
this.issueView.$('.js-issue-' + action).click(); | |||
} | |||
handleKeyPress = (e: Object) => { | |||
const tagName = e.target.tagName.toUpperCase(); | |||
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON'; | |||
if (shouldHandle) { | |||
switch (e.key) { | |||
case 'f': | |||
return this.doIssueAction('transition'); | |||
case 'a': | |||
return this.doIssueAction('assign'); | |||
case 'm': | |||
return this.doIssueAction('assign-to-me'); | |||
case 'p': | |||
return this.doIssueAction('plan'); | |||
case 'i': | |||
return this.doIssueAction('set-severity'); | |||
case 'c': | |||
return this.doIssueAction('comment'); | |||
case 't': | |||
return this.doIssueAction('edit-tags'); | |||
} | |||
} | |||
}; | |||
destroyIssueView() { | |||
this.issueView.destroy(); | |||
} | |||
renderIssueView() { | |||
const model = this.props.issue.toJSON ? this.props.issue : new IssueModel(this.props.issue); | |||
this.issueView = new IssueView({ | |||
model, | |||
checked: this.props.checked, | |||
onCheck: this.props.onCheck, | |||
onClick: this.props.onClick, | |||
onFilterClick: this.props.onFilterClick, | |||
onIssueChange: this.props.onIssueChange | |||
}); | |||
this.issueView.render().$el.appendTo(this.node); | |||
if (this.props.selected) { | |||
this.issueView.select(); | |||
} | |||
} | |||
render() { | |||
return <div className="issue-container" ref={node => this.node = node} />; | |||
} | |||
} | |||
const onIssueChange = issue => | |||
dispatch => { | |||
dispatch(receiveIssues([issue])); | |||
}; | |||
const mapDispatchToProps = { onIssueChange }; | |||
export default connect(null, mapDispatchToProps)(Issue); | |||
export default connect(null, mapDispatchToProps)(BaseIssue); |
@@ -0,0 +1,121 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import Checkbox from '../../components/controls/Checkbox'; | |||
import IssueTitleBar from './components/IssueTitleBar'; | |||
import IssueActionsBar from './components/IssueActionsBar'; | |||
import IssueCommentLine from './components/IssueCommentLine'; | |||
import { deleteIssueComment, editIssueComment } from '../../api/issues'; | |||
import type { Issue } from './types'; | |||
type Props = { | |||
checked?: boolean, | |||
currentPopup: string, | |||
issue: Issue, | |||
onAssign: (string) => void, | |||
onCheck?: () => void, | |||
onClick: (string) => void, | |||
onFail: (Error) => void, | |||
onFilterClick?: () => void, | |||
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, | |||
selected: boolean, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueView extends React.PureComponent { | |||
props: Props; | |||
handleClick = (evt: MouseEvent) => { | |||
evt.preventDefault(); | |||
if (this.props.onClick) { | |||
this.props.onClick(this.props.issue.key); | |||
} | |||
}; | |||
editComment = (comment: string, text: string) => { | |||
this.props.onIssueChange(editIssueComment({ comment, text })); | |||
}; | |||
deleteComment = (comment: string) => { | |||
this.props.onIssueChange(deleteIssueComment({ comment })); | |||
}; | |||
render() { | |||
const { issue } = this.props; | |||
const hasCheckbox = this.props.onCheck != null; | |||
const issueClass = classNames('issue', { | |||
'issue-with-checkbox': hasCheckbox, | |||
selected: this.props.selected | |||
}); | |||
return ( | |||
<div | |||
className={issueClass} | |||
data-issue={issue.key} | |||
onClick={this.handleClick} | |||
tabIndex={0} | |||
role="listitem"> | |||
<IssueTitleBar | |||
issue={issue} | |||
currentPopup={this.props.currentPopup} | |||
onFail={this.props.onFail} | |||
onFilterClick={this.props.onFilterClick} | |||
togglePopup={this.props.togglePopup} | |||
/> | |||
<IssueActionsBar | |||
issue={issue} | |||
currentPopup={this.props.currentPopup} | |||
onAssign={this.props.onAssign} | |||
onFail={this.props.onFail} | |||
togglePopup={this.props.togglePopup} | |||
onIssueChange={this.props.onIssueChange} | |||
/> | |||
{issue.comments && | |||
issue.comments.length > 0 && | |||
<div className="issue-comments"> | |||
{issue.comments.map(comment => ( | |||
<IssueCommentLine | |||
comment={comment} | |||
key={comment.key} | |||
onEdit={this.editComment} | |||
onDelete={this.deleteComment} | |||
/> | |||
))} | |||
</div>} | |||
<a className="issue-navigate js-issue-navigate"> | |||
<i className="issue-navigate-to-left icon-chevron-left" /> | |||
<i className="issue-navigate-to-right icon-chevron-right" /> | |||
</a> | |||
{hasCheckbox && | |||
<div className="js-toggle issue-checkbox-container"> | |||
<Checkbox | |||
className="issue-checkbox" | |||
onCheck={this.props.onCheck} | |||
checked={this.props.checked} | |||
/> | |||
</div>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,52 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import type { Dispatch } from 'redux'; | |||
import type { Issue } from './types'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { receiveIssues } from '../../store/issues/duck'; | |||
import { parseIssueFromResponse } from '../../helpers/issues'; | |||
export const updateIssue = (resultPromise: Promise<*>, oldIssue?: Issue, newIssue?: Issue) => | |||
(dispatch: Dispatch<*>) => { | |||
if (oldIssue && newIssue) { | |||
dispatch(receiveIssues([newIssue])); | |||
} | |||
resultPromise.then( | |||
response => { | |||
dispatch( | |||
receiveIssues([ | |||
parseIssueFromResponse( | |||
response.issue, | |||
response.components, | |||
response.users, | |||
response.rules | |||
) | |||
]) | |||
); | |||
}, | |||
error => { | |||
onFail(dispatch)(error); | |||
if (oldIssue && newIssue) { | |||
dispatch(receiveIssues([oldIssue])); | |||
} | |||
} | |||
); | |||
}; |
@@ -0,0 +1,164 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import IssueAssign from './IssueAssign'; | |||
import IssueCommentAction from './IssueCommentAction'; | |||
import IssueSeverity from './IssueSeverity'; | |||
import IssueTags from './IssueTags'; | |||
import IssueTransition from './IssueTransition'; | |||
import IssueType from './IssueType'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
issue: Issue, | |||
currentPopup: string, | |||
onAssign: (string) => void, | |||
onFail: (Error) => void, | |||
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, | |||
togglePopup: (string) => void | |||
}; | |||
type State = { | |||
commentPlaceholder: string | |||
}; | |||
export default class IssueActionsBar extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
commentPlaceholder: '' | |||
}; | |||
componentDidUpdate(prevProps: Props) { | |||
const { resolution } = this.props.issue; | |||
if (!prevProps.issue.resolution && ['FALSE-POSITIVE', 'WONTFIX'].includes(resolution)) { | |||
this.toggleComment(true, translate('issue.comment.tell_why')); | |||
} | |||
} | |||
setIssueProperty = ( | |||
property: string, | |||
popup: string, | |||
apiCall: (Object) => Promise<*>, | |||
value: string | |||
) => { | |||
const { issue } = this.props; | |||
if (issue[property] !== value) { | |||
const newIssue = { ...issue, [property]: value }; | |||
this.props.onIssueChange(apiCall({ issue: issue.key, [property]: value }), issue, newIssue); | |||
} | |||
this.props.togglePopup(popup, false); | |||
}; | |||
toggleComment = (open?: boolean, placeholder?: string) => { | |||
this.setState({ | |||
commentPlaceholder: placeholder || '' | |||
}); | |||
this.props.togglePopup('comment', open); | |||
}; | |||
render() { | |||
const { issue } = this.props; | |||
const canAssign = issue.actions.includes('assign'); | |||
const canComment = issue.actions.includes('comment'); | |||
const canSetSeverity = issue.actions.includes('set_severity'); | |||
const canSetTags = issue.actions.includes('set_tags'); | |||
const hasTransitions = issue.transitions && issue.transitions.length > 0; | |||
return ( | |||
<table className="issue-table"> | |||
<tbody> | |||
<tr> | |||
<td> | |||
<ul className="list-inline issue-meta-list"> | |||
<li className="issue-meta"> | |||
<IssueType | |||
isOpen={this.props.currentPopup === 'set-type' && canSetSeverity} | |||
issue={issue} | |||
canSetSeverity={canSetSeverity} | |||
togglePopup={this.props.togglePopup} | |||
setIssueProperty={this.setIssueProperty} | |||
/> | |||
</li> | |||
<li className="issue-meta"> | |||
<IssueSeverity | |||
isOpen={this.props.currentPopup === 'set-severity' && canSetSeverity} | |||
issue={issue} | |||
canSetSeverity={canSetSeverity} | |||
togglePopup={this.props.togglePopup} | |||
setIssueProperty={this.setIssueProperty} | |||
/> | |||
</li> | |||
<li className="issue-meta"> | |||
<IssueTransition | |||
isOpen={this.props.currentPopup === 'transition' && hasTransitions} | |||
issue={issue} | |||
hasTransitions={hasTransitions} | |||
togglePopup={this.props.togglePopup} | |||
setIssueProperty={this.setIssueProperty} | |||
/> | |||
</li> | |||
<li className="issue-meta"> | |||
<IssueAssign | |||
isOpen={this.props.currentPopup === 'assign' && canAssign} | |||
issue={issue} | |||
canAssign={canAssign} | |||
onAssign={this.props.onAssign} | |||
onFail={this.props.onFail} | |||
togglePopup={this.props.togglePopup} | |||
/> | |||
</li> | |||
{issue.effort && | |||
<li className="issue-meta"> | |||
<span className="issue-meta-label"> | |||
{translateWithParameters('issue.x_effort', issue.effort)} | |||
</span> | |||
</li>} | |||
{canComment && | |||
<IssueCommentAction | |||
issueKey={issue.key} | |||
commentPlaceholder={this.state.commentPlaceholder} | |||
currentPopup={this.props.currentPopup} | |||
onIssueChange={this.props.onIssueChange} | |||
toggleComment={this.toggleComment} | |||
/>} | |||
</ul> | |||
</td> | |||
<td className="issue-table-meta-cell"> | |||
<ul className="list-inline"> | |||
<li className="issue-meta js-issue-tags"> | |||
<IssueTags | |||
isOpen={this.props.currentPopup === 'edit-tags' && canSetTags} | |||
canSetTags={canSetTags} | |||
issue={issue} | |||
onFail={this.props.onFail} | |||
onIssueChange={this.props.onIssueChange} | |||
togglePopup={this.props.togglePopup} | |||
/> | |||
</li> | |||
</ul> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
); | |||
} | |||
} |
@@ -0,0 +1,85 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import SetAssigneePopup from '../popups/SetAssigneePopup'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
isOpen: boolean, | |||
issue: Issue, | |||
canAssign: boolean, | |||
onAssign: (string) => void, | |||
onFail: (Error) => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueAssign extends React.PureComponent { | |||
props: Props; | |||
toggleAssign = (open?: boolean) => { | |||
this.props.togglePopup('assign', open); | |||
}; | |||
renderAssignee() { | |||
const { issue } = this.props; | |||
return ( | |||
<span> | |||
{issue.assignee && | |||
<span className="text-top"> | |||
<Avatar className="little-spacer-right" hash={issue.assigneeAvatar} size={16} /> | |||
</span>} | |||
<span className="issue-meta-label"> | |||
{issue.assignee ? issue.assigneeName : translate('unassigned')} | |||
</span> | |||
</span> | |||
); | |||
} | |||
render() { | |||
if (this.props.canAssign) { | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen && this.props.canAssign} | |||
position="bottomleft" | |||
togglePopup={this.toggleAssign} | |||
popup={ | |||
<SetAssigneePopup | |||
issue={this.props.issue} | |||
onFail={this.props.onFail} | |||
onSelect={this.props.onAssign} | |||
/> | |||
}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-assign" | |||
onClick={this.toggleAssign}> | |||
{this.renderAssignee()} | |||
<i className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} else { | |||
return this.renderAssignee(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import ChangelogPopup from '../popups/ChangelogPopup'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
isOpen: boolean, | |||
issue: Issue, | |||
creationDate: string, | |||
togglePopup: (string) => void, | |||
onFail: (Error) => void | |||
}; | |||
export default class IssueChangelog extends React.PureComponent { | |||
props: Props; | |||
handleClick = (evt: SyntheticInputEvent) => { | |||
evt.preventDefault(); | |||
this.toggleChangelog(); | |||
}; | |||
toggleChangelog = (open?: boolean) => { | |||
this.props.togglePopup('changelog', open); | |||
}; | |||
render() { | |||
const momentCreationDate = moment(this.props.creationDate); | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen} | |||
position="bottomright" | |||
togglePopup={this.toggleChangelog} | |||
popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-show-changelog" | |||
title={momentCreationDate.format('LLL')} | |||
onClick={this.handleClick}> | |||
<span className="issue-meta-label">{momentCreationDate.fromNow()}</span> | |||
<i className="icon-dropdown little-spacer-left" /> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export type ChangelogDiff = { | |||
key: string, | |||
oldValue?: string, | |||
newValue?: string | |||
}; | |||
export default function IssueChangelogDiff(props: { diff: ChangelogDiff }) { | |||
const { diff } = props; | |||
if (diff.key === 'file') { | |||
return ( | |||
<p> | |||
{translateWithParameters( | |||
'issue.change.file_move', | |||
diff.oldValue || '', | |||
diff.newValue || '' | |||
)} | |||
</p> | |||
); | |||
} | |||
let message: string; | |||
if (diff.newValue != null) { | |||
let newValue: string = diff.newValue; | |||
if (diff.key === 'effort') { | |||
newValue = formatMeasure(diff.newValue, 'WORK_DUR'); | |||
} | |||
message = translateWithParameters( | |||
'issue.changelog.changed_to', | |||
translate('issue.changelog.field', diff.key), | |||
newValue | |||
); | |||
} else { | |||
message = translateWithParameters( | |||
'issue.changelog.removed', | |||
translate('issue.changelog.field', diff.key) | |||
); | |||
} | |||
if (diff.oldValue != null) { | |||
let oldValue: string = diff.oldValue; | |||
if (diff.key === 'effort') { | |||
oldValue = formatMeasure(diff.oldValue, 'WORK_DUR'); | |||
} | |||
message += ` (${translateWithParameters('issue.changelog.was', oldValue)})`; | |||
} | |||
return <p>{message}</p>; | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import CommentPopup from '../popups/CommentPopup'; | |||
import { addIssueComment } from '../../../api/issues'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
issueKey: string, | |||
commentPlaceholder: string, | |||
currentPopup: string, | |||
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, | |||
toggleComment: (open?: boolean, placeholder?: string) => void | |||
}; | |||
export default class IssueCommentAction extends React.PureComponent { | |||
props: Props; | |||
addComment = (text: string) => { | |||
this.props.onIssueChange(addIssueComment({ issue: this.props.issueKey, text })); | |||
this.props.toggleComment(false); | |||
}; | |||
handleCommentClick = () => this.props.toggleComment(); | |||
render() { | |||
return ( | |||
<li className="issue-meta"> | |||
<BubblePopupHelper | |||
isOpen={this.props.currentPopup === 'comment'} | |||
position="bottomleft" | |||
togglePopup={this.props.toggleComment} | |||
popup={ | |||
<CommentPopup | |||
customClass="issue-comment-bubble-popup" | |||
placeholder={this.props.commentPlaceholder} | |||
onComment={this.addComment} | |||
toggleComment={this.props.toggleComment} | |||
/> | |||
}> | |||
<button | |||
className="button-link issue-action js-issue-comment" | |||
onClick={this.handleCommentClick}> | |||
<span className="issue-meta-label"> | |||
{translate('issue.comment.formlink')} | |||
</span> | |||
</button> | |||
</BubblePopupHelper> | |||
</li> | |||
); | |||
} | |||
} |
@@ -0,0 +1,122 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import CommentDeletePopup from '../popups/CommentDeletePopup'; | |||
import CommentPopup from '../popups/CommentPopup'; | |||
import type { IssueComment } from '../types'; | |||
type Props = { | |||
comment: IssueComment, | |||
onDelete: (string) => void, | |||
onEdit: (string, string) => void | |||
}; | |||
type State = { | |||
openPopup: string | |||
}; | |||
export default class IssueCommentLine extends React.PureComponent { | |||
props: Props; | |||
state: State = { | |||
openPopup: '' | |||
}; | |||
handleEdit = (text: string) => { | |||
this.props.onEdit(this.props.comment.key, text); | |||
this.toggleEditPopup(false); | |||
}; | |||
handleDelete = () => { | |||
this.props.onDelete(this.props.comment.key); | |||
this.toggleDeletePopup(false); | |||
}; | |||
togglePopup = (popupName: string, force?: boolean) => { | |||
this.setState((prevState: State) => { | |||
if (prevState.openPopup !== popupName && force !== false) { | |||
return { openPopup: popupName }; | |||
} else if (prevState.openPopup === popupName && force !== true) { | |||
return { openPopup: '' }; | |||
} | |||
return prevState; | |||
}); | |||
}; | |||
toggleDeletePopup = (force?: boolean) => this.togglePopup('delete', force); | |||
toggleEditPopup = (force?: boolean) => this.togglePopup('edit', force); | |||
render() { | |||
const { comment } = this.props; | |||
return ( | |||
<div className="issue-comment"> | |||
<div className="issue-comment-author" title={comment.authorName}> | |||
<Avatar className="little-spacer-right" hash={comment.authorAvatar} size={16} /> | |||
{comment.authorName} | |||
</div> | |||
<div | |||
className="issue-comment-text markdown" | |||
dangerouslySetInnerHTML={{ __html: comment.htmlText }} | |||
/> | |||
<div className="issue-comment-age">({moment(comment.createdAt).fromNow()})</div> | |||
<div className="issue-comment-actions"> | |||
{comment.updatable && | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={this.state.openPopup === 'edit'} | |||
offset={{ vertical: 0, horizontal: -6 }} | |||
position="bottomright" | |||
togglePopup={this.toggleDeletePopup} | |||
popup={ | |||
<CommentPopup | |||
comment={comment} | |||
customClass="issue-edit-comment-bubble-popup" | |||
onComment={this.handleEdit} | |||
placeholder="" | |||
toggleComment={this.toggleEditPopup} | |||
/> | |||
}> | |||
<button | |||
className="js-issue-comment-edit button-link icon-edit icon-half-transparent" | |||
onClick={this.toggleEditPopup} | |||
/> | |||
</BubblePopupHelper>} | |||
{comment.updatable && | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={this.state.openPopup === 'delete'} | |||
offset={{ vertical: 0, horizontal: -10 }} | |||
position="bottomright" | |||
togglePopup={this.toggleDeletePopup} | |||
popup={<CommentDeletePopup onDelete={this.handleDelete} />}> | |||
<button | |||
className="js-issue-comment-delete button-link icon-delete icon-half-transparent" | |||
onClick={this.toggleDeletePopup} | |||
/> | |||
</BubblePopupHelper>} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class IssueMessage extends React.PureComponent { | |||
props: { | |||
message: string, | |||
rule: string, | |||
organization: string | |||
}; | |||
handleClick = (e: MouseEvent) => { | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
const Workspace = require('../../workspace/main').default; | |||
Workspace.openRule({ | |||
key: this.props.rule, | |||
organization: this.props.organization | |||
}); | |||
}; | |||
render() { | |||
return ( | |||
<div className="issue-message"> | |||
{this.props.message} | |||
<button | |||
className="button-link issue-rule icon-ellipsis-h little-spacer-left" | |||
aria-label={translate('issue.rule_details')} | |||
onClick={this.handleClick} | |||
/> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,70 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import SetSeverityPopup from '../popups/SetSeverityPopup'; | |||
import SeverityHelper from '../../../components/shared/SeverityHelper'; | |||
import { setIssueSeverity } from '../../../api/issues'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
canSetSeverity: boolean, | |||
isOpen: boolean, | |||
issue: Issue, | |||
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueSeverity extends React.PureComponent { | |||
props: Props; | |||
toggleSetSeverity = (open?: boolean) => { | |||
this.props.togglePopup('set-severity', open); | |||
}; | |||
setSeverity = (severity: string) => | |||
this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity); | |||
render() { | |||
const { issue } = this.props; | |||
if (this.props.canSetSeverity) { | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen && this.props.canSetSeverity} | |||
position="bottomleft" | |||
togglePopup={this.toggleSetSeverity} | |||
popup={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-severity" | |||
onClick={this.toggleSetSeverity}> | |||
<SeverityHelper | |||
className="issue-meta-label little-spacer-right" | |||
severity={issue.severity} | |||
/> | |||
<i className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} else { | |||
return <SeverityHelper className="issue-meta-label" severity={issue.severity} />; | |||
} | |||
} | |||
} |
@@ -0,0 +1,90 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import SetIssueTagsPopup from '../popups/SetIssueTagsPopup'; | |||
import TagsList from '../../../components/tags/TagsList'; | |||
import { setIssueTags } from '../../../api/issues'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
canSetTags: boolean, | |||
isOpen: boolean, | |||
issue: Issue, | |||
onFail: (Error) => void, | |||
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueTags extends React.PureComponent { | |||
props: Props; | |||
toggleSetTags = (open?: boolean) => { | |||
this.props.togglePopup('edit-tags', open); | |||
}; | |||
setTags = (tags: Array<string>) => { | |||
const { issue } = this.props; | |||
const newIssue = { ...issue, tags }; | |||
this.props.onIssueChange( | |||
setIssueTags({ issue: issue.key, tags: tags.join(',') }), | |||
issue, | |||
newIssue | |||
); | |||
}; | |||
render() { | |||
const { issue } = this.props; | |||
if (this.props.canSetTags) { | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen} | |||
position="bottomright" | |||
togglePopup={this.toggleSetTags} | |||
popup={ | |||
<SetIssueTagsPopup | |||
onFail={this.props.onFail} | |||
selectedTags={issue.tags} | |||
setTags={this.setTags} | |||
/> | |||
}> | |||
<button | |||
className={'js-issue-edit-tags button-link issue-action issue-action-with-options'} | |||
onClick={this.toggleSetTags}> | |||
<TagsList | |||
tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} | |||
allowUpdate={this.props.canSetTags} | |||
/> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} else { | |||
return ( | |||
<TagsList | |||
tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]} | |||
allowUpdate={this.props.canSetTags} | |||
/> | |||
); | |||
} | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import IssueChangelog from './IssueChangelog'; | |||
import IssueMessage from './IssueMessage'; | |||
import { getSingleIssueUrl } from '../../../helpers/urls'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
issue: Issue, | |||
currentPopup: string, | |||
onFail: (Error) => void, | |||
onFilterClick?: () => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default function IssueTitleBar(props: Props) { | |||
const { issue } = props; | |||
const hasSimilarIssuesFilter = props.onFilterClick != null; | |||
return ( | |||
<table className="issue-table"> | |||
<tbody> | |||
<tr> | |||
<td> | |||
<IssueMessage | |||
message={issue.message} | |||
rule={issue.rule} | |||
organization={issue.organization} | |||
/> | |||
</td> | |||
<td className="issue-table-meta-cell issue-table-meta-cell-first"> | |||
<ul className="list-inline issue-meta-list"> | |||
<li className="issue-meta"> | |||
<IssueChangelog | |||
creationDate={issue.creationDate} | |||
isOpen={props.currentPopup === 'changelog'} | |||
issue={issue} | |||
togglePopup={props.togglePopup} | |||
onFail={props.onFail} | |||
/> | |||
</li> | |||
{issue.line != null && | |||
<li className="issue-meta"> | |||
<span className="issue-meta-label" title={translate('line_number')}> | |||
L{issue.line} | |||
</span> | |||
</li>} | |||
<li className="issue-meta"> | |||
<a | |||
className="js-issue-permalink icon-link" | |||
href={getSingleIssueUrl(issue.key)} | |||
target="_blank" | |||
/> | |||
</li> | |||
{hasSimilarIssuesFilter && | |||
<li className="issue-meta"> | |||
<button | |||
className="js-issue-filter button-link issue-action issue-action-with-options" | |||
aria-label={translate('issue.filter_similar_issues')} | |||
onClick={props.onFilterClick}> | |||
<i className="icon-filter icon-half-transparent" />{' '} | |||
<i className="icon-dropdown" /> | |||
</button> | |||
</li>} | |||
</ul> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
); | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import SetTransitionPopup from '../popups/SetTransitionPopup'; | |||
import StatusHelper from '../../../components/shared/StatusHelper'; | |||
import { setIssueTransition } from '../../../api/issues'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
hasTransitions: boolean, | |||
isOpen: boolean, | |||
issue: Issue, | |||
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueTransition extends React.PureComponent { | |||
props: Props; | |||
setTransition = (transition: string) => | |||
this.props.setIssueProperty('transition', 'transition', setIssueTransition, transition); | |||
toggleSetTransition = (open?: boolean) => { | |||
this.props.togglePopup('transition', open); | |||
}; | |||
render() { | |||
const { issue } = this.props; | |||
if (this.props.hasTransitions) { | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen && this.props.hasTransitions} | |||
position="bottomleft" | |||
togglePopup={this.toggleSetTransition} | |||
popup={ | |||
<SetTransitionPopup transitions={issue.transitions} onSelect={this.setTransition} /> | |||
}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-transition" | |||
onClick={this.toggleSetTransition}> | |||
<StatusHelper | |||
className="issue-meta-label little-spacer-right" | |||
status={issue.status} | |||
resolution={issue.resolution} | |||
/> | |||
<i className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} else { | |||
return ( | |||
<StatusHelper | |||
className="issue-meta-label" | |||
status={issue.status} | |||
resolution={issue.resolution} | |||
/> | |||
); | |||
} | |||
} | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopupHelper from '../../../components/common/BubblePopupHelper'; | |||
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; | |||
import SetTypePopup from '../popups/SetTypePopup'; | |||
import { setIssueType } from '../../../api/issues'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
canSetSeverity: boolean, | |||
isOpen: boolean, | |||
issue: Issue, | |||
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void, | |||
togglePopup: (string) => void | |||
}; | |||
export default class IssueType extends React.PureComponent { | |||
props: Props; | |||
toggleSetType = (open?: boolean) => { | |||
this.props.togglePopup('set-type', open); | |||
}; | |||
setType = (type: string) => this.props.setIssueProperty('type', 'set-type', setIssueType, type); | |||
render() { | |||
const { issue } = this.props; | |||
if (this.props.canSetSeverity) { | |||
return ( | |||
<BubblePopupHelper | |||
isOpen={this.props.isOpen && this.props.canSetSeverity} | |||
position="bottomleft" | |||
togglePopup={this.toggleSetType} | |||
popup={<SetTypePopup issue={issue} onSelect={this.setType} />}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-type" | |||
onClick={this.toggleSetType}> | |||
<IssueTypeIcon className="little-spacer-right" query={issue.type} /> | |||
{translate('issue.type', issue.type)} | |||
<i className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
); | |||
} else { | |||
return ( | |||
<span> | |||
<IssueTypeIcon className="little-spacer-right" query={issue.type} /> | |||
{translate('issue.type', issue.type)} | |||
</span> | |||
); | |||
} | |||
} | |||
} |
@@ -0,0 +1,75 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueAssign from '../IssueAssign'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
assignee: 'john', | |||
assigneeAvatar: 'gravatarhash', | |||
assigneeName: 'John Doe' | |||
}; | |||
it('should render without the action when the correct rights are missing', () => { | |||
const element = shallow( | |||
<IssueAssign | |||
canAssign={false} | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
onAssign={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with the action', () => { | |||
const element = shallow( | |||
<IssueAssign | |||
canAssign={true} | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
onAssign={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueAssign | |||
canAssign={true} | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
onAssign={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import IssueChangelog from '../IssueChangelog'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
key: 'issuekey', | |||
author: 'john.david.dalton@gmail.com', | |||
creationDate: '2017-03-01T09:36:01+0100' | |||
}; | |||
moment.fn.fromNow = jest.fn(() => 'a month ago'); | |||
it('should render correctly', () => { | |||
const element = shallow( | |||
<IssueChangelog | |||
creationDate="2017-03-01T09:36:01+0100" | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueChangelog | |||
creationDate="2017-03-01T09:36:01+0100" | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueCommentAction from '../IssueCommentAction'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render correctly', () => { | |||
const element = shallow( | |||
<IssueCommentAction | |||
issueKey="issue-key" | |||
currentPopup="" | |||
onFail={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
toggleComment={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueCommentAction | |||
issueKey="issue-key" | |||
currentPopup="" | |||
onFail={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
toggleComment={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls.length).toBe(1); | |||
element.setProps({ currentPopup: 'comment' }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import IssueCommentLine from '../IssueCommentLine'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const comment = { | |||
key: 'comment-key', | |||
authorName: 'John Doe', | |||
authorAvatar: 'gravatarhash', | |||
htmlText: '<b>test</b>', | |||
createdAt: '2017-03-01T09:36:01+0100', | |||
updatable: true | |||
}; | |||
moment.fn.fromNow = jest.fn(() => 'a month ago'); | |||
it('should render correctly a comment that is not updatable', () => { | |||
const element = shallow( | |||
<IssueCommentLine | |||
comment={{ ...comment, updatable: false }} | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render correctly a comment that is updatable', () => { | |||
const element = shallow( | |||
<IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} /> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the right popups when the buttons are clicked', () => { | |||
const element = shallow( | |||
<IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} /> | |||
); | |||
click(element.find('button.js-issue-comment-edit')); | |||
expect(element.state()).toMatchSnapshot(); | |||
click(element.find('button.js-issue-comment-delete')); | |||
expect(element.state()).toMatchSnapshot(); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,33 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueMessage from '../IssueMessage'; | |||
it('should render with the message and a link to open the rule', () => { | |||
const element = shallow( | |||
<IssueMessage | |||
rule="javascript:S1067" | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
organization="myorg" | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,70 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueSeverity from '../IssueSeverity'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
severity: 'BLOCKER' | |||
}; | |||
it('should render without the action when the correct rights are missing', () => { | |||
const element = shallow( | |||
<IssueSeverity | |||
canSetSeverity={false} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with the action', () => { | |||
const element = shallow( | |||
<IssueSeverity | |||
canSetSeverity={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueSeverity | |||
canSetSeverity={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,77 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueTags from '../IssueTags'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
key: 'issuekey', | |||
tags: ['mytag', 'test'] | |||
}; | |||
it('should render without the action when the correct rights are missing', () => { | |||
const element = shallow( | |||
<IssueTags | |||
canSetTags={false} | |||
isOpen={false} | |||
issue={{ | |||
transitions: [], | |||
status: 'CLOSED' | |||
}} | |||
onFail={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with the action', () => { | |||
const element = shallow( | |||
<IssueTags | |||
canSetTags={true} | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueTags | |||
canSetTags={true} | |||
isOpen={false} | |||
issue={issue} | |||
onFail={jest.fn()} | |||
onIssueChange={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueTitleBar from '../IssueTitleBar'; | |||
const issue = { | |||
line: 26, | |||
creationDate: '2017-03-01T09:36:01+0100', | |||
organization: 'myorg', | |||
key: 'AVsae-CQS-9G3txfbFN2', | |||
rule: 'javascript:S1067', | |||
message: 'Reduce the number of conditional operators (4) used in the expression' | |||
}; | |||
it('should render the titlebar correctly', () => { | |||
const element = shallow( | |||
<IssueTitleBar issue={issue} currentPopup="" onFail={jest.fn()} togglePopup={jest.fn()} /> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render the titlebar with the filter', () => { | |||
const element = shallow( | |||
<IssueTitleBar | |||
issue={issue} | |||
currentPopup="" | |||
onFail={jest.fn()} | |||
onFilterClick={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,91 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueTransition from '../IssueTransition'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], | |||
status: 'OPEN' | |||
}; | |||
it('should render without the action when there is no transitions', () => { | |||
const element = shallow( | |||
<IssueTransition | |||
hasTransitions={false} | |||
isOpen={false} | |||
issue={{ | |||
transitions: [], | |||
status: 'CLOSED' | |||
}} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with the action', () => { | |||
const element = shallow( | |||
<IssueTransition | |||
hasTransitions={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with a resolution', () => { | |||
const element = shallow( | |||
<IssueTransition | |||
hasTransitions={true} | |||
isOpen={false} | |||
issue={{ | |||
transitions: ['reopen'], | |||
status: 'RESOLVED', | |||
resolution: 'FIXED' | |||
}} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueTransition | |||
hasTransitions={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,70 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import IssueType from '../IssueType'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
const issue = { | |||
type: 'bug' | |||
}; | |||
it('should render without the action when the correct rights are missing', () => { | |||
const element = shallow( | |||
<IssueType | |||
canSetSeverity={false} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render with the action', () => { | |||
const element = shallow( | |||
<IssueType | |||
canSetSeverity={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={jest.fn()} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should open the popup when the button is clicked', () => { | |||
const toggle = jest.fn(); | |||
const element = shallow( | |||
<IssueType | |||
canSetSeverity={true} | |||
isOpen={false} | |||
issue={issue} | |||
setIssueProperty={jest.fn()} | |||
togglePopup={toggle} | |||
/> | |||
); | |||
click(element.find('button')); | |||
expect(toggle.mock.calls).toMatchSnapshot(); | |||
element.setProps({ isOpen: true }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,111 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"assign", | |||
Object { | |||
"currentTarget": Object { | |||
"blur": [Function], | |||
}, | |||
"preventDefault": [Function], | |||
"stopPropagation": [Function], | |||
"target": Object { | |||
"blur": [Function], | |||
}, | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<SetAssigneePopup | |||
issue={ | |||
Object { | |||
"assignee": "john", | |||
"assigneeAvatar": "gravatarhash", | |||
"assigneeName": "John Doe", | |||
} | |||
} | |||
onFail={[Function]} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-assign" | |||
onClick={[Function]}> | |||
<span> | |||
<span | |||
className="text-top"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
</span> | |||
<span | |||
className="issue-meta-label"> | |||
John Doe | |||
</span> | |||
</span> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with the action 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetAssigneePopup | |||
issue={ | |||
Object { | |||
"assignee": "john", | |||
"assigneeAvatar": "gravatarhash", | |||
"assigneeName": "John Doe", | |||
} | |||
} | |||
onFail={[Function]} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-assign" | |||
onClick={[Function]}> | |||
<span> | |||
<span | |||
className="text-top"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
</span> | |||
<span | |||
className="issue-meta-label"> | |||
John Doe | |||
</span> | |||
</span> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render without the action when the correct rights are missing 1`] = ` | |||
<span> | |||
<span | |||
className="text-top"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
</span> | |||
<span | |||
className="issue-meta-label"> | |||
John Doe | |||
</span> | |||
</span> | |||
`; |
@@ -0,0 +1,68 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"changelog", | |||
undefined, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<ChangelogPopup | |||
issue={ | |||
Object { | |||
"author": "john.david.dalton@gmail.com", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"key": "issuekey", | |||
} | |||
} | |||
onFail={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-show-changelog" | |||
onClick={[Function]} | |||
title="March 1, 2017 9:36 AM"> | |||
<span | |||
className="issue-meta-label"> | |||
a month ago | |||
</span> | |||
<i | |||
className="icon-dropdown little-spacer-left" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render correctly 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<ChangelogPopup | |||
issue={ | |||
Object { | |||
"author": "john.david.dalton@gmail.com", | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"key": "issuekey", | |||
} | |||
} | |||
onFail={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-show-changelog" | |||
onClick={[Function]} | |||
title="March 1, 2017 9:36 AM"> | |||
<span | |||
className="issue-meta-label"> | |||
a month ago | |||
</span> | |||
<i | |||
className="icon-dropdown little-spacer-left" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; |
@@ -0,0 +1,49 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
<li | |||
className="issue-meta"> | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<CommentPopup | |||
customClass="issue-comment-bubble-popup" | |||
onComment={[Function]} | |||
toggleComment={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action js-issue-comment" | |||
onClick={[Function]}> | |||
<span | |||
className="issue-meta-label"> | |||
issue.comment.formlink | |||
</span> | |||
</button> | |||
</BubblePopupHelper> | |||
</li> | |||
`; | |||
exports[`test should render correctly 1`] = ` | |||
<li | |||
className="issue-meta"> | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<CommentPopup | |||
customClass="issue-comment-bubble-popup" | |||
onComment={[Function]} | |||
toggleComment={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action js-issue-comment" | |||
onClick={[Function]}> | |||
<span | |||
className="issue-meta-label"> | |||
issue.comment.formlink | |||
</span> | |||
</button> | |||
</BubblePopupHelper> | |||
</li> | |||
`; |
@@ -0,0 +1,205 @@ | |||
exports[`test should open the right popups when the buttons are clicked 1`] = ` | |||
Object { | |||
"openPopup": "edit", | |||
} | |||
`; | |||
exports[`test should open the right popups when the buttons are clicked 2`] = ` | |||
Object { | |||
"openPopup": "delete", | |||
} | |||
`; | |||
exports[`test should open the right popups when the buttons are clicked 3`] = ` | |||
<div | |||
className="issue-comment"> | |||
<div | |||
className="issue-comment-author" | |||
title="John Doe"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
John Doe | |||
</div> | |||
<div | |||
className="issue-comment-text markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<b>test</b>", | |||
} | |||
} /> | |||
<div | |||
className="issue-comment-age"> | |||
( | |||
a month ago | |||
) | |||
</div> | |||
<div | |||
className="issue-comment-actions"> | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={false} | |||
offset={ | |||
Object { | |||
"horizontal": -6, | |||
"vertical": 0, | |||
} | |||
} | |||
popup={ | |||
<CommentPopup | |||
comment={ | |||
Object { | |||
"authorAvatar": "gravatarhash", | |||
"authorName": "John Doe", | |||
"createdAt": "2017-03-01T09:36:01+0100", | |||
"htmlText": "<b>test</b>", | |||
"key": "comment-key", | |||
"updatable": true, | |||
} | |||
} | |||
customClass="issue-edit-comment-bubble-popup" | |||
onComment={[Function]} | |||
placeholder="" | |||
toggleComment={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-comment-edit button-link icon-edit icon-half-transparent" | |||
onClick={[Function]} /> | |||
</BubblePopupHelper> | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={true} | |||
offset={ | |||
Object { | |||
"horizontal": -10, | |||
"vertical": 0, | |||
} | |||
} | |||
popup={ | |||
<CommentDeletePopup | |||
onDelete={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-comment-delete button-link icon-delete icon-half-transparent" | |||
onClick={[Function]} /> | |||
</BubblePopupHelper> | |||
</div> | |||
</div> | |||
`; | |||
exports[`test should render correctly a comment that is not updatable 1`] = ` | |||
<div | |||
className="issue-comment"> | |||
<div | |||
className="issue-comment-author" | |||
title="John Doe"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
John Doe | |||
</div> | |||
<div | |||
className="issue-comment-text markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<b>test</b>", | |||
} | |||
} /> | |||
<div | |||
className="issue-comment-age"> | |||
( | |||
a month ago | |||
) | |||
</div> | |||
<div | |||
className="issue-comment-actions" /> | |||
</div> | |||
`; | |||
exports[`test should render correctly a comment that is updatable 1`] = ` | |||
<div | |||
className="issue-comment"> | |||
<div | |||
className="issue-comment-author" | |||
title="John Doe"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
John Doe | |||
</div> | |||
<div | |||
className="issue-comment-text markdown" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "<b>test</b>", | |||
} | |||
} /> | |||
<div | |||
className="issue-comment-age"> | |||
( | |||
a month ago | |||
) | |||
</div> | |||
<div | |||
className="issue-comment-actions"> | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={false} | |||
offset={ | |||
Object { | |||
"horizontal": -6, | |||
"vertical": 0, | |||
} | |||
} | |||
popup={ | |||
<CommentPopup | |||
comment={ | |||
Object { | |||
"authorAvatar": "gravatarhash", | |||
"authorName": "John Doe", | |||
"createdAt": "2017-03-01T09:36:01+0100", | |||
"htmlText": "<b>test</b>", | |||
"key": "comment-key", | |||
"updatable": true, | |||
} | |||
} | |||
customClass="issue-edit-comment-bubble-popup" | |||
onComment={[Function]} | |||
placeholder="" | |||
toggleComment={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-comment-edit button-link icon-edit icon-half-transparent" | |||
onClick={[Function]} /> | |||
</BubblePopupHelper> | |||
<BubblePopupHelper | |||
className="bubble-popup-helper-inline" | |||
isOpen={false} | |||
offset={ | |||
Object { | |||
"horizontal": -10, | |||
"vertical": 0, | |||
} | |||
} | |||
popup={ | |||
<CommentDeletePopup | |||
onDelete={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-comment-delete button-link icon-delete icon-half-transparent" | |||
onClick={[Function]} /> | |||
</BubblePopupHelper> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,10 @@ | |||
exports[`test should render with the message and a link to open the rule 1`] = ` | |||
<div | |||
className="issue-message"> | |||
Reduce the number of conditional operators (4) used in the expression | |||
<button | |||
aria-label="issue.rule_details" | |||
className="button-link issue-rule icon-ellipsis-h little-spacer-left" | |||
onClick={[Function]} /> | |||
</div> | |||
`; |
@@ -0,0 +1,75 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"set-severity", | |||
Object { | |||
"currentTarget": Object { | |||
"blur": [Function], | |||
}, | |||
"preventDefault": [Function], | |||
"stopPropagation": [Function], | |||
"target": Object { | |||
"blur": [Function], | |||
}, | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<SetSeverityPopup | |||
issue={ | |||
Object { | |||
"severity": "BLOCKER", | |||
} | |||
} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-severity" | |||
onClick={[Function]}> | |||
<SeverityHelper | |||
className="issue-meta-label little-spacer-right" | |||
severity="BLOCKER" /> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with the action 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetSeverityPopup | |||
issue={ | |||
Object { | |||
"severity": "BLOCKER", | |||
} | |||
} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-severity" | |||
onClick={[Function]}> | |||
<SeverityHelper | |||
className="issue-meta-label little-spacer-right" | |||
severity="BLOCKER" /> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render without the action when the correct rights are missing 1`] = ` | |||
<SeverityHelper | |||
className="issue-meta-label" | |||
severity="BLOCKER" /> | |||
`; |
@@ -0,0 +1,89 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"edit-tags", | |||
Object { | |||
"currentTarget": Object { | |||
"blur": [Function], | |||
}, | |||
"preventDefault": [Function], | |||
"stopPropagation": [Function], | |||
"target": Object { | |||
"blur": [Function], | |||
}, | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<SetIssueTagsPopup | |||
onFail={[Function]} | |||
selectedTags={ | |||
Array [ | |||
"mytag", | |||
"test", | |||
] | |||
} | |||
setTags={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-edit-tags button-link issue-action issue-action-with-options" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
"mytag", | |||
"test", | |||
] | |||
} /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with the action 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetIssueTagsPopup | |||
onFail={[Function]} | |||
selectedTags={ | |||
Array [ | |||
"mytag", | |||
"test", | |||
] | |||
} | |||
setTags={[Function]} /> | |||
} | |||
position="bottomright" | |||
togglePopup={[Function]}> | |||
<button | |||
className="js-issue-edit-tags button-link issue-action issue-action-with-options" | |||
onClick={[Function]}> | |||
<TagsList | |||
allowUpdate={true} | |||
tags={ | |||
Array [ | |||
"mytag", | |||
"test", | |||
] | |||
} /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render without the action when the correct rights are missing 1`] = ` | |||
<TagsList | |||
allowUpdate={false} | |||
tags={ | |||
Array [ | |||
"issue.no_tag", | |||
] | |||
} /> | |||
`; |
@@ -0,0 +1,124 @@ | |||
exports[`test should render the titlebar correctly 1`] = ` | |||
<table | |||
className="issue-table"> | |||
<tbody> | |||
<tr> | |||
<td> | |||
<IssueMessage | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
organization="myorg" | |||
rule="javascript:S1067" /> | |||
</td> | |||
<td | |||
className="issue-table-meta-cell issue-table-meta-cell-first"> | |||
<ul | |||
className="list-inline issue-meta-list"> | |||
<li | |||
className="issue-meta"> | |||
<IssueChangelog | |||
creationDate="2017-03-01T09:36:01+0100" | |||
isOpen={false} | |||
issue={ | |||
Object { | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"key": "AVsae-CQS-9G3txfbFN2", | |||
"line": 26, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"organization": "myorg", | |||
"rule": "javascript:S1067", | |||
} | |||
} | |||
onFail={[Function]} | |||
togglePopup={[Function]} /> | |||
</li> | |||
<li | |||
className="issue-meta"> | |||
<span | |||
className="issue-meta-label" | |||
title="line_number"> | |||
L | |||
26 | |||
</span> | |||
</li> | |||
<li | |||
className="issue-meta"> | |||
<a | |||
className="js-issue-permalink icon-link" | |||
href="/issues/search#issues=AVsae-CQS-9G3txfbFN2" | |||
target="_blank" /> | |||
</li> | |||
</ul> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
`; | |||
exports[`test should render the titlebar with the filter 1`] = ` | |||
<table | |||
className="issue-table"> | |||
<tbody> | |||
<tr> | |||
<td> | |||
<IssueMessage | |||
message="Reduce the number of conditional operators (4) used in the expression" | |||
organization="myorg" | |||
rule="javascript:S1067" /> | |||
</td> | |||
<td | |||
className="issue-table-meta-cell issue-table-meta-cell-first"> | |||
<ul | |||
className="list-inline issue-meta-list"> | |||
<li | |||
className="issue-meta"> | |||
<IssueChangelog | |||
creationDate="2017-03-01T09:36:01+0100" | |||
isOpen={false} | |||
issue={ | |||
Object { | |||
"creationDate": "2017-03-01T09:36:01+0100", | |||
"key": "AVsae-CQS-9G3txfbFN2", | |||
"line": 26, | |||
"message": "Reduce the number of conditional operators (4) used in the expression", | |||
"organization": "myorg", | |||
"rule": "javascript:S1067", | |||
} | |||
} | |||
onFail={[Function]} | |||
togglePopup={[Function]} /> | |||
</li> | |||
<li | |||
className="issue-meta"> | |||
<span | |||
className="issue-meta-label" | |||
title="line_number"> | |||
L | |||
26 | |||
</span> | |||
</li> | |||
<li | |||
className="issue-meta"> | |||
<a | |||
className="js-issue-permalink icon-link" | |||
href="/issues/search#issues=AVsae-CQS-9G3txfbFN2" | |||
target="_blank" /> | |||
</li> | |||
<li | |||
className="issue-meta"> | |||
<button | |||
aria-label="issue.filter_similar_issues" | |||
className="js-issue-filter button-link issue-action issue-action-with-options" | |||
onClick={[Function]}> | |||
<i | |||
className="icon-filter icon-half-transparent" /> | |||
<i | |||
className="icon-dropdown" /> | |||
</button> | |||
</li> | |||
</ul> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
`; |
@@ -0,0 +1,108 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"transition", | |||
Object { | |||
"currentTarget": Object { | |||
"blur": [Function], | |||
}, | |||
"preventDefault": [Function], | |||
"stopPropagation": [Function], | |||
"target": Object { | |||
"blur": [Function], | |||
}, | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<SetTransitionPopup | |||
onSelect={[Function]} | |||
transitions={ | |||
Array [ | |||
"confirm", | |||
"resolve", | |||
"falsepositive", | |||
"wontfix", | |||
] | |||
} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-transition" | |||
onClick={[Function]}> | |||
<StatusHelper | |||
className="issue-meta-label little-spacer-right" | |||
status="OPEN" /> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with a resolution 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetTransitionPopup | |||
onSelect={[Function]} | |||
transitions={ | |||
Array [ | |||
"reopen", | |||
] | |||
} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-transition" | |||
onClick={[Function]}> | |||
<StatusHelper | |||
className="issue-meta-label little-spacer-right" | |||
resolution="FIXED" | |||
status="RESOLVED" /> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with the action 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetTransitionPopup | |||
onSelect={[Function]} | |||
transitions={ | |||
Array [ | |||
"confirm", | |||
"resolve", | |||
"falsepositive", | |||
"wontfix", | |||
] | |||
} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-transition" | |||
onClick={[Function]}> | |||
<StatusHelper | |||
className="issue-meta-label little-spacer-right" | |||
status="OPEN" /> | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render without the action when there is no transitions 1`] = ` | |||
<StatusHelper | |||
className="issue-meta-label" | |||
status="CLOSED" /> | |||
`; |
@@ -0,0 +1,80 @@ | |||
exports[`test should open the popup when the button is clicked 1`] = ` | |||
Array [ | |||
Array [ | |||
"set-type", | |||
Object { | |||
"currentTarget": Object { | |||
"blur": [Function], | |||
}, | |||
"preventDefault": [Function], | |||
"stopPropagation": [Function], | |||
"target": Object { | |||
"blur": [Function], | |||
}, | |||
}, | |||
], | |||
] | |||
`; | |||
exports[`test should open the popup when the button is clicked 2`] = ` | |||
<BubblePopupHelper | |||
isOpen={true} | |||
popup={ | |||
<SetTypePopup | |||
issue={ | |||
Object { | |||
"type": "bug", | |||
} | |||
} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-type" | |||
onClick={[Function]}> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="bug" /> | |||
issue.type.bug | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render with the action 1`] = ` | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={ | |||
<SetTypePopup | |||
issue={ | |||
Object { | |||
"type": "bug", | |||
} | |||
} | |||
onSelect={[Function]} /> | |||
} | |||
position="bottomleft" | |||
togglePopup={[Function]}> | |||
<button | |||
className="button-link issue-action issue-action-with-options js-issue-set-type" | |||
onClick={[Function]}> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="bug" /> | |||
issue.type.bug | |||
<i | |||
className="little-spacer-left icon-dropdown" /> | |||
</button> | |||
</BubblePopupHelper> | |||
`; | |||
exports[`test should render without the action when the correct rights are missing 1`] = ` | |||
<span> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="bug" /> | |||
issue.type.bug | |||
</span> | |||
`; |
@@ -0,0 +1,116 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import moment from 'moment'; | |||
import { getIssueChangelog } from '../../../api/issues'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import IssueChangelogDiff from '../components/IssueChangelogDiff'; | |||
import type { ChangelogDiff } from '../components/IssueChangelogDiff'; | |||
import type { Issue } from '../types'; | |||
type Changelog = { | |||
avatar?: string, | |||
creationDate: string, | |||
diffs: Array<ChangelogDiff>, | |||
user: string, | |||
userName: string | |||
}; | |||
type Props = { | |||
issue: Issue, | |||
onFail: (Error) => void, | |||
popupPosition?: {} | |||
}; | |||
type State = { | |||
changelogs: Array<Changelog> | |||
}; | |||
export default class ChangelogPopup extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
changelogs: [] | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.loadChangelog(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
loadChangelog() { | |||
getIssueChangelog(this.props.issue.key).then( | |||
changelogs => { | |||
if (this.mounted) { | |||
this.setState({ changelogs }); | |||
} | |||
}, | |||
this.props.onFail | |||
); | |||
} | |||
render() { | |||
const { issue } = this.props; | |||
const { author } = issue; | |||
return ( | |||
<BubblePopup position={this.props.popupPosition} customClass="bubble-popup-bottom-right"> | |||
<div className="issue-changelog"> | |||
<table className="spaced"> | |||
<tbody> | |||
<tr> | |||
<td className="thin text-left text-top nowrap"> | |||
{moment(issue.creationDate).format('LLL')} | |||
</td> | |||
<td className="thin text-left text-top nowrap" /> | |||
<td className="text-left text-top"> | |||
{author ? `${translate('created_by')} ${author}` : translate('created')} | |||
</td> | |||
</tr> | |||
{this.state.changelogs.map((item, idx) => ( | |||
<tr key={idx}> | |||
<td className="thin text-left text-top nowrap"> | |||
{moment(item.creationDate).format('LLL')} | |||
</td> | |||
<td className="thin text-left text-top nowrap"> | |||
{item.userName && | |||
item.avatar && | |||
<Avatar className="little-spacer-right" hash={item.avatar} size={16} />} | |||
{item.userName} | |||
</td> | |||
<td className="text-left text-top"> | |||
{item.diffs.map(diff => <IssueChangelogDiff key={diff.key} diff={diff} />)} | |||
</td> | |||
</tr> | |||
))} | |||
</tbody> | |||
</table> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
type Props = { | |||
onDelete: () => void, | |||
popupPosition?: {} | |||
}; | |||
export default function CommentDeletePopup(props: Props) { | |||
return ( | |||
<BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom-right"> | |||
<div className="text-right"> | |||
<div className="spacer-bottom">{translate('issue.comment.delete_confirm_message')}</div> | |||
<button className="button-red" onClick={props.onDelete}>{translate('delete')}</button> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} |
@@ -0,0 +1,113 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import MarkdownTips from '../../../components/common/MarkdownTips'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { IssueComment } from '../types'; | |||
type Props = { | |||
comment?: IssueComment, | |||
customClass?: string, | |||
onComment: (string) => void, | |||
toggleComment: (boolean) => void, | |||
placeholder: string, | |||
popupPosition?: {} | |||
}; | |||
type State = { | |||
textComment: string | |||
}; | |||
export default class CommentPopup extends React.PureComponent { | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
textComment: props.comment ? props.comment.markdown : '' | |||
}; | |||
} | |||
handleCommentChange = (evt: SyntheticInputEvent) => { | |||
this.setState({ textComment: evt.target.value }); | |||
}; | |||
handleCommentClick = () => { | |||
if (this.state.textComment.trim().length > 0) { | |||
this.props.onComment(this.state.textComment); | |||
} | |||
}; | |||
handleCancelClick = (evt: MouseEvent) => { | |||
evt.preventDefault(); | |||
this.props.toggleComment(false); | |||
}; | |||
handleKeyboard = (evt: KeyboardEvent) => { | |||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { | |||
// Ctrl + Enter | |||
this.handleCommentClick(); | |||
} else if ([37, 38, 39, 40].includes(evt.keyCode)) { | |||
// Arrow keys | |||
evt.stopPropagation(); | |||
} | |||
}; | |||
render() { | |||
const { comment } = this.props; | |||
return ( | |||
<BubblePopup | |||
position={this.props.popupPosition} | |||
customClass={classNames(this.props.customClass, 'bubble-popup-bottom-right')}> | |||
<div className="issue-comment-form-text"> | |||
<textarea | |||
autoFocus={true} | |||
placeholder={this.props.placeholder} | |||
onChange={this.handleCommentChange} | |||
onKeyDown={this.handleKeyboard} | |||
value={this.state.textComment} | |||
rows="2" | |||
/> | |||
</div> | |||
<div className="issue-comment-form-footer"> | |||
<div className="issue-comment-form-actions"> | |||
<button | |||
className="js-issue-comment-submit little-spacer-right" | |||
disabled={this.state.textComment.trim().length < 1} | |||
onClick={this.handleCommentClick}> | |||
{comment && translate('save')} | |||
{!comment && translate('issue.comment.submit')} | |||
</button> | |||
<a href="#" className="js-issue-comment-cancel" onClick={this.handleCancelClick}> | |||
{translate('cancel')} | |||
</a> | |||
</div> | |||
<div className="issue-comment-form-tips"> | |||
<MarkdownTips /> | |||
</div> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,167 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import { css } from 'glamor'; | |||
import { debounce, map } from 'lodash'; | |||
import Avatar from '../../../components/ui/Avatar'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import SelectList from '../../../components/common/SelectList'; | |||
import SelectListItem from '../../../components/common/SelectListItem'; | |||
import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore'; | |||
import { areThereCustomOrganizations } from '../../../store/organizations/utils'; | |||
import { searchMembers } from '../../../api/organizations'; | |||
import { searchUsers } from '../../../api/users'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Issue } from '../types'; | |||
type User = { | |||
avatar?: string, | |||
email?: string, | |||
login: string, | |||
name: string | |||
}; | |||
type Props = { | |||
issue: Issue, | |||
onFail: (Error) => void, | |||
onSelect: (string) => void, | |||
popupPosition?: {} | |||
}; | |||
type State = { | |||
query: string, | |||
users: Array<User>, | |||
currentUser: string | |||
}; | |||
const LIST_SIZE = 10; | |||
const USER_MARGIN = css({ marginLeft: '24px' }); | |||
export default class SetAssigneePopup extends React.PureComponent { | |||
defaultUsersArray: Array<User>; | |||
organizationEnabled: boolean; | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.organizationEnabled = areThereCustomOrganizations(); | |||
this.searchUsers = debounce(this.searchUsers, 250); | |||
this.searchMembers = debounce(this.searchMembers, 250); | |||
this.defaultUsersArray = [{ login: '', name: translate('unassigned') }]; | |||
const currentUser = getCurrentUserFromStore(); | |||
if (currentUser != null) { | |||
this.defaultUsersArray = [currentUser, ...this.defaultUsersArray]; | |||
} | |||
this.state = { | |||
query: '', | |||
users: this.defaultUsersArray, | |||
currentUser: currentUser.login | |||
}; | |||
} | |||
searchMembers = (query: string) => { | |||
searchMembers({ | |||
organization: this.props.issue.projectOrganization, | |||
q: query, | |||
ps: LIST_SIZE | |||
}).then(this.handleSearchResult, this.props.onFail); | |||
}; | |||
searchUsers = (query: string) => { | |||
searchUsers(query, LIST_SIZE).then(this.handleSearchResult, this.props.onFail); | |||
}; | |||
handleSearchResult = (data: Object) => { | |||
this.setState({ | |||
users: data.users, | |||
currentUser: data.users.length > 0 ? data.users[0].login : '' | |||
}); | |||
}; | |||
handleSearchChange = (evt: SyntheticInputEvent) => { | |||
const query = evt.target.value; | |||
if (query.length < 2) { | |||
this.setState({ | |||
query, | |||
users: this.defaultUsersArray, | |||
currentUser: this.defaultUsersArray[0].login | |||
}); | |||
} else { | |||
this.setState({ query }); | |||
if (this.organizationEnabled) { | |||
this.searchMembers(query); | |||
} else { | |||
this.searchUsers(query); | |||
} | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<BubblePopup | |||
position={this.props.popupPosition} | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<div className="multi-select"> | |||
<div className="search-box menu-search"> | |||
<button className="search-box-submit button-clean"> | |||
<i className="icon-search-new" /> | |||
</button> | |||
<input | |||
type="search" | |||
value={this.state.query} | |||
className="search-box-input" | |||
placeholder={translate('search_verb')} | |||
onChange={this.handleSearchChange} | |||
autoComplete="off" | |||
autoFocus={true} | |||
/> | |||
</div> | |||
<SelectList | |||
items={map(this.state.users, 'login')} | |||
currentItem={this.state.currentUser} | |||
onSelect={this.props.onSelect}> | |||
{this.state.users.map(user => ( | |||
<SelectListItem key={user.login} item={user.login}> | |||
{(user.avatar || user.email) && | |||
<Avatar | |||
className="spacer-right" | |||
email={user.email} | |||
hash={user.avatar} | |||
size={16} | |||
/>} | |||
<span | |||
className={classNames('vertical-middle', { | |||
[USER_MARGIN]: !(user.avatar || user.email) | |||
})}> | |||
{user.name} | |||
</span> | |||
</SelectListItem> | |||
))} | |||
</SelectList> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,86 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { debounce, without } from 'lodash'; | |||
import TagsSelector from '../../../components/tags/TagsSelector'; | |||
import { searchIssueTags } from '../../../api/issues'; | |||
type Props = { | |||
popupPosition?: {}, | |||
onFail: (Error) => void, | |||
selectedTags: Array<string>, | |||
setTags: (Array<string>) => void | |||
}; | |||
type State = { | |||
searchResult: Array<string> | |||
}; | |||
const LIST_SIZE = 10; | |||
export default class SetIssueTagsPopup extends React.PureComponent { | |||
props: Props; | |||
state: State; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { searchResult: [] }; | |||
this.onSearch = debounce(this.onSearch, 250); | |||
} | |||
componentDidMount() { | |||
this.onSearch(''); | |||
} | |||
onSearch = (query: string) => { | |||
searchIssueTags({ | |||
q: query || '', | |||
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) | |||
}).then( | |||
(tags: Array<string>) => { | |||
this.setState({ searchResult: tags }); | |||
}, | |||
this.props.onFail | |||
); | |||
}; | |||
onSelect = (tag: string) => { | |||
this.props.setTags([...this.props.selectedTags, tag]); | |||
}; | |||
onUnselect = (tag: string) => { | |||
this.props.setTags(without(this.props.selectedTags, tag)); | |||
}; | |||
render() { | |||
return ( | |||
<TagsSelector | |||
position={this.props.popupPosition} | |||
tags={this.state.searchResult} | |||
selectedTags={this.props.selectedTags} | |||
listSize={LIST_SIZE} | |||
onSearch={this.onSearch} | |||
onSelect={this.onSelect} | |||
onUnselect={this.onUnselect} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import SelectList from '../../../components/common/SelectList'; | |||
import SelectListItem from '../../../components/common/SelectListItem'; | |||
import SeverityIcon from '../../../components/shared/SeverityIcon'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
issue: Issue, | |||
onSelect: (string) => void, | |||
popupPosition?: {} | |||
}; | |||
const SEVERITY = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; | |||
export default class SetSeverityPopup extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
return ( | |||
<BubblePopup | |||
position={this.props.popupPosition} | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList | |||
items={SEVERITY} | |||
currentItem={this.props.issue.severity} | |||
onSelect={this.props.onSelect}> | |||
{SEVERITY.map(severity => ( | |||
<SelectListItem key={severity} item={severity}> | |||
<SeverityIcon className="little-spacer-right" severity={severity} /> | |||
{translate('severity', severity)} | |||
</SelectListItem> | |||
))} | |||
</SelectList> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import SelectList from '../../../components/common/SelectList'; | |||
import SelectListItem from '../../../components/common/SelectListItem'; | |||
import { translate } from '../../../helpers/l10n'; | |||
type Props = { | |||
transitions: Array<string>, | |||
onSelect: (string) => void, | |||
popupPosition?: {} | |||
}; | |||
export default class SetTransitionPopup extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
const { transitions } = this.props; | |||
return ( | |||
<BubblePopup | |||
position={this.props.popupPosition} | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList items={transitions} currentItem={transitions[0]} onSelect={this.props.onSelect}> | |||
{transitions.map(transition => { | |||
return ( | |||
<SelectListItem | |||
key={transition} | |||
item={transition} | |||
title={translate('issue.transition', transition, 'description')}> | |||
{translate('issue.transition', transition)} | |||
</SelectListItem> | |||
); | |||
})} | |||
</SelectList> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; | |||
import SelectList from '../../../components/common/SelectList'; | |||
import SelectListItem from '../../../components/common/SelectListItem'; | |||
import type { Issue } from '../types'; | |||
type Props = { | |||
issue: Issue, | |||
onSelect: (string) => void, | |||
popupPosition?: {} | |||
}; | |||
const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; | |||
export default class SetTypePopup extends React.PureComponent { | |||
props: Props; | |||
render() { | |||
return ( | |||
<BubblePopup | |||
position={this.props.popupPosition} | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList | |||
items={TYPES} | |||
currentItem={this.props.issue.type} | |||
onSelect={this.props.onSelect}> | |||
{TYPES.map(type => ( | |||
<SelectListItem key={type} item={type}> | |||
<IssueTypeIcon className="little-spacer-right" query={type} /> | |||
{translate('issue.type', type)} | |||
</SelectListItem> | |||
))} | |||
</SelectList> | |||
</BubblePopup> | |||
); | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import ChangelogPopup from '../ChangelogPopup'; | |||
it('should render the changelog popup correctly', () => { | |||
const element = shallow( | |||
<ChangelogPopup | |||
issue={{ | |||
key: 'issuekey', | |||
author: 'john.david.dalton@gmail.com', | |||
creationDate: '2017-03-01T09:36:01+0100' | |||
}} | |||
onFail={jest.fn()} | |||
/> | |||
); | |||
element.setState({ | |||
changelogs: [ | |||
{ | |||
creationDate: '2017-03-01T09:36:01+0100', | |||
userName: 'john.doe', | |||
avatar: 'gravatarhash', | |||
diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }] | |||
} | |||
] | |||
}); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,31 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import CommentDeletePopup from '../CommentDeletePopup'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render the comment delete popup correctly', () => { | |||
const onDelete = jest.fn(); | |||
const element = shallow(<CommentDeletePopup onDelete={onDelete} />); | |||
expect(element).toMatchSnapshot(); | |||
click(element.find('button')); | |||
expect(onDelete.mock.calls.length).toBe(1); | |||
}); |
@@ -0,0 +1,66 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import CommentPopup from '../CommentPopup'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
it('should render the comment popup correctly without existing comment', () => { | |||
const element = shallow( | |||
<CommentPopup | |||
onComment={jest.fn()} | |||
toggleComment={jest.fn()} | |||
placeholder="placeholder test" | |||
customClass="myclass" | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render the comment popup correctly when changing a comment', () => { | |||
const element = shallow( | |||
<CommentPopup | |||
comment={{ | |||
markdown: '*test*' | |||
}} | |||
onComment={jest.fn()} | |||
toggleComment={jest.fn()} | |||
placeholder="" | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); | |||
it('should render not allow to send comment with only spaces', () => { | |||
const onComment = jest.fn(); | |||
const element = shallow( | |||
<CommentPopup | |||
onComment={onComment} | |||
toggleComment={jest.fn()} | |||
placeholder="placeholder test" | |||
customClass="myclass" | |||
/> | |||
); | |||
click(element.find('button.js-issue-comment-submit')); | |||
expect(onComment.mock.calls.length).toBe(0); | |||
element.setState({ textComment: 'mycomment' }); | |||
click(element.find('button.js-issue-comment-submit')); | |||
expect(onComment.mock.calls.length).toBe(1); | |||
}); |
@@ -17,21 +17,14 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SeverityIcon from './severity-icon'; | |||
import { translate } from '../../helpers/l10n'; | |||
import SetIssueTagsPopup from '../SetIssueTagsPopup'; | |||
export default React.createClass({ | |||
render() { | |||
if (!this.props.severity) { | |||
return null; | |||
} | |||
return ( | |||
<span> | |||
<SeverityIcon severity={this.props.severity} /> | |||
{' '} | |||
{translate('severity', this.props.severity)} | |||
</span> | |||
); | |||
} | |||
it('should render tags popup correctly', () => { | |||
const element = shallow( | |||
<SetIssueTagsPopup onFail={jest.fn()} selectedTags="mytag" setTags={jest.fn()} /> | |||
); | |||
element.setState({ searchResult: ['mytag', 'test', 'second'] }); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,27 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SetSeverityPopup from '../SetSeverityPopup'; | |||
it('should render tags popup correctly', () => { | |||
const element = shallow(<SetSeverityPopup issue={{ severity: 'MAJOR' }} onSelect={jest.fn()} />); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,32 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SetTransitionPopup from '../SetTransitionPopup'; | |||
it('should render tags popup correctly', () => { | |||
const element = shallow( | |||
<SetTransitionPopup | |||
onSelect={jest.fn()} | |||
transitions={['confirm', 'resolve', 'falsepositive', 'wontfix']} | |||
/> | |||
); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,27 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import SetTypePopup from '../SetTypePopup'; | |||
it('should render tags popup correctly', () => { | |||
const element = shallow(<SetTypePopup issue={{ type: 'BUG' }} onSelect={jest.fn()} />); | |||
expect(element).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,50 @@ | |||
exports[`test should render the changelog popup correctly 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom-right"> | |||
<div | |||
className="issue-changelog"> | |||
<table | |||
className="spaced"> | |||
<tbody> | |||
<tr> | |||
<td | |||
className="thin text-left text-top nowrap"> | |||
March 1, 2017 9:36 AM | |||
</td> | |||
<td | |||
className="thin text-left text-top nowrap" /> | |||
<td | |||
className="text-left text-top"> | |||
created_by john.david.dalton@gmail.com | |||
</td> | |||
</tr> | |||
<tr> | |||
<td | |||
className="thin text-left text-top nowrap"> | |||
March 1, 2017 9:36 AM | |||
</td> | |||
<td | |||
className="thin text-left text-top nowrap"> | |||
<Connect(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
size={16} /> | |||
john.doe | |||
</td> | |||
<td | |||
className="text-left text-top"> | |||
<IssueChangelogDiff | |||
diff={ | |||
Object { | |||
"key": "severity", | |||
"newValue": "MINOR", | |||
"oldValue": "CRITICAL", | |||
} | |||
} /> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,17 @@ | |||
exports[`test should render the comment delete popup correctly 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom-right"> | |||
<div | |||
className="text-right"> | |||
<div | |||
className="spacer-bottom"> | |||
issue.comment.delete_confirm_message | |||
</div> | |||
<button | |||
className="button-red" | |||
onClick={[Function]}> | |||
delete | |||
</button> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,75 @@ | |||
exports[`test should render the comment popup correctly when changing a comment 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom-right"> | |||
<div | |||
className="issue-comment-form-text"> | |||
<textarea | |||
autoFocus={true} | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="" | |||
rows="2" | |||
value="*test*" /> | |||
</div> | |||
<div | |||
className="issue-comment-form-footer"> | |||
<div | |||
className="issue-comment-form-actions"> | |||
<button | |||
className="js-issue-comment-submit little-spacer-right" | |||
disabled={false} | |||
onClick={[Function]}> | |||
save | |||
</button> | |||
<a | |||
className="js-issue-comment-cancel" | |||
href="#" | |||
onClick={[Function]}> | |||
cancel | |||
</a> | |||
</div> | |||
<div | |||
className="issue-comment-form-tips"> | |||
<MarkdownTips /> | |||
</div> | |||
</div> | |||
</BubblePopup> | |||
`; | |||
exports[`test should render the comment popup correctly without existing comment 1`] = ` | |||
<BubblePopup | |||
customClass="myclass bubble-popup-bottom-right"> | |||
<div | |||
className="issue-comment-form-text"> | |||
<textarea | |||
autoFocus={true} | |||
onChange={[Function]} | |||
onKeyDown={[Function]} | |||
placeholder="placeholder test" | |||
rows="2" | |||
value="" /> | |||
</div> | |||
<div | |||
className="issue-comment-form-footer"> | |||
<div | |||
className="issue-comment-form-actions"> | |||
<button | |||
className="js-issue-comment-submit little-spacer-right" | |||
disabled={true} | |||
onClick={[Function]}> | |||
issue.comment.submit | |||
</button> | |||
<a | |||
className="js-issue-comment-cancel" | |||
href="#" | |||
onClick={[Function]}> | |||
cancel | |||
</a> | |||
</div> | |||
<div | |||
className="issue-comment-form-tips"> | |||
<MarkdownTips /> | |||
</div> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,15 @@ | |||
exports[`test should render tags popup correctly 1`] = ` | |||
<TagsSelector | |||
listSize={10} | |||
onSearch={[Function]} | |||
onSelect={[Function]} | |||
onUnselect={[Function]} | |||
selectedTags="mytag" | |||
tags={ | |||
Array [ | |||
"mytag", | |||
"test", | |||
"second", | |||
] | |||
} /> | |||
`; |
@@ -0,0 +1,53 @@ | |||
exports[`test should render tags popup correctly 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList | |||
currentItem="MAJOR" | |||
items={ | |||
Array [ | |||
"BLOCKER", | |||
"CRITICAL", | |||
"MAJOR", | |||
"MINOR", | |||
"INFO", | |||
] | |||
} | |||
onSelect={[Function]}> | |||
<SelectListItem | |||
item="BLOCKER"> | |||
<SeverityIcon | |||
className="little-spacer-right" | |||
severity="BLOCKER" /> | |||
severity.BLOCKER | |||
</SelectListItem> | |||
<SelectListItem | |||
item="CRITICAL"> | |||
<SeverityIcon | |||
className="little-spacer-right" | |||
severity="CRITICAL" /> | |||
severity.CRITICAL | |||
</SelectListItem> | |||
<SelectListItem | |||
item="MAJOR"> | |||
<SeverityIcon | |||
className="little-spacer-right" | |||
severity="MAJOR" /> | |||
severity.MAJOR | |||
</SelectListItem> | |||
<SelectListItem | |||
item="MINOR"> | |||
<SeverityIcon | |||
className="little-spacer-right" | |||
severity="MINOR" /> | |||
severity.MINOR | |||
</SelectListItem> | |||
<SelectListItem | |||
item="INFO"> | |||
<SeverityIcon | |||
className="little-spacer-right" | |||
severity="INFO" /> | |||
severity.INFO | |||
</SelectListItem> | |||
</SelectList> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,37 @@ | |||
exports[`test should render tags popup correctly 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList | |||
currentItem="confirm" | |||
items={ | |||
Array [ | |||
"confirm", | |||
"resolve", | |||
"falsepositive", | |||
"wontfix", | |||
] | |||
} | |||
onSelect={[Function]}> | |||
<SelectListItem | |||
item="confirm" | |||
title="issue.transition.confirm.description"> | |||
issue.transition.confirm | |||
</SelectListItem> | |||
<SelectListItem | |||
item="resolve" | |||
title="issue.transition.resolve.description"> | |||
issue.transition.resolve | |||
</SelectListItem> | |||
<SelectListItem | |||
item="falsepositive" | |||
title="issue.transition.falsepositive.description"> | |||
issue.transition.falsepositive | |||
</SelectListItem> | |||
<SelectListItem | |||
item="wontfix" | |||
title="issue.transition.wontfix.description"> | |||
issue.transition.wontfix | |||
</SelectListItem> | |||
</SelectList> | |||
</BubblePopup> | |||
`; |
@@ -0,0 +1,37 @@ | |||
exports[`test should render tags popup correctly 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-menu bubble-popup-bottom"> | |||
<SelectList | |||
currentItem="BUG" | |||
items={ | |||
Array [ | |||
"BUG", | |||
"VULNERABILITY", | |||
"CODE_SMELL", | |||
] | |||
} | |||
onSelect={[Function]}> | |||
<SelectListItem | |||
item="BUG"> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="BUG" /> | |||
issue.type.BUG | |||
</SelectListItem> | |||
<SelectListItem | |||
item="VULNERABILITY"> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="VULNERABILITY" /> | |||
issue.type.VULNERABILITY | |||
</SelectListItem> | |||
<SelectListItem | |||
item="CODE_SMELL"> | |||
<IssueTypeIcon | |||
className="little-spacer-right" | |||
query="CODE_SMELL" /> | |||
issue.type.CODE_SMELL | |||
</SelectListItem> | |||
</SelectList> | |||
</BubblePopup> | |||
`; |
@@ -30,13 +30,44 @@ export type FlowLocation = { | |||
textRange?: TextRange | |||
}; | |||
export type IssueComment = { | |||
author?: string, | |||
authorActive?: boolean, | |||
authorAvatar?: string, | |||
authorLogin?: string, | |||
authorName?: string, | |||
createdAt: string, | |||
htmlText: string, | |||
key: string, | |||
markdown: string, | |||
updatable: boolean | |||
}; | |||
export type Issue = { | |||
actions: Array<string>, | |||
assignee?: string, | |||
assigneeActive?: string, | |||
assigneeAvatar?: string, | |||
assigneeLogin?: string, | |||
assigneeName?: string, | |||
author?: string, | |||
comments?: Array<IssueComment>, | |||
creationDate: string, | |||
effort?: string, | |||
key: string, | |||
flows: Array<{ | |||
locations?: Array<FlowLocation> | |||
}>, | |||
line?: number, | |||
message: string, | |||
organization: string, | |||
projectOrganization: string, | |||
resolution?: string, | |||
rule: string, | |||
severity: string, | |||
textRange: TextRange | |||
status: string, | |||
tags?: Array<string>, | |||
textRange: TextRange, | |||
transitions?: Array<string>, | |||
type: string | |||
}; |
@@ -0,0 +1,36 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import SeverityIcon from './SeverityIcon'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default function SeverityHelper(props: { severity: ?string, className?: string }) { | |||
const { severity } = props; | |||
if (!severity) { | |||
return null; | |||
} | |||
return ( | |||
<span className={props.className}> | |||
<SeverityIcon className="little-spacer-right" severity={severity} /> | |||
{translate('severity', severity)} | |||
</span> | |||
); | |||
} |
@@ -0,0 +1,30 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
export default function SeverityIcon(props: { severity: ?string, className?: string }) { | |||
if (!props.severity) { | |||
return null; | |||
} | |||
const className = classNames('icon-severity-' + props.severity.toLowerCase(), props.className); | |||
return <i className={className} />; | |||
} |
@@ -0,0 +1,37 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import StatusIcon from './StatusIcon'; | |||
import { translate } from '../../helpers/l10n'; | |||
export default function StatusHelper( | |||
props: { resolution?: string, status: string, className?: string } | |||
) { | |||
const resolution = props.resolution != null && | |||
` (${translate('issue.resolution', props.resolution)})`; | |||
return ( | |||
<span className={props.className}> | |||
<StatusIcon className="little-spacer-right" status={props.status} /> | |||
{translate('issue.status', props.status)} | |||
{resolution} | |||
</span> | |||
); | |||
} |
@@ -0,0 +1,27 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
export default function StatusIcon(props: { status: string, className?: string }) { | |||
const className = classNames('icon-status-' + props.status.toLowerCase(), props.className); | |||
return <i className={className} />; | |||
} |
@@ -6,16 +6,11 @@ | |||
font-size: 12px; | |||
} | |||
.tags-list i.icon-dropdown::before { | |||
top: 1px; | |||
} | |||
.tags-list span { | |||
display: inline-block; | |||
vertical-align: text-top; | |||
vertical-align: middle; | |||
text-align: left; | |||
max-width: 220px; | |||
padding-left: 4px; | |||
padding-right: 4px; | |||
margin-top: 2px; | |||
} |
@@ -25,7 +25,6 @@ import './TagsList.css'; | |||
type Props = { | |||
tags: Array<string>, | |||
allowUpdate: boolean, | |||
allowMultiLine: boolean, | |||
customClass?: string | |||
}; | |||
@@ -33,23 +32,19 @@ export default class TagsList extends React.PureComponent { | |||
props: Props; | |||
static defaultProps = { | |||
allowUpdate: false, | |||
allowMultiLine: false | |||
allowUpdate: false | |||
}; | |||
render() { | |||
const { tags, allowUpdate } = this.props; | |||
const spanClass = classNames({ | |||
note: !allowUpdate, | |||
'text-ellipsis': !this.props.allowMultiLine | |||
}); | |||
const spanClass = classNames('text-ellipsis', { note: !allowUpdate }); | |||
const tagListClass = classNames('tags-list', this.props.customClass); | |||
return ( | |||
<span className={tagListClass} title={tags.join(', ')}> | |||
<i className="icon-tags icon-half-transparent" /> | |||
<span className={spanClass}>{tags.join(', ')}</span> | |||
{allowUpdate && <i className="icon-dropdown icon-half-transparent" />} | |||
{allowUpdate && <i className="icon-dropdown" />} | |||
</span> | |||
); | |||
} |
@@ -39,9 +39,9 @@ it('should correctly handle a lot of tags', () => { | |||
for (let i = 0; i < 20; i++) { | |||
lotOfTags.push(tags); | |||
} | |||
const taglist = shallow(<TagsList tags={lotOfTags} allowMultiLine={true} />); | |||
const taglist = shallow(<TagsList tags={lotOfTags} />); | |||
expect(taglist.text()).toBe(lotOfTags.join(', ')); | |||
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(false); | |||
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(true); | |||
}); | |||
it('should render with a caret on the right if update is allowed', () => { |
@@ -23,7 +23,7 @@ import BugIcon from './BugIcon'; | |||
import VulnerabilityIcon from './VulnerabilityIcon'; | |||
import CodeSmellIcon from './CodeSmellIcon'; | |||
export default class IssueTypeIcon extends React.Component { | |||
export default class IssueTypeIcon extends React.PureComponent { | |||
props: { | |||
className?: string, | |||
query: string |
@@ -36,3 +36,5 @@ export const change = (element, value) => | |||
target: { value }, | |||
currentTarget: { value } | |||
}); | |||
export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode }); |
@@ -64,6 +64,15 @@ export function getComponentIssuesUrl(componentKey, query) { | |||
return '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery; | |||
} | |||
/** | |||
* Generate URL for a single issue | |||
* @param {string} issueKey | |||
* @returns {string} | |||
*/ | |||
export function getSingleIssueUrl(issueKey) { | |||
return window.baseUrl + '/issues/search#issues=' + issueKey; | |||
} | |||
/** | |||
* Generate URL for a component's drilldown page | |||
* @param {string} componentKey | |||
@@ -140,3 +149,7 @@ export function getDeprecatedActiveRulesUrl(query = {}, organization?: string) { | |||
export const getProjectsUrl = () => { | |||
return window.baseUrl + '/projects'; | |||
}; | |||
export const getMarkdownHelpUrl = () => { | |||
return window.baseUrl + '/markdown/help'; | |||
}; |
@@ -104,6 +104,18 @@ | |||
overflow: auto; | |||
} | |||
.bubble-popup-helper { | |||
position: relative; | |||
&:focus { | |||
outline: none; | |||
} | |||
} | |||
.bubble-popup-helper-inline { | |||
display: inline-block; | |||
} | |||
.bubble-popup-title { | |||
margin-bottom: 5px; | |||
font-weight: 600; |