From 75f9cf94f6f3fcfddc8efb546599cbf61078a245 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 3 Jan 2018 10:21:17 +0100 Subject: Add id to modal.vue to support data-toggle="modal" --- app/assets/javascripts/dispatcher.js | 3 + .../javascripts/groups/components/item_actions.vue | 6 +- .../ide/components/new_dropdown/index.vue | 8 +- .../ide/components/new_dropdown/modal.vue | 8 +- .../ide/components/repo_commit_section.vue | 2 +- .../ide/components/repo_edit_button.vue | 2 +- .../show/components/delete_account_modal.vue | 127 ++++++++++++++++++ .../pages/profiles/accounts/show/index.js | 40 ++++++ .../account/components/delete_account_modal.vue | 146 --------------------- app/assets/javascripts/profile/account/index.js | 21 --- .../javascripts/vue_shared/components/modal.vue | 30 +++-- .../vue_shared/components/recaptcha_modal.vue | 2 +- app/views/profiles/accounts/show.html.haml | 14 +- changelogs/unreleased/winh-modal-target-id.yml | 5 + config/webpack.config.js | 1 - .../groups/components/item_actions_spec.js | 12 +- .../show/components/delete_account_modal_spec.js | 129 ++++++++++++++++++ .../pages/profiles/accounts/show/index_spec.js | 33 +++++ .../components/delete_account_modal_spec.js | 129 ------------------ .../repo/components/new_dropdown/index_spec.js | 13 +- .../vue_shared/components/modal_spec.js | 66 +++++++++- 21 files changed, 450 insertions(+), 347 deletions(-) create mode 100644 app/assets/javascripts/pages/profiles/accounts/show/components/delete_account_modal.vue create mode 100644 app/assets/javascripts/pages/profiles/accounts/show/index.js delete mode 100644 app/assets/javascripts/profile/account/components/delete_account_modal.vue delete mode 100644 app/assets/javascripts/profile/account/index.js create mode 100644 changelogs/unreleased/winh-modal-target-id.yml create mode 100644 spec/javascripts/pages/profiles/accounts/show/components/delete_account_modal_spec.js create mode 100644 spec/javascripts/pages/profiles/accounts/show/index_spec.js delete mode 100644 spec/javascripts/profile/account/components/delete_account_modal_spec.js diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 42f61d33f6e..64090695d53 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -158,6 +158,9 @@ import Activities from './activities'; const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); switch (page) { + case 'profiles:accounts:show': + import('./pages/profiles/accounts/show').then(m => m.default()).catch(fail); + break; case 'profiles:preferences:show': initExperimentalFlags(); break; diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 58ba5aff7cf..b98cfcf7563 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -45,11 +45,9 @@ export default { onLeaveGroup() { this.modalStatus = true; }, - leaveGroup(leaveConfirmed) { + leaveGroup() { this.modalStatus = false; - if (leaveConfirmed) { - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - } + eventHub.$emit('leaveGroup', this.group, this.parentGroup); }, }, }; diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 6e67e99a70f..d475813c4f7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -32,10 +32,10 @@ methods: { createNewItem(type) { this.modalType = type; - this.toggleModalOpen(); + this.openModal = true; }, - toggleModalOpen() { - this.openModal = !this.openModal; + hideModal() { + this.openModal = false; }, }, }; @@ -95,7 +95,7 @@ :branch-id="branch" :path="path" :parent="parent" - @toggle="toggleModalOpen" + @hide="hideModal" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a0650d37690..0312f56efbd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -43,10 +43,10 @@ type: this.type, }); - this.toggleModalOpen(); + this.hideModal(); }, - toggleModalOpen() { - this.$emit('toggle'); + hideModal() { + this.$emit('hide'); }, }, computed: { @@ -86,7 +86,7 @@ :title="modalTitle" :primary-button-label="buttonLabel" kind="success" - @toggle="toggleModalOpen" + @cancel="hideModal" @submit="createEntryInStore" >
diff --git a/app/assets/javascripts/pages/profiles/accounts/show/components/delete_account_modal.vue b/app/assets/javascripts/pages/profiles/accounts/show/components/delete_account_modal.vue new file mode 100644 index 00000000000..1107498c12e --- /dev/null +++ b/app/assets/javascripts/pages/profiles/accounts/show/components/delete_account_modal.vue @@ -0,0 +1,127 @@ + + + diff --git a/app/assets/javascripts/pages/profiles/accounts/show/index.js b/app/assets/javascripts/pages/profiles/accounts/show/index.js new file mode 100644 index 00000000000..2ad89ca6cfa --- /dev/null +++ b/app/assets/javascripts/pages/profiles/accounts/show/index.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import deleteAccountModal from './components/delete_account_modal.vue'; + +Vue.use(Translate); + +export function mountDeleteAccountModal() { + const deleteAccountButton = document.getElementById('delete-account-button'); + if (!deleteAccountButton) { + return null; + } + + const deleteAccountModalEl = document.createElement('div'); + deleteAccountButton.parentNode.appendChild(deleteAccountModalEl); + + return new Vue({ + el: deleteAccountModalEl, + components: { + deleteAccountModal, + }, + mounted() { + deleteAccountButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('delete-account-modal', { + props: { + actionUrl: deleteAccountButton.dataset.actionUrl, + confirmWithPassword: !!deleteAccountButton.dataset.confirmWithPassword, + username: deleteAccountButton.dataset.username, + }, + }); + }, + }); +} + +export default () => { + mountDeleteAccountModal(); +}; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue deleted file mode 100644 index 78be6b6e884..00000000000 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js deleted file mode 100644 index 635056e0eeb..00000000000 --- a/app/assets/javascripts/profile/account/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import Vue from 'vue'; - -import deleteAccountModal from './components/delete_account_modal.vue'; - -const deleteAccountModalEl = document.getElementById('delete-account-modal'); -// eslint-disable-next-line no-new -new Vue({ - el: deleteAccountModalEl, - components: { - deleteAccountModal, - }, - render(createElement) { - return createElement('delete-account-modal', { - props: { - actionUrl: deleteAccountModalEl.dataset.actionUrl, - confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword, - username: deleteAccountModalEl.dataset.username, - }, - }); - }, -}); diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 55f466b7b41..00089dfef38 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -3,6 +3,10 @@ export default { name: 'modal', props: { + id: { + type: String, + required: false, + }, title: { type: String, required: false, @@ -62,11 +66,11 @@ export default { }, methods: { - close() { - this.$emit('toggle', false); + emitCancel(event) { + this.$emit('cancel', event); }, - emitSubmit(status) { - this.$emit('submit', status); + emitSubmit(event) { + this.$emit('submit', event); }, }, }; @@ -75,7 +79,9 @@ export default { diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 8053c65d498..16d60bb2876 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -70,7 +70,7 @@ export default { class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" - @toggle="close" + @cancel="close" >

diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index f1313b79589..ef168b6aff3 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -84,11 +84,14 @@ = s_('Profiles|Deleting an account has the following effects:') = render 'users/deletion_guidance', user: current_user - #delete-account-modal{ data: { action_url: user_registration_path, + %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal', + target: '#delete-account-modal', + action_url: user_registration_path, confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), - username: current_user.username } } - %button.btn.btn-danger.disabled - = s_('Profiles|Delete account') + username: current_user.username }, + type: 'button' } + = s_('Profiles|Delete account') + - else - if @user.solo_owned_groups.present? %p @@ -100,6 +103,3 @@ %p = s_("Profiles|You don't have access to delete this user.") .append-bottom-default - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('account') diff --git a/changelogs/unreleased/winh-modal-target-id.yml b/changelogs/unreleased/winh-modal-target-id.yml new file mode 100644 index 00000000000..f8d5b72be50 --- /dev/null +++ b/changelogs/unreleased/winh-modal-target-id.yml @@ -0,0 +1,5 @@ +--- +title: Add id to modal.vue to support data-toggle="modal" +merge_request: 16189 +author: +type: other diff --git a/config/webpack.config.js b/config/webpack.config.js index 5f95255334c..222c5084abf 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -26,7 +26,6 @@ var config = { }, context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: { - account: './profile/account/index.js', balsamiq_viewer: './blob/balsamiq_viewer.js', blob: './blob_edit/blob_bundle.js', boards: './boards/boards_bundle.js', diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 7a5c1da4d1d..6d6fb410859 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -47,17 +47,11 @@ describe('ItemActionsComponent', () => { it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { spyOn(eventHub, '$emit'); vm.modalStatus = true; - vm.leaveGroup(true); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); - }); - it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { - spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(false); + vm.leaveGroup(); + expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); }); }); }); diff --git a/spec/javascripts/pages/profiles/accounts/show/components/delete_account_modal_spec.js b/spec/javascripts/pages/profiles/accounts/show/components/delete_account_modal_spec.js new file mode 100644 index 00000000000..12ce81719f6 --- /dev/null +++ b/spec/javascripts/pages/profiles/accounts/show/components/delete_account_modal_spec.js @@ -0,0 +1,129 @@ +import Vue from 'vue'; + +import deleteAccountModal from '~/pages/profiles/accounts/show/components/delete_account_modal.vue'; + +import mountComponent from '../../../../../helpers/vue_mount_component_helper'; + +describe('DeleteAccountModal component', () => { + const actionUrl = `${gl.TEST_HOST}/delete/user`; + const username = 'hasnoname'; + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(deleteAccountModal); + }); + + afterEach(() => { + vm.$destroy(); + }); + + const findElements = () => { + const confirmation = vm.confirmWithPassword ? 'password' : 'username'; + return { + form: vm.$refs.form, + input: vm.$el.querySelector(`[name="${confirmation}"]`), + submitButton: vm.$el.querySelector('.btn-danger'), + }; + }; + + describe('with password confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: true, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept empty password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = ''; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with password', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'anything'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredPassword).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('with username confirmation', () => { + beforeEach((done) => { + vm = mountComponent(Component, { + actionUrl, + confirmWithPassword: false, + username, + }); + + vm.isOpen = true; + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not accept wrong username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = 'this is wrong'; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('submits form with correct username', (done) => { + const { form, input, submitButton } = findElements(); + spyOn(form, 'submit'); + input.value = username; + input.dispatchEvent(new Event('input')); + + Vue.nextTick() + .then(() => { + expect(vm.enteredUsername).toBe(input.value); + expect(submitButton).not.toHaveClass('disabled'); + submitButton.click(); + expect(form.submit).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pages/profiles/accounts/show/index_spec.js b/spec/javascripts/pages/profiles/accounts/show/index_spec.js new file mode 100644 index 00000000000..af393893bf5 --- /dev/null +++ b/spec/javascripts/pages/profiles/accounts/show/index_spec.js @@ -0,0 +1,33 @@ +import * as profileAccountModule from '~/pages/profiles/accounts/show'; + +describe('Profile Account bundle', () => { + beforeEach(() => { + setFixtures(` +

+ +
+ `); + }); + + it('mounts the delete account modal', () => { + const container = document.getElementById('container'); + + profileAccountModule.default(); + + expect(container.querySelector('#delete-account-modal')).not.toBeNull(); + }); + + describe('mountDeleteAccountModal', () => { + it('passes the props from delete account button to the modal', () => { + const vm = profileAccountModule.mountDeleteAccountModal(); + const modal = vm.$children[0]; + + expect(modal.actionUrl).toBe('dummy-action-url'); + expect(modal.username).toBe('dummy-username'); + }); + }); +}); diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js deleted file mode 100644 index 2e94948cfb2..00000000000 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ /dev/null @@ -1,129 +0,0 @@ -import Vue from 'vue'; - -import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue'; - -import mountComponent from '../../../helpers/vue_mount_component_helper'; - -describe('DeleteAccountModal component', () => { - const actionUrl = `${gl.TEST_HOST}/delete/user`; - const username = 'hasnoname'; - let Component; - let vm; - - beforeEach(() => { - Component = Vue.extend(deleteAccountModal); - }); - - afterEach(() => { - vm.$destroy(); - }); - - const findElements = () => { - const confirmation = vm.confirmWithPassword ? 'password' : 'username'; - return { - form: vm.$refs.form, - input: vm.$el.querySelector(`[name="${confirmation}"]`), - submitButton: vm.$el.querySelector('.btn-danger'), - }; - }; - - describe('with password confirmation', () => { - beforeEach((done) => { - vm = mountComponent(Component, { - actionUrl, - confirmWithPassword: true, - username, - }); - - vm.isOpen = true; - - Vue.nextTick() - .then(done) - .catch(done.fail); - }); - - it('does not accept empty password', (done) => { - const { form, input, submitButton } = findElements(); - spyOn(form, 'submit'); - input.value = ''; - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); - submitButton.click(); - expect(form.submit).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('submits form with password', (done) => { - const { form, input, submitButton } = findElements(); - spyOn(form, 'submit'); - input.value = 'anything'; - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); - submitButton.click(); - expect(form.submit).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('with username confirmation', () => { - beforeEach((done) => { - vm = mountComponent(Component, { - actionUrl, - confirmWithPassword: false, - username, - }); - - vm.isOpen = true; - - Vue.nextTick() - .then(done) - .catch(done.fail); - }); - - it('does not accept wrong username', (done) => { - const { form, input, submitButton } = findElements(); - spyOn(form, 'submit'); - input.value = 'this is wrong'; - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); - submitButton.click(); - expect(form.submit).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('submits form with correct username', (done) => { - const { form, input, submitButton } = findElements(); - spyOn(form, 'submit'); - input.value = username; - input.dispatchEvent(new Event('input')); - - Vue.nextTick() - .then(() => { - expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); - submitButton.click(); - expect(form.submit).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index b001c1655b4..6efbbf6d75e 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -57,15 +57,16 @@ describe('new dropdown component', () => { }); }); - describe('toggleModalOpen', () => { + describe('hideModal', () => { + beforeAll((done) => { + vm.openModal = true; + Vue.nextTick(done); + }); + it('closes modal after toggling', (done) => { - vm.toggleModalOpen(); + vm.hideModal(); Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }) - .then(vm.toggleModalOpen) .then(() => { expect(vm.$el.querySelector('.modal')).toBeNull(); }) diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 721f4044659..75b67bcc792 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -2,11 +2,69 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; +const modalComponent = Vue.extend(modal); + describe('Modal', () => { - it('does not render a primary button if no primaryButtonLabel', () => { - const modalComponent = Vue.extend(modal); - const vm = mountComponent(modalComponent); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('without primaryButtonLabel', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + primaryButtonLabel: null, + }); + }); + + it('does not render a primary button', () => { + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); + }); + + describe('with id', () => { + it('does not render a primary button', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + id: 'my-modal', + }); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull(); + }); + + it('does not show the modal immediately', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show'); + }); + + it('does not show a backdrop', () => { + expect(vm.$el.querySelector('modal-backdrop')).toBeNull(); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + + + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + const modalElement = vm.$el.querySelector('#my-modal'); + expect(modalElement).not.toHaveClass('in'); + $(modalElement).on('shown.bs.modal', () => { + expect(modalElement).toHaveClass('in'); + done(); + }); - expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + modalButton.click(); + }); }); }); -- cgit v1.2.1