Commit d85d856b authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Tim Zallmann

Approve additionally

parent fb3005fe
---
title: Approve merge requests additionally
merge_request: 4134
author:
type: changed
import { n__, s__, sprintf } from '~/locale';
import Flash from '~/flash'; import Flash from '~/flash';
import MRWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue'; import MRWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
...@@ -44,8 +45,24 @@ export default { ...@@ -44,8 +45,24 @@ export default {
}, },
computed: { computed: {
approvalsRequiredStringified() { approvalsRequiredStringified() {
const baseString = `${this.approvalsLeft} more approval`; let approvedString = s__('MergeRequest|Approved');
return this.approvalsLeft === 1 ? baseString : `${baseString}s`; if (this.approvalsLeft >= 1) {
approvedString = sprintf(n__('mrWidget|Requires 1 more approval by',
'MergeRequest|Requires %d more approvals by', this.approvalsLeft));
}
return approvedString;
},
approveButtonText() {
let approveButtonText = s__('mrWidget|Approve');
if (this.approvalsLeft <= 0) {
approveButtonText = s__('mrWidget|Approve additionally');
}
return approveButtonText;
},
approveButtonClass() {
return {
'btn-inverted': this.showApproveButton && this.approvalsLeft <= 0,
};
}, },
showApproveButton() { showApproveButton() {
return this.userCanApprove && !this.userHasApproved; return this.userCanApprove && !this.userHasApproved;
...@@ -65,7 +82,7 @@ export default { ...@@ -65,7 +82,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.approving = false; this.approving = false;
new Flash('An error occured while submitting your approval.'); // eslint-disable-line Flash(s__('mrWidget|An error occured while submitting your approval.'));
}); });
}, },
}, },
...@@ -75,18 +92,18 @@ export default { ...@@ -75,18 +92,18 @@ export default {
<button <button
:disabled="approving" :disabled="approving"
@click="approveMergeRequest" @click="approveMergeRequest"
class="btn btn-primary btn-sm approve-btn"> class="btn btn-primary btn-sm approve-btn"
:class="approveButtonClass">
<i <i
v-if="approving" v-if="approving"
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true" /> aria-hidden="true" />
Approve {{approveButtonText}}
</button> </button>
</span> </span>
<span class="approvals-required-text bold"> <span class="approvals-required-text bold">
Requires {{approvalsRequiredStringified}} {{approvalsRequiredStringified}}
<span v-if="showSuggestedApprovers"> <span v-if="showSuggestedApprovers">
<span class="dash">&mdash;</span>
<mr-widget-author <mr-widget-author
v-for="approver in suggestedApprovers" v-for="approver in suggestedApprovers"
:key="approver.username" :key="approver.username"
......
import Flash from '~/flash'; import Flash from '~/flash';
import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar'; import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar';
import { s__ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
export default { export default {
...@@ -47,6 +48,12 @@ export default { ...@@ -47,6 +48,12 @@ export default {
const isMerged = this.mr.state === 'merged'; const isMerged = this.mr.state === 'merged';
return this.userHasApproved && !this.userCanApprove && !isMerged; return this.userHasApproved && !this.userCanApprove && !isMerged;
}, },
approvedByText() {
return s__('mrWidget|Approved by');
},
removeApprovalText() {
return s__('mrWidget|Remove your approval');
},
}, },
methods: { methods: {
unapproveMergeRequest() { unapproveMergeRequest() {
...@@ -59,7 +66,7 @@ export default { ...@@ -59,7 +66,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.unapproving = false; this.unapproving = false;
Flash('An error occured while removing your approval.'); Flash(s__('mrWidget|An error occured while removing your approval.'));
}); });
}, },
}, },
...@@ -68,14 +75,14 @@ export default { ...@@ -68,14 +75,14 @@ export default {
v-if="approvedBy.length" v-if="approvedBy.length"
class="approved-by-users approvals-footer clearfix mr-info-list"> class="approved-by-users approvals-footer clearfix mr-info-list">
<div class="approvers-prefix"> <div class="approvers-prefix">
<p>Approved by</p> <p>{{approvedByText}}</p>
<div class="approvers-list"> <div class="approvers-list">
<link-to-member-avatar <link-to-member-avatar
v-for="(approver, index) in approvedBy" v-for="(approver, index) in approvedBy"
:key="index" :key="index"
:avatar-size="20" :avatar-size="20"
:avatar-url="approver.user.avatar_url" :avatar-url="approver.user.avatar_url"
extra-link-class="approver-avatar" extra-link-class="approver-avatar js-approver-list-member"
:display-name="approver.user.name" :display-name="approver.user.name"
:profile-url="approver.user.web_url" :profile-url="approver.user.web_url"
:show-tooltip="true" :show-tooltip="true"
...@@ -99,7 +106,7 @@ export default { ...@@ -99,7 +106,7 @@ export default {
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true"> aria-hidden="true">
</i> </i>
Remove your approval {{removeApprovalText}}
</button> </button>
</div> </div>
</div> </div>
......
import Flash from '~/flash'; import Flash from '~/flash';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import { s__ } from '~/locale';
import ApprovalsBody from './approvals_body'; import ApprovalsBody from './approvals_body';
import ApprovalsFooter from './approvals_footer'; import ApprovalsFooter from './approvals_footer';
...@@ -34,7 +35,7 @@ export default { ...@@ -34,7 +35,7 @@ export default {
}, },
}, },
created() { created() {
const flashErrorMessage = 'An error occured while retrieving approval data for this merge request.'; const flashErrorMessage = s__('mrWidget|An error occured while retrieving approval data for this merge request.');
this.service.fetchApprovals() this.service.fetchApprovals()
.then((data) => { .then((data) => {
......
...@@ -134,11 +134,14 @@ module Approvable ...@@ -134,11 +134,14 @@ module Approvable
approved_by_users.include?(user) approved_by_users.include?(user)
end end
# Once there are fewer approvers left in the list than approvals required, allow other # Once there are fewer approvers left in the list than approvals required or
# project members to approve the MR. # there are no more approvals required
# allow other project members to approve the MR.
# #
def any_approver_allowed? def any_approver_allowed?
approvals_left > approvers_left.count remaining_approvals = approvals_left
remaining_approvals.zero? || remaining_approvals > approvers_left.count
end end
def approved_by_users def approved_by_users
......
...@@ -5,6 +5,7 @@ describe 'User approves a merge request', :js do ...@@ -5,6 +5,7 @@ describe 'User approves a merge request', :js do
let(:project) { create(:project, :repository, approvals_before_merge: 1) } let(:project) { create(:project, :repository, approvals_before_merge: 1) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) }
before do before do
project.add_developer(user) project.add_developer(user)
...@@ -27,6 +28,34 @@ describe 'User approves a merge request', :js do ...@@ -27,6 +28,34 @@ describe 'User approves a merge request', :js do
end end
end end
context 'when a merge request is approved additionally' do
before do
project.add_developer(user2)
project.add_developer(user3)
visit(merge_request_path(merge_request))
end
it 'shows multiple approvers beyond the needed count' do
click_button('Approve')
wait_for_requests
sign_out(user)
sign_in_visit_merge_request(user2)
sign_in_visit_merge_request(user3)
expect(all('.js-approver-list-member').count).to eq(3)
end
def sign_in_visit_merge_request(user)
sign_in(user)
visit(merge_request_path(merge_request))
click_button('Approve')
wait_for_requests
sign_out(user)
end
end
context 'when user cannot approve' do context 'when user cannot approve' do
before do before do
merge_request.approvers.create(user_id: user2.id) merge_request.approvers.create(user_id: user2.id)
......
...@@ -56,7 +56,7 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr ...@@ -56,7 +56,7 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr
describe('Computed properties', function () { describe('Computed properties', function () {
describe('approvalsRequiredStringified', function () { describe('approvalsRequiredStringified', function () {
it('should display the correct string for 1 possible approver', function () { it('should display the correct string for 1 possible approver', function () {
const correctText = '1 more approval'; const correctText = 'Requires 1 more approval by';
expect(this.approvalsBody.approvalsRequiredStringified).toBe(correctText); expect(this.approvalsBody.approvalsRequiredStringified).toBe(correctText);
}); });
...@@ -65,11 +65,20 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr ...@@ -65,11 +65,20 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr
this.approvalsBody.suggestedApprovers.push({ name: 'Approver 2' }); this.approvalsBody.suggestedApprovers.push({ name: 'Approver 2' });
Vue.nextTick(() => { Vue.nextTick(() => {
const correctText = '2 more approvals'; const correctText = 'Requires 2 more approvals by';
expect(this.approvalsBody.approvalsRequiredStringified).toBe(correctText); expect(this.approvalsBody.approvalsRequiredStringified).toBe(correctText);
done(); done();
}); });
}); });
it('shows the "Approved" text message when there is enough approvals in place', function (done) {
this.approvalsBody.approvalsLeft = 0;
Vue.nextTick(() => {
expect(this.approvalsBody.approvalsRequiredStringified).toBe('Approved');
done();
});
});
}); });
describe('showApproveButton', function () { describe('showApproveButton', function () {
...@@ -91,6 +100,30 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr ...@@ -91,6 +100,30 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr
}); });
}); });
}); });
describe('approveButtonText', function () {
it('The approve button should have the "Approve" text', function (done) {
this.approvalsBody.approvalsLeft = 1;
this.approvalsBody.userHasApproved = false;
this.approvalsBody.userCanApprove = true;
Vue.nextTick(() => {
expect(this.approvalsBody.approveButtonText).toBe('Approve');
done();
});
});
it('The approve button should have the "Approve additionally" text', function (done) {
this.approvalsBody.approvalsLeft = 0;
this.approvalsBody.userHasApproved = false;
this.approvalsBody.userCanApprove = true;
Vue.nextTick(() => {
expect(this.approvalsBody.approveButtonText).toBe('Approve additionally');
done();
});
});
});
}); });
}); });
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -1885,6 +1885,7 @@ describe MergeRequest do ...@@ -1885,6 +1885,7 @@ describe MergeRequest do
context 'on a project with several members' do context 'on a project with several members' do
let(:approver_2) { create(:user) } let(:approver_2) { create(:user) }
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:other_developer) { create(:user) }
let(:reporter) { create(:user) } let(:reporter) { create(:user) }
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
...@@ -1893,6 +1894,7 @@ describe MergeRequest do ...@@ -1893,6 +1894,7 @@ describe MergeRequest do
project.add_developer(approver) project.add_developer(approver)
project.add_developer(approver_2) project.add_developer(approver_2)
project.add_developer(developer) project.add_developer(developer)
project.add_developer(other_developer)
project.add_reporter(reporter) project.add_reporter(reporter)
end end
...@@ -2041,6 +2043,36 @@ describe MergeRequest do ...@@ -2041,6 +2043,36 @@ describe MergeRequest do
expect(merge_request.can_approve?(stranger)).to be_falsey expect(merge_request.can_approve?(stranger)).to be_falsey
expect(merge_request.can_approve?(nil)).to be_falsey expect(merge_request.can_approve?(nil)).to be_falsey
end end
context 'when only 1 approval approved' do
it 'only allows the approvers to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
expect(merge_request.can_approve?(developer)).to be_truthy
expect(merge_request.can_approve?(approver)).to be_falsey
expect(merge_request.can_approve?(approver_2)).to be_truthy
expect(merge_request.can_approve?(author)).to be_falsey
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(other_developer)).to be_falsey
expect(merge_request.can_approve?(stranger)).to be_falsey
expect(merge_request.can_approve?(nil)).to be_falsey
end
end
context 'when all approvals received' do
it 'allows anyone with write access except for author to approve the MR' do
create(:approval, user: approver, merge_request: merge_request)
create(:approval, user: approver_2, merge_request: merge_request)
create(:approval, user: developer, merge_request: merge_request)
expect(merge_request.can_approve?(author)).to be_falsey
expect(merge_request.can_approve?(reporter)).to be_falsey
expect(merge_request.can_approve?(other_developer)).to be_truthy
expect(merge_request.can_approve?(stranger)).to be_falsey
expect(merge_request.can_approve?(nil)).to be_falsey
end
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment