/* * 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 { pickBy, sortBy } from 'lodash'; import SearchSelect from '../../../components/controls/SearchSelect'; import Checkbox from '../../../components/controls/Checkbox'; import Modal from '../../../components/controls/Modal'; import Select, { Creatable } from '../../../components/controls/Select'; import Tooltip from '../../../components/controls/Tooltip'; import MarkdownTips from '../../../components/common/MarkdownTips'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import Avatar from '../../../components/ui/Avatar'; import IssueTypeIcon from '../../../components/ui/IssueTypeIcon'; import throwGlobalError from '../../../app/utils/throwGlobalError'; import { searchIssueTags, bulkChangeIssues } from '../../../api/issues'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { searchAssignees } from '../utils'; /*:: import type { Paging, Component, CurrentUser } from '../utils'; */ /*:: import type { Issue } from '../../../components/issue/types'; */ /*:: type Props = {| component?: Component, currentUser: CurrentUser, fetchIssues: ({}) => Promise<*>, onClose: () => void, onDone: () => void, organization?: { key: string } |}; */ /*:: type State = {| issues: Array, // used for initial loading of issues loading: boolean, paging?: Paging, // used when submitting a form submitting: boolean, tags?: Array, // form fields addTags?: Array, assignee?: string, comment?: string, notifications?: boolean, organization?: string, removeTags?: Array, severity?: string, transition?: string, type?: string |}; */ const hasAction = (action /*: string */) => (issue /*: Issue */) => issue.actions && issue.actions.includes(action); export default class BulkChangeModal extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ /*:: state: State; */ constructor(props /*: Props */) { super(props); let organization = props.component && props.component.organization; if (props.organization && !organization) { organization = props.organization.key; } this.state = { issues: [], loading: true, submitting: false, organization }; } componentDidMount() { this.mounted = true; Promise.all([ this.loadIssues(), searchIssueTags({ organization: this.state.organization }) ]).then(([issues, tags]) => { if (this.mounted) { this.setState({ issues: issues.issues, loading: false, paging: issues.paging, tags }); } }); } componentWillUnmount() { this.mounted = false; } handleCloseClick = (e /*: Event & { target: HTMLElement } */) => { e.preventDefault(); e.target.blur(); this.props.onClose(); }; loadIssues = () => { return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 }); }; handleAssigneeSearch = (query /*: string */) => { if (query.length > 1) { return searchAssignees(query, this.state.organization); } else { const { currentUser } = this.props; const { issues } = this.state; const options = []; if (currentUser.isLoggedIn) { const canBeAssignedToMe = issues.filter(issue => issue.assignee !== currentUser.login).length > 0; if (canBeAssignedToMe) { options.push({ email: currentUser.email, label: currentUser.name, value: currentUser.login }); } } const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0; if (canBeUnassigned) { options.push({ label: translate('unassigned'), value: '' }); } return Promise.resolve(options); } }; handleAssigneeSelect = (assignee /*: string */) => { this.setState({ assignee }); }; handleFieldCheck = (field /*: string */) => (checked /*: boolean */) => { if (!checked) { this.setState({ [field]: undefined }); } else if (field === 'notifications') { this.setState({ [field]: true }); } }; handleFieldChange = (field /*: string */) => (event /*: { target: HTMLInputElement } */) => { this.setState({ [field]: event.target.value }); }; handleSelectFieldChange = (field /*: string */) => ({ value } /*: { value: string } */) => { this.setState({ [field]: value }); }; handleMultiSelectFieldChange = (field /*: string */) => ( options /*: Array<{ value: string }> */ ) => { this.setState({ [field]: options.map(option => option.value) }); }; handleSubmit = (e /*: Event */) => { e.preventDefault(); const query = pickBy( { assign: this.state.assignee, set_type: this.state.type, set_severity: this.state.severity, add_tags: this.state.addTags && this.state.addTags.join(), remove_tags: this.state.removeTags && this.state.removeTags.join(), do_transition: this.state.transition, comment: this.state.comment, sendNotifications: this.state.notifications }, // remove null, but keep empty string x => x != null ); const issueKeys = this.state.issues.map(issue => issue.key); this.setState({ submitting: true }); bulkChangeIssues(issueKeys, query).then( () => { this.setState({ submitting: false }); this.props.onDone(); }, (error /*: Error */) => { this.setState({ submitting: false }); throwGlobalError(error); } ); }; getAvailableTransitions( issues /*: Array */ ) /*: Array<{ transition: string, count: number }> */ { const transitions = {}; issues.forEach(issue => { if (issue.transitions) { issue.transitions.forEach(t => { if (transitions[t] != null) { transitions[t]++; } else { transitions[t] = 1; } }); } }); return sortBy(Object.keys(transitions)).map(transition => ({ transition, count: transitions[transition] })); } renderCancelButton = () => ( {translate('cancel')} ); renderLoading = () => (

{translate('bulk_change')}

{this.renderCancelButton()}
); renderCheckbox = (field /*: string */) => ( ); renderAffected = (affected /*: number */) => (
({translateWithParameters('issue_bulk_change.x_issues', affected)})
); renderField = ( field /*: string */, label /*: string */, affected /*: ?number */, input /*: Object */ ) => (
{this.renderCheckbox(field)} {input} {affected != null && this.renderAffected(affected)}
); renderAssigneeOption = (option /*: { avatar?: string, email?: string, label: string } */) => ( {option.avatar != null && ( )} {option.label} ); renderAssigneeField = () => { const affected /*: number */ = this.state.issues.filter(hasAction('assign')).length; if (affected === 0) { return null; } const input = ( ); return this.renderField('assignee', 'issue.assign.formlink', affected, input); }; renderTypeField = () => { const affected /*: number */ = this.state.issues.filter(hasAction('set_type')).length; if (affected === 0) { return null; } const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; const options = types.map(type => ({ label: translate('issue.type', type), value: type })); const optionRenderer = (option /*: { label: string, value: string } */) => ( {option.label} ); const input = ( } searchable={false} value={this.state.severity} valueRenderer={option => } /> ); return this.renderField('severity', 'issue.set_severity', affected, input); }; renderTagsField = (field /*: string */, label /*: string */, allowCreate /*: boolean */) => { const affected /*: number */ = this.state.issues.filter(hasAction('set_tags')).length; if (this.state.tags == null || affected === 0) { return null; } const Component = allowCreate ? Creatable : Select; const options = [...this.state.tags, ...(this.state[field] || [])].map(tag => ({ label: tag, value: tag })); const input = ( ); return this.renderField(field, label, affected, input); }; renderTransitionsField = () => { const transitions = this.getAvailableTransitions(this.state.issues); if (transitions.length === 0) { return null; } return (
{transitions.map(transition => ( {this.renderAffected(transition.count)}
))}
); }; renderCommentField = () => { const affected /*: number */ = this.state.issues.filter(hasAction('comment')).length; if (affected === 0) { return null; } return (