Commit a0228bb8 authored by Fatih Acet's avatar Fatih Acet

Merge branch '11159-move-approval-user-password-input-from-inline-to-a-modal' into 'master'

Resolve "Move approval user password input from inline to a modal"

Closes #11159

See merge request gitlab-org/gitlab-ee!14123
parents 51581edf 9de6cf95
...@@ -41,8 +41,8 @@ export default { ...@@ -41,8 +41,8 @@ export default {
isApproving: false, isApproving: false,
isExpanded: false, isExpanded: false,
isLoadingRules: false, isLoadingRules: false,
showApprovalAuth: false,
hasApprovalAuthError: false, hasApprovalAuthError: false,
modalId: 'approvals-auth',
}; };
}, },
computed: { computed: {
...@@ -151,7 +151,7 @@ export default { ...@@ -151,7 +151,7 @@ export default {
}, },
approve() { approve() {
if (this.requirePasswordToApprove) { if (this.requirePasswordToApprove) {
this.showApprovalAuth = true; this.$root.$emit('bv::show::modal', this.modalId);
return; return;
} }
this.updateApproval( this.updateApproval(
...@@ -184,7 +184,7 @@ export default { ...@@ -184,7 +184,7 @@ export default {
.then(data => { .then(data => {
this.mr.setApprovals(data); this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
this.showApprovalAuth = false; this.$root.$emit('bv::hide::modal', this.modalId);
}) })
.catch(errFn) .catch(errFn)
.then(() => { .then(() => {
...@@ -192,10 +192,6 @@ export default { ...@@ -192,10 +192,6 @@ export default {
this.refreshRules(); this.refreshRules();
}); });
}, },
onApprovalAuthCancel() {
this.showApprovalAuth = false;
this.clearError();
},
}, },
FETCH_LOADING, FETCH_LOADING,
}; };
...@@ -207,37 +203,35 @@ export default { ...@@ -207,37 +203,35 @@ export default {
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div> <div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
<template v-else> <template v-else>
<approvals-auth <approvals-auth
v-if="showApprovalAuth"
:is-approving="isApproving" :is-approving="isApproving"
:has-error="hasApprovalAuthError" :has-error="hasApprovalAuthError"
:modal-id="modalId"
@approve="approveWithAuth" @approve="approveWithAuth"
@cancel="onApprovalAuthCancel" @hide="clearError"
/>
<gl-button
v-if="action"
:variant="action.variant"
:class="{ 'btn-inverted': action.inverted }"
size="sm"
class="mr-3"
@click="action.action"
>
<gl-loading-icon v-if="isApproving" inline />
{{ action.text }}
</gl-button>
<approvals-summary-optional
v-if="isOptional"
:can-approve="hasAction"
:help-path="mr.approvalsHelpPath"
/>
<approvals-summary
v-else
:approved="isApproved"
:approvals-left="approvals.approvals_left"
:rules-left="approvals.approvalRuleNamesLeft"
:approvers="approvedBy"
/> />
<template v-else>
<gl-button
v-if="action"
:variant="action.variant"
:class="{ 'btn-inverted': action.inverted }"
size="sm"
class="mr-3"
@click="action.action"
>
<gl-loading-icon v-if="isApproving" inline />
{{ action.text }}
</gl-button>
<approvals-summary-optional
v-if="isOptional"
:can-approve="hasAction"
:help-path="mr.approvalsHelpPath"
/>
<approvals-summary
v-else
:approved="isApproved"
:approvals-left="approvals.approvals_left"
:rules-left="approvals.approvalRuleNamesLeft"
:approvers="approvedBy"
/>
</template>
</template> </template>
</div> </div>
<approvals-footer <approvals-footer
......
<script> <script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlModal } from '@gitlab/ui';
export default { export default {
components: { components: {
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
GlModal,
}, },
props: { props: {
isApproving: { isApproving: {
...@@ -17,63 +18,77 @@ export default { ...@@ -17,63 +18,77 @@ export default {
default: false, default: false,
required: false, required: false,
}, },
modalId: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
approvalPassword: '', approvalPassword: '',
}; };
}, },
mounted() {
this.$nextTick(() => this.$refs.approvalPassword.focus());
},
methods: { methods: {
approve() { approve(event) {
event.preventDefault();
this.$emit('approve', this.approvalPassword); this.$emit('approve', this.approvalPassword);
}, },
cancel() { onHide() {
this.$emit('cancel'); this.approvalPassword = '';
this.$emit('hide');
},
onShow() {
setTimeout(() => {
this.$refs.approvalPasswordInput.focus();
}, 0);
}, },
}, },
}; };
</script> </script>
<template> <template>
<form class="form-inline align-items-center" @submit.prevent="approve"> <gl-modal
<div class="form-group mb-2 mr-2 mb-sm-0"> :modal-id="modalId"
<input :ok-disabled="isApproving"
ref="approvalPassword" :title="s__('Enter your password to approve')"
v-model="approvalPassword" ok-variant="success"
type="password" modal-class="js-mr-approvals-modal"
class="form-control" @ok="approve"
:class="{ 'is-invalid': hasError }" @hide="onHide"
autocomplete="new-password" @show="onShow"
:placeholder="s__('Password')" >
/> <form @submit.prevent="approve">
</div> <p>
<div class="form-group mb-2 mr-2 mb-sm-0"> {{
<gl-button s__(
variant="primary" 'mrWidget|To approve this merge request, please enter your password. This project requires all approvals to be authenticated.',
:disabled="isApproving" )
size="sm" }}
class="mr-1 js-confirm" </p>
@click="approve" <div class="form-group mb-0">
> <label class="mb-1" for="approvalPasswordInput">{{ s__('mrWidget|Your password') }}</label>
<gl-loading-icon v-if="isApproving" inline /> <div>
{{ s__('Confirm') }} <input
</gl-button> id="approvalPasswordInput"
<gl-button ref="approvalPasswordInput"
variant="default" v-model="approvalPassword"
:disabled="isApproving" type="password"
size="sm" class="form-control"
class="js-cancel" :class="{ 'is-invalid': hasError }"
@click="cancel" autocomplete="new-password"
> :placeholder="s__('Password')"
{{ s__('Cancel') }} />
</gl-button> </div>
</div> </div>
<div v-if="hasError"> <div v-if="hasError">
<span class="gl-field-error"> <span class="gl-field-error">{{ s__('mrWidget|Approval password is invalid.') }}</span>
{{ s__('mrWidget|Approval password is invalid.') }} </div>
</span> </form>
</div>
</form> <template slot="modal-cancel">{{ s__('Cancel') }}</template>
<template slot="modal-ok">
<gl-loading-icon v-if="isApproving" inline />
{{ s__('Approve') }}
</template>
</gl-modal>
</template> </template>
---
title: Resolve Move approval user password input from inline to a modal
merge_request: 14123
author:
type: changed
...@@ -16,9 +16,9 @@ describe 'Merge request > User approves with password', :js do ...@@ -16,9 +16,9 @@ describe 'Merge request > User approves with password', :js do
end end
it 'works, when user approves and enters correct password' do it 'works, when user approves and enters correct password' do
page.within('.js-mr-approvals') do approve_with_password '12345678'
approve_with_password '12345678'
page.within('.js-mr-approvals') do
expect(page).not_to have_button('Approve') expect(page).not_to have_button('Approve')
expect(page).to have_text('Approved by') expect(page).to have_text('Approved by')
end end
...@@ -33,9 +33,9 @@ describe 'Merge request > User approves with password', :js do ...@@ -33,9 +33,9 @@ describe 'Merge request > User approves with password', :js do
end end
it 'shows error, when user approves and enters incorrect password' do it 'shows error, when user approves and enters incorrect password' do
page.within('.js-mr-approvals') do approve_with_password 'nottherightpassword'
approve_with_password 'nottherightpassword'
page.within('.js-mr-approvals-modal') do
expect(page).to have_text('Approval password is invalid.') expect(page).to have_text('Approval password is invalid.')
click_button 'Cancel' click_button 'Cancel'
expect(page).not_to have_text('Approved by') expect(page).not_to have_text('Approved by')
...@@ -44,10 +44,15 @@ describe 'Merge request > User approves with password', :js do ...@@ -44,10 +44,15 @@ describe 'Merge request > User approves with password', :js do
end end
def approve_with_password(password) def approve_with_password(password)
click_button('Approve') page.within('.js-mr-approvals') do
fill_in(type: 'password', with: password) click_button('Approve')
click_button('Confirm') end
wait_for_requests
page.within('#approvals-auth') do
fill_in(type: 'password', with: password)
click_button('Approve')
wait_for_requests
end
end end
def unapprove def unapprove
......
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import ApprovalsAuth from 'ee/vue_merge_request_widget/components/approvals/approvals_auth.vue'; import ApprovalsAuth from 'ee/vue_merge_request_widget/components/approvals/approvals_auth.vue';
const TEST_PASSWORD = 'password'; const TEST_PASSWORD = 'password';
...@@ -21,9 +21,9 @@ describe('Approval auth component', () => { ...@@ -21,9 +21,9 @@ describe('Approval auth component', () => {
wrapper = shallowMount(localVue.extend(ApprovalsAuth), { wrapper = shallowMount(localVue.extend(ApprovalsAuth), {
propsData: { propsData: {
...props, ...props,
modalId: 'testid',
}, },
localVue, localVue,
sync: false,
}); });
}; };
...@@ -32,9 +32,6 @@ describe('Approval auth component', () => { ...@@ -32,9 +32,6 @@ describe('Approval auth component', () => {
wrapper = null; wrapper = null;
}); });
const findConfirm = () => wrapper.find('.js-confirm');
const findCancel = () => wrapper.find('.js-cancel');
const findLoading = () => findConfirm().find(GlLoadingIcon);
const findInput = () => wrapper.find('input[type=password]'); const findInput = () => wrapper.find('input[type=password]');
const findErrorMessage = () => wrapper.find('.gl-field-error'); const findErrorMessage = () => wrapper.find('.gl-field-error');
...@@ -44,14 +41,14 @@ describe('Approval auth component', () => { ...@@ -44,14 +41,14 @@ describe('Approval auth component', () => {
waitForTick(done); waitForTick(done);
}); });
it('approve button, cancel button, and password input controls are rendered', () => { it('password input control is rendered', () => {
expect(findConfirm().exists()).toBe(true);
expect(findCancel().exists()).toBe(true);
expect(wrapper.find('input').exists()).toBe(true); expect(wrapper.find('input').exists()).toBe(true);
}); });
it('does not show loading icon', () => { it('does not disable approve button', () => {
expect(findLoading().exists()).toBe(false); const attrs = wrapper.attributes();
expect(attrs['ok-disabled']).toBeUndefined();
}); });
it('does not show error message', () => { it('does not show error message', () => {
...@@ -66,25 +63,15 @@ describe('Approval auth component', () => { ...@@ -66,25 +63,15 @@ describe('Approval auth component', () => {
describe('when approve clicked', () => { describe('when approve clicked', () => {
beforeEach(done => { beforeEach(done => {
createComponent(); createComponent();
findInput().setValue(TEST_PASSWORD);
findConfirm().vm.$emit('click');
waitForTick(done); waitForTick(done);
}); });
it('emits the approve event', () => { it('emits the approve event', done => {
expect(wrapper.emittedByOrder()).toEqual([{ name: 'approve', args: [TEST_PASSWORD] }]); findInput().setValue(TEST_PASSWORD);
}); wrapper.find(GlModal).vm.$emit('ok', { preventDefault: () => null });
});
describe('when cancel is clicked', () => {
beforeEach(done => {
createComponent();
findCancel().vm.$emit('click');
waitForTick(done); waitForTick(done);
});
it('emits the cancel event', () => { expect(wrapper.emittedByOrder()).toEqual([{ name: 'approve', args: [TEST_PASSWORD] }]);
expect(wrapper.emittedByOrder()).toEqual([{ name: 'cancel', args: [] }]);
}); });
}); });
...@@ -94,8 +81,10 @@ describe('Approval auth component', () => { ...@@ -94,8 +81,10 @@ describe('Approval auth component', () => {
waitForTick(done); waitForTick(done);
}); });
it('shows loading icon when isApproving is true', () => { it('disables the approve button', () => {
expect(findLoading().exists()).toBe(true); const attrs = wrapper.attributes();
expect(attrs['ok-disabled']).toEqual('true');
}); });
}); });
......
...@@ -296,20 +296,12 @@ describe('EE MRWidget approvals', () => { ...@@ -296,20 +296,12 @@ describe('EE MRWidget approvals', () => {
waitForTick(done); waitForTick(done);
}); });
it('does not initially show approvals auth component', () => {
expect(wrapper.find(ApprovalsAuth).exists()).toBe(false);
});
describe('when approve is clicked', () => { describe('when approve is clicked', () => {
beforeEach(done => { beforeEach(done => {
findAction().vm.$emit('click'); findAction().vm.$emit('click');
waitForTick(done); waitForTick(done);
}); });
it('shows approvals auth component', () => {
expect(wrapper.find(ApprovalsAuth).exists()).toBe(true);
});
describe('when emits approve', () => { describe('when emits approve', () => {
let authReject; let authReject;
......
...@@ -1548,6 +1548,9 @@ msgstr "" ...@@ -1548,6 +1548,9 @@ msgstr ""
msgid "Approvals" msgid "Approvals"
msgstr "" msgstr ""
msgid "Approve"
msgstr ""
msgid "Apr" msgid "Apr"
msgstr "" msgstr ""
...@@ -5035,6 +5038,9 @@ msgstr "" ...@@ -5035,6 +5038,9 @@ msgstr ""
msgid "Enter the merge request title" msgid "Enter the merge request title"
msgstr "" msgstr ""
msgid "Enter your password to approve"
msgstr ""
msgid "Enter zen mode" msgid "Enter zen mode"
msgstr "" msgstr ""
...@@ -17276,6 +17282,9 @@ msgstr "" ...@@ -17276,6 +17282,9 @@ msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled" msgid "mrWidget|This project is archived, write access has been disabled"
msgstr "" msgstr ""
msgid "mrWidget|To approve this merge request, please enter your password. This project requires all approvals to be authenticated."
msgstr ""
msgid "mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged" msgid "mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
msgstr "" msgstr ""
...@@ -17288,6 +17297,9 @@ msgstr "" ...@@ -17288,6 +17297,9 @@ msgstr ""
msgid "mrWidget|You can merge this merge request manually using the" msgid "mrWidget|You can merge this merge request manually using the"
msgstr "" msgstr ""
msgid "mrWidget|Your password"
msgstr ""
msgid "mrWidget|branch does not exist." msgid "mrWidget|branch does not exist."
msgstr "" msgstr ""
......
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