aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/issues.js28
-rw-r--r--server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js291
-rw-r--r--server/sonar-web/src/main/js/apps/issues/controller.js12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs143
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-header-view.js37
-rw-r--r--server/sonar-web/src/main/less/components/modals.less6
7 files changed, 490 insertions, 29 deletions
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<string>): 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<string>, 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<string>,
+ assignee: string | null,
+ transitions?: Array<string>
+};
+
+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 ?
+ `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${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<Issue>) {
+ return issues.filter(hasAction('assign')).length;
+ },
+
+ canBeAssignedToMe (issues: Array<Issue>) {
+ return issues.filter(hasAction('assign_to_me')).length;
+ },
+
+ canBeUnassigned (issues: Array<Issue>) {
+ return issues.filter(issue => issue.assignee).length;
+ },
+
+ canChangeType (issues: Array<Issue>) {
+ return issues.filter(hasAction('set_type')).length;
+ },
+
+ canChangeSeverity (issues: Array<Issue>) {
+ return issues.filter(hasAction('set_severity')).length;
+ },
+
+ canChangeTags (issues: Array<Issue>) {
+ return issues.filter(hasAction('set_tags')).length;
+ },
+
+ canBeCommented (issues: Array<Issue>) {
+ return issues.filter(hasAction('comment')).length;
+ },
+
+ availableTransitions (issues: Array<Issue>) {
+ 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}}
+ <form id="bulk-change-form">
+ <div class="modal-head">
+ <h2>{{tp 'issue_bulk_change.form.title' issues.length}}</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+
+ {{#if limitReached}}
+ <div class="alert alert-warning">
+ {{tp 'issue_bulk_change.max_issues_reached' issues.length}}
+ </div>
+ {{/if}}
+
+ {{! assign }}
+ {{#if canBeAssigned}}
+ <div class="modal-field">
+ <label for="assignee">{{t 'issue.assign.formlink'}}</label>
+ <input id="assign-action" name="actions[]" type="checkbox" value="assign">
+ <input id="assignee" type="hidden">
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' canBeAssigned}})
+ </div>
+ </div>
+ {{/if}}
+
+ {{! type }}
+ {{#if canChangeType}}
+ <div class="modal-field">
+ <label for="type">{{t 'issue.set_type'}}</label>
+ <input id="set-type-action" name="actions[]" type="checkbox" value="set_type">
+ <select id="type" name="set_type.type">
+ <option value="BUG">{{t 'issue.type.BUG'}}</option>
+ <option value="VULNERABILITY">{{t 'issue.type.VULNERABILITY'}}</option>
+ <option value="CODE_SMELL">{{t 'issue.type.CODE_SMELL'}}</option>
+ </select>
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' canChangeType}})
+ </div>
+ </div>
+ {{/if}}
+
+ {{! severity }}
+ {{#if canChangeSeverity}}
+ <div class="modal-field">
+ <label for="severity">{{t 'issue.set_severity'}}</label>
+ <input id="set-severity-action" name="actions[]" type="checkbox" value="set_severity">
+ <select id="severity" name="set_severity.severity">
+ <option value="BLOCKER">{{t 'severity.BLOCKER'}}</option>
+ <option value="CRITICAL">{{t 'severity.CRITICAL'}}</option>
+ <option value="MAJOR">{{t 'severity.MAJOR'}}</option>
+ <option value="MINOR">{{t 'severity.MINOR'}}</option>
+ <option value="INFO">{{t 'severity.INFO'}}</option>
+ </select>
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' canChangeSeverity}})
+ </div>
+ </div>
+ {{/if}}
+
+ {{! add tags }}
+ {{#if canChangeTags}}
+ <div class="modal-field">
+ <label for="add_tags">{{t 'issue.add_tags'}}</label>
+ <input id="add-tags-action" name="actions[]" type="checkbox" value="add_tags">
+ <input id="add_tags" name="add_tags.tags" type="text">
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
+ </div>
+ </div>
+ {{/if}}
+
+ {{! remove tags }}
+ {{#if canChangeTags}}
+ <div class="modal-field">
+ <label for="remove_tags">{{t 'issue.remove_tags'}}</label>
+ <input id="remove-tags-action" name="actions[]" type="checkbox" value="remove_tags">
+ <input id="remove_tags" name="remove_tags.tags" type="text">
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
+ </div>
+ </div>
+ {{/if}}
+
+ {{! transitions }}
+ {{#notEmpty availableTransitions}}
+ <div class="modal-field">
+ <label>{{t 'issue.transition'}}</label>
+ {{#each availableTransitions}}
+ <input type="radio" id="transition-{{transition}}" name="do_transition.transition"
+ value="{{transition}}">
+ <label for="transition-{{transition}}" style="float: none; display: inline; left: 0; cursor: pointer;">
+ {{t 'issue.transition' transition}}
+ </label>
+ <div class="pull-right note">
+ ({{tp 'issue_bulk_change.x_issues' count}})
+ </div>
+ <br>
+ {{/each}}
+ </div>
+ {{/notEmpty}}
+
+ {{! comment }}
+ {{#if canBeCommented}}
+ <div class="modal-field">
+ <label for="comment">
+ {{t 'issue.comment.formlink'}}
+ <i class="icon-help" title="{{t 'issue_bulk_change.comment.help'}}"></i>
+ </label>
+ <div>
+ <textarea rows="4" name="comment" id="comment" style="width: 100%"></textarea>
+ </div>
+ <div class="pull-right">
+ {{> ../../../components/common/templates/_markdown-tips}}
+ </div>
+ </div>
+ {{/if}}
+
+ {{! notifications }}
+ <div class="modal-field">
+ <label for="send-notifications">{{t 'issue.send_notifications'}}</label>
+ <input id="send-notifications" name="sendNotifications" type="checkbox" value="true">
+ </div>
+
+ </div>
+ <div class="modal-foot">
+ <i class="js-modal-spinner spinner spacer-right hidden"></i>
+ <button id="bulk-change-submit">{{t 'apply'}}</button>
+ <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a>
+ </div>
+ </form>
+{{else}}
+ <div class="modal-head">
+ <h2>{{t 'bulk_change'}}</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+ <i class="icon-spinner"></i>
+ </div>
+ <div class="modal-foot">
+ &nbsp;
+ </div>
+{{/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 @@
<button id="issues-reload" class="js-reload">{{t "reload"}}</button>
<button class="js-new-search" id="issues-new-search">{{t "issue_filter.new_search"}}</button>
{{#if state.canBulkChange}}
- <button id="issues-bulk-change" class="dropdown-toggle" data-toggle="dropdown" disabled>
+ <button id="issues-bulk-change" class="dropdown-toggle" data-toggle="dropdown">
{{t "bulk_change"}}
</button>
<ul class="dropdown-menu dropdown-menu-right">
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
index 83123885cdd..ae3d4e613d8 100644
--- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
@@ -17,8 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
import WorkspaceHeaderView from '../../components/navigator/workspace-header-view';
+import BulkChangeForm from './BulkChangeForm';
import Template from './templates/issues-workspace-header.hbs';
export default WorkspaceHeaderView.extend({
@@ -34,17 +34,6 @@ export default WorkspaceHeaderView.extend({
};
},
- initialize () {
- WorkspaceHeaderView.prototype.initialize.apply(this, arguments);
- this._onBulkIssues = window.onBulkIssues;
- window.onBulkIssues = this.afterBulkChange.bind(this);
- },
-
- onDestroy () {
- WorkspaceHeaderView.prototype.onDestroy.apply(this, arguments);
- window.onBulkIssues = this._onBulkIssues;
- },
-
onSelectionClick (e) {
e.preventDefault();
this.toggleSelection();
@@ -56,13 +45,11 @@ export default WorkspaceHeaderView.extend({
},
afterBulkChange () {
- const that = this;
- $('#modal').dialog('close');
const selectedIndex = this.options.app.state.get('selectedIndex');
const selectedKeys = this.options.app.list.where({ selected: true }).map(item => item.id);
this.options.app.controller.fetchList().done(() => {
- that.options.app.state.set({ selectedIndex });
- that.options.app.list.selectByKeys(selectedKeys);
+ this.options.app.state.set({ selectedIndex });
+ this.options.app.list.selectByKeys(selectedKeys);
});
},
@@ -104,17 +91,21 @@ export default WorkspaceHeaderView.extend({
},
bulkChange () {
- const query = this.options.app.controller.getQuery('&', true, true);
- const url = window.baseUrl + '/issues/bulk_change_form?' + query;
- window.openModalWindow(url, {});
+ const query = this.options.app.controller.getQueryAsObject();
+ new BulkChangeForm({
+ query,
+ onChange: () => this.afterBulkChange()
+ }).render();
},
bulkChangeSelected () {
const selected = this.options.app.list.where({ selected: true });
- const selectedKeys = selected.map(item => item.id).slice(0, 200);
- const query = 'issues=' + selectedKeys.join();
- const url = window.baseUrl + '/issues/bulk_change_form?' + query;
- window.openModalWindow(url, {});
+ const selectedKeys = selected.map(item => item.id).slice(0, 500);
+ const query = { issues: selectedKeys.join() };
+ new BulkChangeForm({
+ query,
+ onChange: () => this.afterBulkChange()
+ }).render();
},
serializeData () {
diff --git a/server/sonar-web/src/main/less/components/modals.less b/server/sonar-web/src/main/less/components/modals.less
index bc771a5d25b..3739c4f81fc 100644
--- a/server/sonar-web/src/main/less/components/modals.less
+++ b/server/sonar-web/src/main/less/components/modals.less
@@ -155,6 +155,12 @@ ul.modal-head-metadata li {
margin-bottom: 10px;
}
+.modal-field input[type="radio"],
+.modal-field input[type="checkbox"] {
+ margin-top: 5px;
+ margin-bottom: 4px;
+}
+
.modal-field input[type=text],
.modal-field input[type=email],
.modal-field input[type=password],