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 {
required: true,
type: String,
},
selector: {
required: true,
type: String,
},
},
data() {
return {
......@@ -34,22 +38,24 @@ export default {
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeDestroy() {
document.removeEventListener('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;
methods: {
handleClick(e) {
const { glModalAction: action } = e.target.dataset;
if (!action) return;
this.show(e.target.dataset);
e.preventDefault();
this.show(button.dataset);
});
});
},
methods: {
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