Commit 8f792928 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '285110-improve-confirm-delete-modal' into 'master'

Update delete user modal manager

See merge request gitlab-org/gitlab!53462
parents f9b63263 1af7065b
...@@ -12,6 +12,10 @@ export default { ...@@ -12,6 +12,10 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
selector: {
required: true,
type: String,
},
}, },
data() { data() {
return { return {
...@@ -34,22 +38,24 @@ export default { ...@@ -34,22 +38,24 @@ export default {
}, },
mounted() { mounted() {
document.addEventListener('click', this.handleClick); /*
}, * Here we're looking for every button that needs to launch a modal
* on click, and then attaching a click event handler to show the modal
* if it's correctly configured.
*
* TODO: Replace this with integrated modal components https://gitlab.com/gitlab-org/gitlab/-/issues/320922
*/
document.querySelectorAll(this.selector).forEach((button) => {
button.addEventListener('click', (e) => {
if (!button.dataset.glModalAction) return;
beforeDestroy() { e.preventDefault();
document.removeEventListener('click', this.handleClick); this.show(button.dataset);
});
});
}, },
methods: { methods: {
handleClick(e) {
const { glModalAction: action } = e.target.dataset;
if (!action) return;
this.show(e.target.dataset);
e.preventDefault();
},
show(modalData) { show(modalData) {
const { glModalAction: requestedAction } = modalData; const { glModalAction: requestedAction } = modalData;
......
...@@ -7,6 +7,7 @@ import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users'; ...@@ -7,6 +7,7 @@ import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
import initTabs from '~/admin/users/tabs'; import initTabs from '~/admin/users/tabs';
import ModalManager from './components/user_modal_manager.vue'; import ModalManager from './components/user_modal_manager.vue';
const CONFIRM_DELETE_BUTTON_SELECTOR = '.js-delete-user-modal-button';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts'; const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal'; const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
...@@ -50,6 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -50,6 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
return h(ModalManager, { return h(ModalManager, {
ref: 'manager', ref: 'manager',
props: { props: {
selector: CONFIRM_DELETE_BUTTON_SELECTOR,
modalConfiguration, modalConfiguration,
csrfToken: csrf.token, csrfToken: csrf.token,
}, },
......
...@@ -59,13 +59,13 @@ ...@@ -59,13 +59,13 @@
%li.divider %li.divider
- if user.can_be_removed? - if user.can_be_removed?
%li %li
%button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete', %button.js-delete-user-modal-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(user), delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } } username: sanitize_name(user.name) } }
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
%li %li
%button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', %button.js-delete-user-modal-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(user, hard_delete: true), delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } } username: sanitize_name(user.name) } }
......
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user = render 'users/deletion_guidance', user: @user
%br %br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete', %button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user), delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } } username: sanitize_name(@user.name) } }
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
the user, and projects in them, will also be removed. Commits the user, and projects in them, will also be removed. Commits
to other projects are unaffected. to other projects are unaffected.
%br %br
%button.delete-user-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions', %button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true), delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: @user.name } } username: @user.name } }
......
...@@ -3,6 +3,8 @@ import UserModalManager from '~/pages/admin/users/components/user_modal_manager. ...@@ -3,6 +3,8 @@ import UserModalManager from '~/pages/admin/users/components/user_modal_manager.
import ModalStub from './stubs/modal_stub'; import ModalStub from './stubs/modal_stub';
describe('Users admin page Modal Manager', () => { describe('Users admin page Modal Manager', () => {
let wrapper;
const modalConfiguration = { const modalConfiguration = {
action1: { action1: {
title: 'action1', title: 'action1',
...@@ -14,11 +16,12 @@ describe('Users admin page Modal Manager', () => { ...@@ -14,11 +16,12 @@ describe('Users admin page Modal Manager', () => {
}, },
}; };
let wrapper; const findModal = () => wrapper.find({ ref: 'modal' });
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = mount(UserModalManager, { wrapper = mount(UserModalManager, {
propsData: { propsData: {
selector: '.js-delete-user-modal-button',
modalConfiguration, modalConfiguration,
csrfToken: 'dummyCSRF', csrfToken: 'dummyCSRF',
...props, ...props,
...@@ -37,7 +40,7 @@ describe('Users admin page Modal Manager', () => { ...@@ -37,7 +40,7 @@ describe('Users admin page Modal Manager', () => {
describe('render behavior', () => { describe('render behavior', () => {
it('does not renders modal when initialized', () => { it('does not renders modal when initialized', () => {
createComponent(); createComponent();
expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy(); expect(findModal().exists()).toBeFalsy();
}); });
it('throws if action has no proper configuration', () => { it('throws if action has no proper configuration', () => {
...@@ -55,7 +58,7 @@ describe('Users admin page Modal Manager', () => { ...@@ -55,7 +58,7 @@ describe('Users admin page Modal Manager', () => {
}); });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' }); const modal = findModal();
expect(modal.exists()).toBeTruthy(); expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF'); expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue'); expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
...@@ -64,68 +67,60 @@ describe('Users admin page Modal Manager', () => { ...@@ -64,68 +67,60 @@ describe('Users admin page Modal Manager', () => {
}); });
}); });
describe('global listener', () => { describe('click handling', () => {
let button;
let button2;
const createButtons = () => {
button = document.createElement('button');
button2 = document.createElement('button');
button.setAttribute('class', 'js-delete-user-modal-button');
button.setAttribute('data-username', 'foo');
button.setAttribute('data-gl-modal-action', 'action1');
button.setAttribute('data-block-user-url', '/block');
button.setAttribute('data-delete-user-url', '/delete');
document.body.appendChild(button);
document.body.appendChild(button2);
};
const removeButtons = () => {
button.remove();
button = null;
button2.remove();
button2 = null;
};
beforeEach(() => { beforeEach(() => {
jest.spyOn(document, 'addEventListener'); createButtons();
jest.spyOn(document, 'removeEventListener'); createComponent();
}); });
afterAll(() => { afterEach(() => {
jest.restoreAllMocks(); removeButtons();
}); });
it('registers global listener on mount', () => { it('renders the modal when the button is clicked', async () => {
createComponent(); button.click();
expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
});
it('removes global listener on destroy', () => { await wrapper.vm.$nextTick();
createComponent();
wrapper.destroy(); expect(findModal().exists()).toBe(true);
expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
}); });
});
describe('click handling', () => { it('does not render the modal when a misconfigured button is clicked', async () => {
let node; button.removeAttribute('data-gl-modal-action');
button.click();
beforeEach(() => { await wrapper.vm.$nextTick();
node = document.createElement('div');
document.body.appendChild(node);
});
afterEach(() => { expect(findModal().exists()).toBe(false);
node.remove();
node = null;
}); });
it('ignores wrong clicks', () => { it('does not render the modal when a button without the selector class is clicked', async () => {
createComponent(); button2.click();
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('captures click with glModalAction', () => { await wrapper.vm.$nextTick();
createComponent();
node.dataset.glModalAction = 'action1';
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled(); expect(findModal().exists()).toBe(false);
return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' });
expect(modal.exists()).toBeTruthy();
expect(modal.vm.showWasCalled).toBeTruthy();
});
}); });
}); });
}); });
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