Commit 1af7065b authored by Jiaan Louw's avatar Jiaan Louw Committed by Miguel Rincon

Update delete user modal manager

Update the modal manager to only listen for
clicks on mataching buttons, rather than
the entire document.
parent aaaf3ac5
......@@ -12,6 +12,10 @@ export default {
required: true,
type: String,
},
selector: {
required: true,
type: String,
},
},
data() {
return {
......@@ -34,22 +38,24 @@ export default {
},
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() {
document.removeEventListener('click', this.handleClick);
e.preventDefault();
this.show(button.dataset);
});
});
},
methods: {
handleClick(e) {
const { glModalAction: action } = e.target.dataset;
if (!action) return;
this.show(e.target.dataset);
e.preventDefault();
},
show(modalData) {
const { glModalAction: requestedAction } = modalData;
......
......@@ -7,6 +7,7 @@ import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
import initTabs from '~/admin/users/tabs';
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_MANAGER_SELECTOR = '#js-delete-user-modal';
......@@ -50,6 +51,7 @@ document.addEventListener('DOMContentLoaded', () => {
return h(ModalManager, {
ref: 'manager',
props: {
selector: CONFIRM_DELETE_BUTTON_SELECTOR,
modalConfiguration,
csrfToken: csrf.token,
},
......
......@@ -59,13 +59,13 @@
%li.divider
- if user.can_be_removed?
%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),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Delete user')
%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),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name) } }
......
......@@ -205,7 +205,7 @@
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%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),
block_user_url: block_admin_user_path(@user),
username: sanitize_name(@user.name) } }
......@@ -235,7 +235,7 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%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),
block_user_url: block_admin_user_path(@user),
username: @user.name } }
......
......@@ -3,6 +3,8 @@ import UserModalManager from '~/pages/admin/users/components/user_modal_manager.
import ModalStub from './stubs/modal_stub';
describe('Users admin page Modal Manager', () => {
let wrapper;
const modalConfiguration = {
action1: {
title: 'action1',
......@@ -14,11 +16,12 @@ describe('Users admin page Modal Manager', () => {
},
};
let wrapper;
const findModal = () => wrapper.find({ ref: 'modal' });
const createComponent = (props = {}) => {
wrapper = mount(UserModalManager, {
propsData: {
selector: '.js-delete-user-modal-button',
modalConfiguration,
csrfToken: 'dummyCSRF',
...props,
......@@ -37,7 +40,7 @@ describe('Users admin page Modal Manager', () => {
describe('render behavior', () => {
it('does not renders modal when initialized', () => {
createComponent();
expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy();
expect(findModal().exists()).toBeFalsy();
});
it('throws if action has no proper configuration', () => {
......@@ -55,7 +58,7 @@ describe('Users admin page Modal Manager', () => {
});
return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' });
const modal = findModal();
expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
......@@ -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(() => {
jest.spyOn(document, 'addEventListener');
jest.spyOn(document, 'removeEventListener');
createButtons();
createComponent();
});
afterAll(() => {
jest.restoreAllMocks();
afterEach(() => {
removeButtons();
});
it('registers global listener on mount', () => {
createComponent();
expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
});
it('renders the modal when the button is clicked', async () => {
button.click();
it('removes global listener on destroy', () => {
createComponent();
wrapper.destroy();
expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
await wrapper.vm.$nextTick();
expect(findModal().exists()).toBe(true);
});
});
describe('click handling', () => {
let node;
it('does not render the modal when a misconfigured button is clicked', async () => {
button.removeAttribute('data-gl-modal-action');
button.click();
beforeEach(() => {
node = document.createElement('div');
document.body.appendChild(node);
});
await wrapper.vm.$nextTick();
afterEach(() => {
node.remove();
node = null;
expect(findModal().exists()).toBe(false);
});
it('ignores wrong clicks', () => {
createComponent();
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('does not render the modal when a button without the selector class is clicked', async () => {
button2.click();
it('captures click with glModalAction', () => {
createComponent();
node.dataset.glModalAction = 'action1';
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
await wrapper.vm.$nextTick();
expect(event.preventDefault).toHaveBeenCalled();
return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' });
expect(modal.exists()).toBeTruthy();
expect(modal.vm.showWasCalled).toBeTruthy();
});
expect(findModal().exists()).toBe(false);
});
});
});
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