diff options
103 files changed, 5503 insertions, 197 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js index cb5bfeb951e..912f8b2f6d3 100644 --- a/server/sonar-web/src/main/js/api/issues.js +++ b/server/sonar-web/src/main/js/api/issues.js @@ -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(), diff --git a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js new file mode 100644 index 00000000000..cadec2974e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js index 33152d954f4..6b2509eab90 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap index a92d8b33fb1..b36fabcffcf 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap @@ -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 [ diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js index 1db66ab8553..14b7d37e613 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap index 8eda732fdbb..017cb79cab6 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap @@ -60,7 +60,6 @@ exports[`test should display tags 1`] = ` </Link> </h2> <TagsList - allowMultiLine={false} allowUpdate={false} customClass="spacer-left" tags={ diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js index 5497d9ad0fb..d6f03008f15 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js @@ -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 = { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js index d6f5e10e353..8597dd0ab8c 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js @@ -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); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js index ad79758344a..0e5e292b694 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js @@ -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'; diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js index 41b3b90bb12..d346aa2f480 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js @@ -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( diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js index 78383dac350..daf1785ffd2 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js @@ -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'; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap index a945f7600ad..fecacdb7488 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap @@ -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"> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap index 9279cc173b3..30bbfaa7779 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap @@ -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} /> diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js new file mode 100644 index 00000000000..d298166d630 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js new file mode 100644 index 00000000000..2d83b6aeb24 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.js b/server/sonar-web/src/main/js/components/common/MultiSelect.js index ecc74825395..2faa0f2ef81 100644 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.js +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.js @@ -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]); diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js new file mode 100644 index 00000000000..ba2f82b34b7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/SelectList.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/common/SelectListItem.js b/server/sonar-web/src/main/js/components/common/SelectListItem.js new file mode 100644 index 00000000000..9d432fd7faf --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/SelectListItem.js @@ -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(); + } + } +} diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js new file mode 100644 index 00000000000..d17527119b7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/shared/severity-icon.js b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js index 6c5e5d4a452..366d79dd3fd 100644 --- a/server/sonar-web/src/main/js/components/shared/severity-icon.js +++ b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js new file mode 100644 index 00000000000..9c0e88e6aa3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js @@ -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 +}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js b/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js new file mode 100644 index 00000000000..235be5bfae6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap new file mode 100644 index 00000000000..5892f90b6f8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap new file mode 100644 index 00000000000..864d20d48b4 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap new file mode 100644 index 00000000000..4cf15f469cb --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap new file mode 100644 index 00000000000..4aaf6adfdc8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/controls/Checkbox.js b/server/sonar-web/src/main/js/components/controls/Checkbox.js index 40554f1027d..f5e7289dd45 100644 --- a/server/sonar-web/src/main/js/components/controls/Checkbox.js +++ b/server/sonar-web/src/main/js/components/controls/Checkbox.js @@ -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 }); diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js index c6692cc751e..8d299009cbd 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js @@ -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); +}); diff --git a/server/sonar-web/src/main/js/components/issue/BaseIssue.js b/server/sonar-web/src/main/js/components/issue/BaseIssue.js new file mode 100644 index 00000000000..d4ada02869b --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/BaseIssue.js @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js b/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js index 28be4c7ba4b..67d71fe37cc 100644 --- a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js +++ b/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js @@ -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); diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js index 67f0083f87e..a121bf738d0 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.js +++ b/server/sonar-web/src/main/js/components/issue/Issue.js @@ -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); diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js new file mode 100644 index 00000000000..52ee7e95280 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/IssueView.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/actions.js b/server/sonar-web/src/main/js/components/issue/actions.js new file mode 100644 index 00000000000..a0631c17001 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/actions.js @@ -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])); + } + } + ); + }; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js new file mode 100644 index 00000000000..e60bc87c991 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js new file mode 100644 index 00000000000..836ef6a5a8c --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js @@ -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(); + } + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js new file mode 100644 index 00000000000..864886af959 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js new file mode 100644 index 00000000000..4006a140956 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js @@ -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>; +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js new file mode 100644 index 00000000000..f5f6bf5b8d3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js new file mode 100644 index 00000000000..6e2b9595997 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js new file mode 100644 index 00000000000..ccbd4f1ec89 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js new file mode 100644 index 00000000000..7e52e538742 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js @@ -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} />; + } + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js new file mode 100644 index 00000000000..ab850061b7d --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js @@ -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} + /> + ); + } + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js new file mode 100644 index 00000000000..4f847049f54 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js new file mode 100644 index 00000000000..03cd4e41d86 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js @@ -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} + /> + ); + } + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.js b/server/sonar-web/src/main/js/components/issue/components/IssueType.js new file mode 100644 index 00000000000..df1d8740031 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.js @@ -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> + ); + } + } +} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js new file mode 100644 index 00000000000..9fb88a41d00 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js new file mode 100644 index 00000000000..ca4a95ff08b --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js new file mode 100644 index 00000000000..c8ac8e05986 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js new file mode 100644 index 00000000000..d681183f2c3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js new file mode 100644 index 00000000000..ca0c7f1ad5e --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js new file mode 100644 index 00000000000..528110e91f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js new file mode 100644 index 00000000000..ac17925d7bc --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js new file mode 100644 index 00000000000..3e110b92f36 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js new file mode 100644 index 00000000000..a450a1ed2cc --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js new file mode 100644 index 00000000000..0ae65de665c --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap new file mode 100644 index 00000000000..f2b10d1c0d1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap new file mode 100644 index 00000000000..291bae1a354 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap new file mode 100644 index 00000000000..600ded2a0bd --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap new file mode 100644 index 00000000000..7be3a2fb768 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap new file mode 100644 index 00000000000..208f8ba1a45 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap new file mode 100644 index 00000000000..5703fe176f6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap @@ -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" /> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap new file mode 100644 index 00000000000..c699b819c61 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap @@ -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", + ] + } /> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap new file mode 100644 index 00000000000..f51811bbd0f --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap new file mode 100644 index 00000000000..c03d91d40d1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap @@ -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" /> +`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap new file mode 100644 index 00000000000..4ba45b49706 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js new file mode 100644 index 00000000000..49bdee8218e --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js new file mode 100644 index 00000000000..3e06c95c07d --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js new file mode 100644 index 00000000000..cb079327d53 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js new file mode 100644 index 00000000000..c700c3a341f --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js new file mode 100644 index 00000000000..f8584f59f07 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js new file mode 100644 index 00000000000..bef971615d5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js new file mode 100644 index 00000000000..d1c83099551 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js new file mode 100644 index 00000000000..7bd10d57ede --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js new file mode 100644 index 00000000000..6c4f9d5977e --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js new file mode 100644 index 00000000000..64a42433849 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js @@ -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); +}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js new file mode 100644 index 00000000000..2eac23a4e32 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js @@ -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); +}); diff --git a/server/sonar-web/src/main/js/components/shared/severity-helper.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js index 544afa754d1..c9d1c0a4e23 100644 --- a/server/sonar-web/src/main/js/components/shared/severity-helper.js +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js new file mode 100644 index 00000000000..ed0f67680b8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js new file mode 100644 index 00000000000..4d54d66b413 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js new file mode 100644 index 00000000000..70b7f7a95a6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap new file mode 100644 index 00000000000..aa5c866b59c --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap new file mode 100644 index 00000000000..23e0864e519 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap new file mode 100644 index 00000000000..b4ab112f14f --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap new file mode 100644 index 00000000000..91a5c0b4d6a --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap @@ -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", + ] + } /> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap new file mode 100644 index 00000000000..0c329d01adc --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap new file mode 100644 index 00000000000..08072d269b9 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap new file mode 100644 index 00000000000..a6719f89bf5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js index 9d3982f8f28..690c38146cb 100644 --- a/server/sonar-web/src/main/js/components/issue/types.js +++ b/server/sonar-web/src/main/js/components/issue/types.js @@ -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 }; diff --git a/server/sonar-web/src/main/js/components/shared/SeverityHelper.js b/server/sonar-web/src/main/js/components/shared/SeverityHelper.js new file mode 100644 index 00000000000..2ac4e149f1e --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/SeverityHelper.js @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/shared/SeverityIcon.js b/server/sonar-web/src/main/js/components/shared/SeverityIcon.js new file mode 100644 index 00000000000..aea18812a32 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/SeverityIcon.js @@ -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} />; +} diff --git a/server/sonar-web/src/main/js/components/shared/StatusHelper.js b/server/sonar-web/src/main/js/components/shared/StatusHelper.js new file mode 100644 index 00000000000..092e3524b70 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/StatusHelper.js @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/components/shared/StatusIcon.js b/server/sonar-web/src/main/js/components/shared/StatusIcon.js new file mode 100644 index 00000000000..b98b4cccfd3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/StatusIcon.js @@ -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} />; +} diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.css b/server/sonar-web/src/main/js/components/tags/TagsList.css index 886a83ff628..8271abfeef5 100644 --- a/server/sonar-web/src/main/js/components/tags/TagsList.css +++ b/server/sonar-web/src/main/js/components/tags/TagsList.css @@ -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; } diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.js b/server/sonar-web/src/main/js/components/tags/TagsList.js index 6f5e9d29029..08a3d4c91b1 100644 --- a/server/sonar-web/src/main/js/components/tags/TagsList.js +++ b/server/sonar-web/src/main/js/components/tags/TagsList.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js index 9eec4be8ed2..438730aff86 100644 --- a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js +++ b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js @@ -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', () => { diff --git a/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js b/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js index 95e9b372da0..32365063111 100644 --- a/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js +++ b/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js @@ -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 diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js index 05104c5482a..602eb3c5786 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.js +++ b/server/sonar-web/src/main/js/helpers/testUtils.js @@ -36,3 +36,5 @@ export const change = (element, value) => target: { value }, currentTarget: { value } }); + +export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode }); diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 1d4097f95c7..34b3b06d89d 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -65,6 +65,15 @@ export function getComponentIssuesUrl(componentKey, query) { } /** + * 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 * @param {string} metric @@ -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'; +}; diff --git a/server/sonar-web/src/main/less/components/bubble-popup.less b/server/sonar-web/src/main/less/components/bubble-popup.less index f3ab73adbd1..1986221db05 100644 --- a/server/sonar-web/src/main/less/components/bubble-popup.less +++ b/server/sonar-web/src/main/less/components/bubble-popup.less @@ -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; diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less index 8202664e4cc..658462e620c 100644 --- a/server/sonar-web/src/main/less/components/issues.less +++ b/server/sonar-web/src/main/less/components/issues.less @@ -161,6 +161,7 @@ max-width: 540px; max-height: 320px; overflow: auto; + white-space: normal; } .issue-comments { @@ -242,6 +243,11 @@ input.issue-action-options-search { } } +.issue-edit-comment-bubble-popup { + width: 440px; + font-size: @smallFontSize; +} + .issue-comment-form-text { } diff --git a/server/sonar-web/src/main/less/components/menu.less b/server/sonar-web/src/main/less/components/menu.less index 1d75e2cef98..08ff56e2fc3 100644 --- a/server/sonar-web/src/main/less/components/menu.less +++ b/server/sonar-web/src/main/less/components/menu.less @@ -32,6 +32,10 @@ background-color: #fff; background-clip: padding-box; + &:focus { + outline: none; + } + > li > a, > li > span { display: block; @@ -78,7 +82,7 @@ padding: 4px 16px 0; .search-box-input { font-size: @smallFontSize; } - + .search-box-submit { vertical-align: baseline; } } diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less index bca41f0afcf..2871f7e7908 100644 --- a/server/sonar-web/src/main/less/init/forms.less +++ b/server/sonar-web/src/main/less/init/forms.less @@ -198,11 +198,16 @@ input[type="submit"].button-grey { line-height: inherit; transition: all 0.2s ease; - &:hover, &:active, &:focus { + &:hover, &:focus { background: transparent; color: @blue; } + &:active { + box-shadow: none; + outline: thin dotted #CCC; + } + &:disabled, &:disabled:hover, &:disabled:active, diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5efbb96248a..9804977b607 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -607,6 +607,7 @@ issue.unassign.submit=Unassign issue.assign.to_me=to me issue.comment.formlink=Comment issue.comment.submit=Comment +issue.comment.tell_why=Please tell why? issue.comment.delete_confirm_title=Delete Comment issue.comment.delete_confirm_message=Do you want to delete this comment? issue.comment.delete_confirm_button=Delete |