diff --git a/app/assets/javascripts/approvers_select.js b/app/assets/javascripts/approvers_select.js
index 656ed95494e5cbc8e3bccfca0f1e3b7feedfc875..03028d9fc1fe9d6a70cdad404591323d76285280 100644
--- a/app/assets/javascripts/approvers_select.js
+++ b/app/assets/javascripts/approvers_select.js
@@ -1,12 +1,41 @@
+import { template } from 'underscore';
import Api from './api';
+const approverItemTemplate = template(`
+
+
+ <% if (isGroup) { %>
+
Group:
+ <% } %>
+
<%- name %>
+ <% if (isGroup) { %>
+
<%- count %>
+ <% } %>
+
+
+
+
+
+`);
+
export default class ApproversSelect {
- constructor() {
+ constructor(page) {
this.$approverSelect = $('.js-select-user-and-group');
+ this.$approversListContainer = $('.js-current-approvers');
+ this.$approversList = $('.approver-list', this.$approversListContainer);
+
const name = this.$approverSelect.data('name');
this.fieldNames = [`${name}[approver_ids]`, `${name}[approver_group_ids]`];
+
+ this.approversField = $(`input[name="${this.fieldNames[0]}"]`);
+ this.approverGroupsField = $(`input[name="${this.fieldNames[1]}"]`);
this.$loadWrapper = $('.load-wrapper');
+ this.isMR = name === 'merge_request';
+ this.isNewMR = this.isMR && page === 'projects:merge_requests:new';
+
this.bindEvents();
this.addEvents();
this.initSelect2();
@@ -20,7 +49,7 @@ export default class ApproversSelect {
addEvents() {
$(document).on('click', '.js-add-approvers', () => this.addApprover());
- $(document).on('click', '.js-approver-remove', e => ApproversSelect.removeApprover(e));
+ $(document).on('click', '.js-approver-remove', e => this.removeApprover(e));
}
static getApprovers(fieldName, approverList) {
@@ -45,14 +74,16 @@ export default class ApproversSelect {
const options = {
skip_users: ApproversSelect.getApprovers(this.fieldNames[0], '.js-approver'),
project_id: $('#project_id').val(),
+ merge_request_id: $('#merge_request_id').val(),
+ approvers: true,
};
return Api.approverUsers(term, options);
}
handleSelectChange(e) {
const { added, removed } = e;
- const userInput = $(`[name="${this.fieldNames[0]}"]`);
- const groupInput = $(`[name="${this.fieldNames[1]}"]`);
+ const userInput = this.approversField;
+ const groupInput = this.approverGroupsField;
if (added) {
if (added.full_name) {
@@ -88,6 +119,7 @@ export default class ApproversSelect {
},
formatResult: ApproversSelect.formatResult,
formatSelection: ApproversSelect.formatSelection,
+ containerCssClass: 'js-approvers-input-container',
dropdownCss() {
const $input = $('.js-select-user-and-group .select2-input');
const offset = $input.offset();
@@ -106,10 +138,25 @@ export default class ApproversSelect {
},
})
.on('change', this.handleSelectChange);
+
+ this.$approversInputContainer = $('.js-approvers-input-container');
}
- static formatSelection(group) {
- return group.full_name || group.name;
+ static formatSelection(approver) {
+ const type = Object.hasOwnProperty.call(approver, 'username') ? 'user' : 'group';
+
+ return `
+
+ ${approver.full_name || approver.name}
+
+ `;
}
static formatResult({
@@ -143,29 +190,30 @@ export default class ApproversSelect {
}
addApprover() {
- this.fieldNames.forEach(ApproversSelect.saveApprovers);
+ this.fieldNames.forEach(this.saveApprovers.bind(this));
}
- static saveApprovers(fieldName) {
+ saveApprovers(fieldName) {
const $input = window.$(`[name="${fieldName}"]`);
const newValue = $input.val();
const $loadWrapper = $('.load-wrapper');
const $approverSelect = $('.js-select-user-and-group');
- if (!newValue) {
- return;
- }
+ if (!newValue) return undefined;
+
+ if (this.isNewMR) return this.addMergeApprover($input);
const $form = $('.js-add-approvers').closest('form');
$loadWrapper.removeClass('hidden');
- window.$.ajax({
+
+ return window.$.ajax({
url: $form.attr('action'),
type: 'POST',
data: {
_method: 'PATCH',
[fieldName]: newValue,
},
- success: ApproversSelect.updateApproverList,
+ success: this.isMR ? this.addMergeApprover.bind(this, $input) : this.updateProjectList,
complete() {
$input.val('');
$approverSelect.select2('val', '');
@@ -177,18 +225,23 @@ export default class ApproversSelect {
});
}
- static removeApprover(e) {
+ removeApprover(e) {
e.preventDefault();
+
const target = e.currentTarget;
+
+ if (this.isNewMR) return this.removeMergeApprover(target);
+
const $loadWrapper = $('.load-wrapper');
$loadWrapper.removeClass('hidden');
- $.ajax({
+
+ return $.ajax({
url: target.getAttribute('href'),
type: 'POST',
data: {
_method: 'DELETE',
},
- success: ApproversSelect.updateApproverList,
+ success: this.isMR ? this.removeMergeApprover.bind(this, target) : this.updateProjectList,
complete: () => $loadWrapper.addClass('hidden'),
error() {
window.Flash('Failed to remove Approver', 'alert');
@@ -196,7 +249,45 @@ export default class ApproversSelect {
});
}
- static updateApproverList(html) {
- $('.js-current-approvers').html($(html).find('.js-current-approvers').html());
+ updateProjectList(html) {
+ const newHtml = $('.js-current-approvers', html).html();
+
+ this.$approversListContainer.html(newHtml);
+ }
+
+ addMergeApprover($input) {
+ const ids = $input.val().split(',');
+ const isGroup = $input.attr('name').contains('approver_group');
+ const selector = isGroup ? '.approver-group' : '.approver-user';
+
+ $(selector, this.$approversInputContainer).forEach((approver) => {
+ const approverItem = approverItemTemplate({
+ id: approver.data('id'),
+ link: approver.data('link'),
+ name: approver.data('name'),
+ count: approver.data('count'),
+ removeLink: approver.data('remove-link'),
+ type: isGroup ? 'group' : 'user',
+ itemClass: isGroup ? 'approver-group' : 'approver',
+ isGroup,
+ });
+
+ this.$approversList.append(approverItem);
+ });
+
+ this.$approverSelect.select2('val', '');
+ }
+
+ removeMergeApprover(target) {
+ const approver = $(target).closest('.approver-list li');
+ const approverID = approver.data('id');
+ const field = approver.hasClass('approver') ? this.approversField : this.approverGroupsField;
+
+ const ids = field.val().split(',');
+ const approverIndex = ids.indexOf(approverID);
+
+ if (approverIndex !== -1) ids.splice(approverIndex, 1);
+ field.val(ids.join(','));
+ approver.remove();
}
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index a9eea0369184516672853e873ed34d3d49f5d9f1..813e8ac072c8872ac53a994065b1619f33c885c2 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -234,6 +234,7 @@ import AuditLogs from './audit_logs';
new MilestoneSelect();
new gl.IssuableTemplateSelectors();
new AutoWidthDropdownSelect($('.js-target-branch-select')).init();
+ new ApproversSelect(page);
break;
case 'projects:tags:new':
new ZenMode();
@@ -499,7 +500,7 @@ import AuditLogs from './audit_logs';
case 'edit':
shortcut_handler = new ShortcutsNavigation();
new ProjectNew();
- new ApproversSelect();
+ new ApproversSelect(page);
break;
case 'new':
new ProjectNew();
diff --git a/app/helpers/ee/merge_requests_helper.rb b/app/helpers/ee/merge_requests_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..900b020e398757ff3c4d66bd56cc2293aac1e224
--- /dev/null
+++ b/app/helpers/ee/merge_requests_helper.rb
@@ -0,0 +1,17 @@
+module EE
+ module MergeRequestsHelper
+ def merge_request_approver_path(project, merge_request, approver)
+ params = {}
+ params[:merge_request_id] = merge_request.iid if merge_request
+
+ case approver
+ when Approver
+ project_approver_path(project, approver, params)
+ when ApproverGroup
+ project_approver_group_path(project, approver, params)
+ else
+ raise TypeError.new('unknown approver type')
+ end
+ end
+ end
+end
diff --git a/app/views/projects/ee/_merge_request_approvals_settings.html.haml b/app/views/projects/ee/_merge_request_approvals_settings.html.haml
index 6fe38d55818b8b6d757fdc58fa3b8b0fc0404823..41b32d2ad779827778cb88325417db77cd875698 100644
--- a/app/views/projects/ee/_merge_request_approvals_settings.html.haml
+++ b/app/views/projects/ee/_merge_request_approvals_settings.html.haml
@@ -1,21 +1,26 @@
- return unless project.feature_available?(:merge_request_approvers)
+- merge_request = local_assigns.fetch(:merge_request, nil)
+- suggested_approvers = local_assigns.fetch(:suggested_approvers, [])
+- model_name = merge_request ? 'merge_request' : 'project'
+- model = merge_request || project
- can_override_approvers = project.can_override_approvers?
-.form-group.reset-approvals-on-push
- .checkbox
- = label_tag :require_approvals do
- = check_box_tag :require_approvals, nil, project.approvals_before_merge.nonzero?, class: 'js-require-approvals-toggle'
- %strong Merge request approvals
- = link_to icon('question-circle'), help_page_path("user/project/merge_requests/merge_request_approvals"), target: '_blank'
+- unless merge_request
+ .form-group.reset-approvals-on-push
+ .checkbox
+ = label_tag :require_approvals do
+ = check_box_tag :require_approvals, nil, project.approvals_before_merge.nonzero?, class: 'js-require-approvals-toggle'
+ %strong Merge request approvals
+ = link_to icon('question-circle'), help_page_path("user/project/merge_requests/merge_request_approvals"), target: '_blank'
.nested-settings{ class: project.approvals_before_merge.nonzero? ? '' : 'hidden' }
.form-group
= form.label :approver_ids, class: 'label-light' do
Approvers
- = hidden_field_tag "project[approver_ids]"
- = hidden_field_tag "project[approver_group_ids]"
+ = hidden_field_tag "#{model_name}[approver_ids]"
+ = hidden_field_tag "#{model_name}[approver_group_ids]"
.input-group.input-btn-group
- = hidden_field_tag :approver_user_and_group_ids, '', { class: 'js-select-user-and-group input-large', tabindex: 1, 'data-name': 'project' }
+ = hidden_field_tag :approver_user_and_group_ids, '', { class: 'js-select-user-and-group input-large', tabindex: 1, 'data-name': model_name }
%button.btn.btn-success.js-add-approvers{ type: 'button', title: 'Add approvers(s)' }
Add
.help-block
@@ -26,9 +31,9 @@
Approvers
%span.badge
- ids = []
- - project.approvers.each do |user|
+ - model.approvers.each do |user|
- ids << user.user_id
- - project.approver_groups.each do |group|
+ - model.approver_groups.each do |group|
- group.users.each do |user|
- unless ids.include?(user.id)
- ids << user.id
@@ -36,13 +41,13 @@
%ul.well-list.approver-list
.load-wrapper.hidden
= icon('spinner spin', class: 'approver-list-loader')
- - project.approvers.each do |approver|
+ - model.approvers.each do |approver|
%li.approver.settings-flex-row.js-approver{ data: { id: approver.user_id } }
= link_to approver.user.name, approver.user
.pull-right
- %button{ href: project_approver_path(project, approver), data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, class: "btn btn-remove js-approver-remove", title: 'Remove approver' }
+ %button{ href: merge_request_approver_path(project, merge_request, approver), data: { confirm: "Are you sure you want to remove approver #{approver.user.name}"}, class: "btn btn-remove js-approver-remove", title: 'Remove approver' }
= icon("trash")
- - project.approver_groups.each do |approver_group|
+ - model.approver_groups.each do |approver_group|
%li.approver-group.settings-flex-row.js-approver-group{ data: { id: approver_group.group.id } }
.span
%span.light
@@ -51,9 +56,9 @@
%span.badge
= approver_group.group.members.count
.pull-right
- %button{ href: project_approver_group_path(project, approver_group), data: { confirm: "Are you sure you want to remove group #{approver_group.group.name}" }, class: "btn btn-remove js-approver-remove", title: 'Remove group' }
+ %button{ href: merge_request_approver_path(project, merge_request, approver_group), data: { confirm: "Are you sure you want to remove group #{approver_group.group.name}" }, class: "btn btn-remove js-approver-remove", title: 'Remove group' }
= icon("trash")
- - if project.approvers.empty? && project.approver_groups.empty?
+ - if model.approvers.empty? && model.approver_groups.empty?
%li There are no approvers
.form-group
@@ -70,9 +75,15 @@
%strong Can override approvers and approvals required per merge request
= link_to icon('question-circle'), help_page_path("user/project/merge_requests/merge_request_approvals", anchor: 'can-override-approvers-and-approvals-required-per-merge-request'), target: '_blank'
+- unless merge_request
.form-group.reset-approvals-on-push
.checkbox
= form.label :reset_approvals_on_push do
= form.check_box :reset_approvals_on_push
%strong Remove all approvals in a merge request when new commits are pushed to its source branch
+- if merge_request
+ .help-block.suggested-approvers
+ - if suggested_approvers.any?
+ Suggested approvers:
+ = raw suggested_approvers.map { |approver| link_to sanitize(approver.name), "#", id: dom_id(approver) }.join(", ")
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 87e7d1b41a5923ddc38e81c770dca479018f21cd..96fe8d65de8b8e8b625b8558dccec0b04197c28e 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -41,7 +41,8 @@
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
-= render 'shared/issuable/approvals', issuable: issuable, form: form
+- if issuable.is_a?(MergeRequest)
+ = render 'projects/ee/merge_request_approvals_settings', project: project, merge_request: issuable, form: form, suggested_approvers: @suggested_approvers
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form