From d431d6e9616e0133328fdc7592fad097966cee97 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 27 Dec 2016 14:08:38 +0100 Subject: [PATCH] SONAR-8562 Rewrite issues bulk change --- .../test/java/it/issue/IssueSearchTest.java | 1 - server/sonar-web/src/main/js/api/issues.js | 28 +- .../src/main/js/apps/issues/BulkChangeForm.js | 291 ++++++++++++++++++ .../src/main/js/apps/issues/controller.js | 12 + .../apps/issues/templates/BulkChangeForm.hbs | 143 +++++++++ .../templates/issues-workspace-header.hbs | 2 +- .../js/apps/issues/workspace-header-view.js | 37 +-- .../src/main/less/components/modals.less | 6 + 8 files changed, 490 insertions(+), 30 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js create mode 100644 server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs diff --git a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java index 6d56d27d45b..f5d0852b953 100644 --- a/it/it-tests/src/test/java/it/issue/IssueSearchTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueSearchTest.java @@ -310,7 +310,6 @@ public class IssueSearchTest extends AbstractIssueTest { } @Test - @Ignore("bulk change form is not available yet") public void bulk_change() { runSelenese(ORCHESTRATOR, "/issue/IssueSearchTest/bulk_change.html"); } diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js index 5ac907806fb..a2592a4af6f 100644 --- a/server/sonar-web/src/main/js/api/issues.js +++ b/server/sonar-web/src/main/js/api/issues.js @@ -18,17 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow -import { getJSON } from '../helpers/request'; +import { getJSON, post } from '../helpers/request'; + +export const searchIssues = (query: {}) => ( + getJSON('/api/issues/search', query) +); export function getFacets (query: {}, facets: Array): Promise<*> { - const url = '/api/issues/search'; const data = { ...query, facets: facets.join(), ps: 1, additionalFields: '_all' }; - return getJSON(url, data).then(r => { + return searchIssues(data).then(r => { return { facets: r.facets, response: r }; }); } @@ -62,9 +65,24 @@ export function getAssignees (query: {}): Promise<*> { } export function getIssuesCount (query: {}): Promise<*> { - const url = '/api/issues/search'; const data = { ...query, ps: 1, facetMode: 'effort' }; - return getJSON(url, data).then(r => { + return searchIssues(data).then(r => { return { issues: r.total, debt: r.debtTotal }; }); } + +export const searchIssueTags = (ps: number = 500) => ( + getJSON('/api/issues/tags', { ps }) +); + +export function getIssueFilters () { + const url = '/api/issue_filters/search'; + return getJSON(url).then(r => r.issueFilters); +} + +export const bulkChangeIssues = (issueKeys: Array, query: {}) => ( + post('/api/issues/bulk_change', { + issues: issueKeys.join(), + ...query + }) +); diff --git a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js new file mode 100644 index 00000000000..302033520c8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js @@ -0,0 +1,291 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import sortBy from 'lodash/sortBy'; +import ModalForm from '../../components/common/modal-form'; +import Template from './templates/BulkChangeForm.hbs'; +import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; +import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues'; +import { searchUsers } from '../../api/users'; +import { translate, translateWithParameters } from '../../helpers/l10n'; + +const LIMIT = 500; +const INPUT_WIDTH = '250px'; +const MINIMUM_QUERY_LENGTH = 2; + +type Issue = { + actions?: Array, + assignee: string | null, + transitions?: Array +}; + +const hasAction = (action: string) => (issue: Issue) => ( + issue.actions && issue.actions.includes(action) +); + +export default ModalForm.extend({ + template: Template, + + initialize () { + this.issues = null; + this.paging = null; + this.tags = null; + this.loadIssues(); + this.loadTags(); + }, + + loadIssues () { + const { query } = this.options; + searchIssues({ + ...query, + additionalFields: 'actions,transitions', + ps: LIMIT + }).then(r => { + this.issues = r.issues; + this.paging = r.paging; + this.render(); + }); + }, + + loadTags () { + searchIssueTags().then(r => { + this.tags = r.tags; + this.render(); + }); + }, + + prepareAssigneeSelect () { + const input = this.$('#assignee'); + if (input.length) { + const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues); + const currentUser = getCurrentUserFromStore(); + const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues); + const defaultOptions = []; + if (canBeAssignedToMe && currentUser.isLoggedIn) { + defaultOptions.push({ id: currentUser.login, text: `${currentUser.name} (${currentUser.login})` }); + } + if (canBeUnassigned) { + defaultOptions.push({ id: '', text: translate('unassigned') }); + } + + input.select2({ + allowClear: false, + placeholder: translate('search_verb'), + width: INPUT_WIDTH, + formatNoMatches: () => translate('select2.noMatches'), + formatSearching: () => translate('select2.searching'), + formatInputTooShort: () => translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH), + query: query => { + if (query.term.length === 0) { + query.callback({ results: defaultOptions }); + } else if (query.term.length >= MINIMUM_QUERY_LENGTH) { + searchUsers(query.term).then(r => { + query.callback({ + results: r.users.map(user => ({ + id: user.login, + text: `${user.name} (${user.login})` + })) + }); + }); + } + } + }); + + input.on('change', () => this.$('#assign-action').prop('checked', true)); + } + }, + + prepareTypeSelect () { + this.$('#type').select2({ + minimumResultsForSearch: 999, + width: INPUT_WIDTH + }).on('change', () => this.$('#set-type-action').prop('checked', true)); + }, + + prepareSeveritySelect () { + const format = state => ( + state.id ? + ` ${state.text}` : + state.text + ); + this.$('#severity').select2({ + minimumResultsForSearch: 999, + width: INPUT_WIDTH, + formatResult: format, + formatSelection: format + }).on('change', () => this.$('#set-severity-action').prop('checked', true)); + }, + + prepareTagsInput () { + this.$('#add_tags').select2({ + width: INPUT_WIDTH, + tags: this.tags + }).on('change', () => this.$('#add-tags-action').prop('checked', true)); + + this.$('#remove_tags').select2({ + width: INPUT_WIDTH, + tags: this.tags + }).on('change', () => this.$('#remove-tags-action').prop('checked', true)); + }, + + onRender () { + ModalForm.prototype.onRender.apply(this, arguments); + this.prepareAssigneeSelect(); + this.prepareTypeSelect(); + this.prepareSeveritySelect(); + this.prepareTagsInput(); + }, + + handleErrors (response) { + const message = response['err_msg'] ? translate(response['err_msg']) : translate('default_error_message'); + this.showErrors([{ msg: message }]); + }, + + onFormSubmit () { + ModalForm.prototype.onFormSubmit.apply(this, arguments); + const actions = []; + const query = {}; + + const assignee = this.$('#assignee').val(); + if (this.$('#assign-action').is(':checked') && assignee != null) { + actions.push('assign'); + query['assign'] = assignee; + } + + const type = this.$('#type').val(); + if (this.$('#set-type-action').is(':checked') && type) { + actions.push('set_type'); + query['set_type'] = type; + } + + const severity = this.$('#severity').val(); + if (this.$('#set-severity-action').is(':checked') && severity) { + actions.push('set_severity'); + query['set_severity'] = severity; + } + + const addedTags = this.$('#add_tags').val(); + if (this.$('#add-tags-action').is(':checked') && addedTags) { + actions.push('add_tags'); + query['add_tags'] = addedTags; + } + + const removedTags = this.$('#remove_tags').val(); + if (this.$('#remove-tags-action').is(':checked') && removedTags) { + actions.push('remove_tags'); + query['remove_tags'] = removedTags; + } + + const transition = this.$('[name="do_transition.transition"]:checked').val(); + if (transition) { + actions.push('do_transition'); + query['do_transition'] = transition; + } + + const comment = this.$('#comment').val(); + if (comment) { + actions.push('comment'); + query['comment'] = comment; + } + + const sendNotifications = this.$('#send-notifications').is(':checked'); + if (sendNotifications) { + query['sendNotifications'] = sendNotifications; + } + + this.disableForm(); + this.showSpinner(); + + const issueKeys = this.issues.map(issue => issue.key); + bulkChangeIssues(issueKeys, { ...query, actions: actions.join() }).then( + () => { + this.destroy(); + this.options.onChange(); + }, + (e: Object) => { + this.enableForm(); + this.hideSpinner(); + e.response.json().then(r => this.handleErrors(r)); + } + ); + }, + + canBeAssigned (issues: Array) { + return issues.filter(hasAction('assign')).length; + }, + + canBeAssignedToMe (issues: Array) { + return issues.filter(hasAction('assign_to_me')).length; + }, + + canBeUnassigned (issues: Array) { + return issues.filter(issue => issue.assignee).length; + }, + + canChangeType (issues: Array) { + return issues.filter(hasAction('set_type')).length; + }, + + canChangeSeverity (issues: Array) { + return issues.filter(hasAction('set_severity')).length; + }, + + canChangeTags (issues: Array) { + return issues.filter(hasAction('set_tags')).length; + }, + + canBeCommented (issues: Array) { + return issues.filter(hasAction('comment')).length; + }, + + availableTransitions (issues: Array) { + 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] + })); + }, + + serializeData () { + return { + ...ModalForm.prototype.serializeData.apply(this, arguments), + isLoaded: this.issues != null && this.tags != null, + issues: this.issues, + limitReached: this.paging && this.paging.total > LIMIT, + canBeAssigned: this.issues && this.canBeAssigned(this.issues), + canChangeType: this.issues && this.canChangeType(this.issues), + canChangeSeverity: this.issues && this.canChangeSeverity(this.issues), + canChangeTags: this.issues && this.canChangeTags(this.issues), + canBeCommented: this.issues && this.canBeCommented(this.issues), + availableTransitions: this.issues && this.availableTransitions(this.issues) + }; + } +}); diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js index 92fe0ebc113..9586f4130a7 100644 --- a/server/sonar-web/src/main/js/apps/issues/controller.js +++ b/server/sonar-web/src/main/js/apps/issues/controller.js @@ -130,6 +130,18 @@ export default Controller.extend({ return q; }, + getQueryAsObject () { + const state = this.options.app.state; + const query = state.get('query'); + if (query.assigned_to_me) { + Object.assign(query, { assignees: '__me__' }); + } + if (state.get('isContext')) { + Object.assign(query, state.get('contextQuery')); + } + return query; + }, + getQuery (separator, addContext, handleMyIssues = false) { if (separator == null) { separator = '|'; diff --git a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs b/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs new file mode 100644 index 00000000000..85f568c9615 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs @@ -0,0 +1,143 @@ +{{#if isLoaded}} +
+ + + +
+{{else}} + + + +{{/if}} diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs index 44325865dd0..b95572f1653 100644 --- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs +++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs @@ -60,7 +60,7 @@ {{#if state.canBulkChange}} -