Commit 6382b1ea authored by Zack Cuddy's avatar Zack Cuddy

Refactor Confirm Modal

Currently we had overly compllicated logic
between the JS hook and Vue component for
firing the modal.

This MR simplifies that logic by moving the DOM
listeners into the Vue component itself.

This way we no longer need to do prop management
from the JS hooks and instead just have all our
logic of showing, hiding, updating, and using
the modal in one place.
parent 598a7656
...@@ -3,40 +3,9 @@ import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; ...@@ -3,40 +3,9 @@ import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
const mountConfirmModal = () => { const mountConfirmModal = () => {
return new Vue({ return new Vue({
data() {
return {
path: '',
method: '',
modalAttributes: null,
showModal: false,
};
},
mounted() {
document.querySelectorAll('.js-confirm-modal-button').forEach(button => {
button.addEventListener('click', e => {
e.preventDefault();
this.path = button.dataset.path;
this.method = button.dataset.method;
this.modalAttributes = JSON.parse(button.dataset.modalAttributes);
this.showModal = true;
});
});
},
methods: {
dismiss() {
this.showModal = false;
},
},
render(h) { render(h) {
return h(ConfirmModal, { return h(ConfirmModal, {
props: { props: { selector: '.js-confirm-modal-button' },
path: this.path,
method: this.method,
modalAttributes: this.modalAttributes,
showModal: this.showModal,
},
on: { dismiss: this.dismiss },
}); });
}, },
}).$mount(); }).$mount();
......
<script> <script>
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import { uniqueId } from 'lodash';
export default { export default {
components: { components: {
GlModal, GlModal,
}, },
props: { props: {
modalAttributes: { selector: {
type: Object,
required: false,
default: () => {
return {};
},
},
path: {
type: String,
required: false,
default: '',
},
method: {
type: String, type: String,
required: false, required: true,
default: '',
},
showModal: {
type: Boolean,
required: false,
default: false,
}, },
}, },
watch: { data() {
showModal(val) { return {
if (val) { modalId: uniqueId('confirm-modal-'),
// Wait for v-if to render path: '',
this.$nextTick(() => { method: '',
this.openModal(); modalAttributes: {},
}); };
} },
}, mounted() {
document.querySelectorAll(this.selector).forEach(button => {
button.addEventListener('click', e => {
e.preventDefault();
this.path = button.dataset.path;
this.method = button.dataset.method;
this.modalAttributes = JSON.parse(button.dataset.modalAttributes);
this.openModal();
});
});
}, },
methods: { methods: {
openModal() { openModal() {
this.$refs.modal.show(); this.$refs.modal.show();
}, },
closeModal() {
this.$refs.modal.hide();
},
submitModal() { submitModal() {
this.$refs.form.submit(); this.$refs.form.submit();
}, },
...@@ -54,11 +50,11 @@ export default { ...@@ -54,11 +50,11 @@ export default {
<template> <template>
<gl-modal <gl-modal
v-if="showModal"
ref="modal" ref="modal"
:modal-id="modalId"
v-bind="modalAttributes" v-bind="modalAttributes"
@primary="submitModal" @primary="submitModal"
@canceled="$emit('dismiss')" @cancel="closeModal"
> >
<form ref="form" :action="path" method="post"> <form ref="form" :action="path" method="post">
<!-- Rails workaround for <form method="delete" /> <!-- Rails workaround for <form method="delete" />
......
...@@ -159,7 +159,6 @@ module EE ...@@ -159,7 +159,6 @@ module EE
path: path, path: path,
method: 'delete', method: 'delete',
modal_attributes: { modal_attributes: {
modalId: 'geo-entry-removal-modal',
title: s_('Geo|Remove tracking database entry'), title: s_('Geo|Remove tracking database entry'),
message: s_('Geo|Tracking database entry will be removed. Are you sure?'), message: s_('Geo|Tracking database entry will be removed. Are you sure?'),
okVariant: 'danger', okVariant: 'danger',
......
...@@ -8,7 +8,6 @@ describe('ConfirmModal', () => { ...@@ -8,7 +8,6 @@ describe('ConfirmModal', () => {
path: `${TEST_HOST}/1`, path: `${TEST_HOST}/1`,
method: 'delete', method: 'delete',
modalAttributes: { modalAttributes: {
modalId: 'geo-entry-removal-modal',
title: 'Remove tracking database entry', title: 'Remove tracking database entry',
message: 'Tracking database entry will be removed. Are you sure?', message: 'Tracking database entry will be removed. Are you sure?',
okVariant: 'danger', okVariant: 'danger',
...@@ -19,7 +18,6 @@ describe('ConfirmModal', () => { ...@@ -19,7 +18,6 @@ describe('ConfirmModal', () => {
path: `${TEST_HOST}/1`, path: `${TEST_HOST}/1`,
method: 'post', method: 'post',
modalAttributes: { modalAttributes: {
modalId: 'geo-entry-removal-modal',
title: 'Update tracking database entry', title: 'Update tracking database entry',
message: 'Tracking database entry will be updated. Are you sure?', message: 'Tracking database entry will be updated. Are you sure?',
okVariant: 'success', okVariant: 'success',
...@@ -53,6 +51,7 @@ describe('ConfirmModal', () => { ...@@ -53,6 +51,7 @@ describe('ConfirmModal', () => {
const findModalOkButton = (modal, variant) => const findModalOkButton = (modal, variant) =>
modal.querySelector(`.modal-footer .btn-${variant}`); modal.querySelector(`.modal-footer .btn-${variant}`);
const findModalCancelButton = modal => modal.querySelector('.modal-footer .btn-secondary'); const findModalCancelButton = modal => modal.querySelector('.modal-footer .btn-secondary');
const modalIsHidden = () => findModal().getAttribute('aria-hidden') === 'true';
const serializeModal = (modal, buttonIndex) => { const serializeModal = (modal, buttonIndex) => {
const { modalAttributes } = buttons[buttonIndex]; const { modalAttributes } = buttons[buttonIndex];
...@@ -61,7 +60,6 @@ describe('ConfirmModal', () => { ...@@ -61,7 +60,6 @@ describe('ConfirmModal', () => {
path: modal.querySelector('form').action, path: modal.querySelector('form').action,
method: modal.querySelector('input[name="_method"]').value, method: modal.querySelector('input[name="_method"]').value,
modalAttributes: { modalAttributes: {
modalId: modal.id,
title: modal.querySelector('.modal-title').innerHTML, title: modal.querySelector('.modal-title').innerHTML,
message: modal.querySelector('.modal-body div').innerHTML, message: modal.querySelector('.modal-body div').innerHTML,
okVariant: [...findModalOkButton(modal, modalAttributes.okVariant).classList] okVariant: [...findModalOkButton(modal, modalAttributes.okVariant).classList]
...@@ -92,6 +90,7 @@ describe('ConfirmModal', () => { ...@@ -92,6 +90,7 @@ describe('ConfirmModal', () => {
describe('GlModal', () => { describe('GlModal', () => {
it('is rendered', () => { it('is rendered', () => {
expect(findModal()).toExist(); expect(findModal()).toExist();
expect(modalIsHidden()).toBe(false);
}); });
describe('Cancel Button', () => { describe('Cancel Button', () => {
...@@ -102,7 +101,7 @@ describe('ConfirmModal', () => { ...@@ -102,7 +101,7 @@ describe('ConfirmModal', () => {
}); });
it('closes the modal', () => { it('closes the modal', () => {
expect(findModal()).not.toExist(); expect(modalIsHidden()).toBe(true);
}); });
}); });
}); });
......
...@@ -6,11 +6,10 @@ import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; ...@@ -6,11 +6,10 @@ import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' })); jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
describe('vue_shared/components/confirm_modal', () => { describe('vue_shared/components/confirm_modal', () => {
const testModalProps = { const MOCK_MODAL_DATA = {
path: `${TEST_HOST}/1`, path: `${TEST_HOST}/1`,
method: 'delete', method: 'delete',
modalAttributes: { modalAttributes: {
modalId: 'test-confirm-modal',
title: 'Are you sure?', title: 'Are you sure?',
message: 'This will remove item 1', message: 'This will remove item 1',
okVariant: 'danger', okVariant: 'danger',
...@@ -18,8 +17,13 @@ describe('vue_shared/components/confirm_modal', () => { ...@@ -18,8 +17,13 @@ describe('vue_shared/components/confirm_modal', () => {
}, },
}; };
const defaultProps = {
selector: '.test-button',
};
const actionSpies = { const actionSpies = {
openModal: jest.fn(), openModal: jest.fn(),
closeModal: jest.fn(),
}; };
let wrapper; let wrapper;
...@@ -27,7 +31,7 @@ describe('vue_shared/components/confirm_modal', () => { ...@@ -27,7 +31,7 @@ describe('vue_shared/components/confirm_modal', () => {
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(ConfirmModal, { wrapper = shallowMount(ConfirmModal, {
propsData: { propsData: {
...testModalProps, ...defaultProps,
...props, ...props,
}, },
methods: { methods: {
...@@ -48,28 +52,18 @@ describe('vue_shared/components/confirm_modal', () => { ...@@ -48,28 +52,18 @@ describe('vue_shared/components/confirm_modal', () => {
.wrappers.map(x => ({ name: x.attributes('name'), value: x.attributes('value') })); .wrappers.map(x => ({ name: x.attributes('name'), value: x.attributes('value') }));
describe('template', () => { describe('template', () => {
describe('when showModal is false', () => { describe('when modal data is set', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
wrapper.vm.modalAttributes = MOCK_MODAL_DATA.modalAttributes;
}); });
it('does not render GlModal', () => { it('renders GlModal wtih data', () => {
expect(findModal().exists()).toBeFalsy();
});
});
describe('when showModal is true', () => {
beforeEach(() => {
createComponent({ showModal: true });
});
it('renders GlModal', () => {
expect(findModal().exists()).toBeTruthy(); expect(findModal().exists()).toBeTruthy();
expect(findModal().attributes()).toEqual( expect(findModal().attributes()).toEqual(
expect.objectContaining({ expect.objectContaining({
modalid: testModalProps.modalAttributes.modalId, oktitle: MOCK_MODAL_DATA.modalAttributes.okTitle,
oktitle: testModalProps.modalAttributes.okTitle, okvariant: MOCK_MODAL_DATA.modalAttributes.okVariant,
okvariant: testModalProps.modalAttributes.okVariant,
}), }),
); );
}); });
...@@ -77,25 +71,49 @@ describe('vue_shared/components/confirm_modal', () => { ...@@ -77,25 +71,49 @@ describe('vue_shared/components/confirm_modal', () => {
}); });
describe('methods', () => { describe('methods', () => {
beforeEach(() => { describe('submitModal', () => {
createComponent({ showModal: true }); beforeEach(() => {
}); createComponent();
wrapper.vm.path = MOCK_MODAL_DATA.path;
wrapper.vm.method = MOCK_MODAL_DATA.method;
});
it('does not submit form', () => {
expect(findForm().element.submit).not.toHaveBeenCalled();
});
describe('when modal submitted', () => {
beforeEach(() => {
findModal().vm.$emit('primary');
});
it('does not submit form', () => { it('submits form', () => {
expect(findForm().element.submit).not.toHaveBeenCalled(); expect(findFormData()).toEqual([
{ name: '_method', value: MOCK_MODAL_DATA.method },
{ name: 'authenticity_token', value: 'test-csrf-token' },
]);
expect(findForm().element.submit).toHaveBeenCalled();
});
});
}); });
describe('when modal submitted', () => { describe('closeModal', () => {
beforeEach(() => { beforeEach(() => {
findModal().vm.$emit('primary'); createComponent();
}); });
it('submits form', () => { it('does not close modal', () => {
expect(findFormData()).toEqual([ expect(actionSpies.closeModal).not.toHaveBeenCalled();
{ name: '_method', value: testModalProps.method }, });
{ name: 'authenticity_token', value: 'test-csrf-token' },
]); describe('when modal closed', () => {
expect(findForm().element.submit).toHaveBeenCalled(); beforeEach(() => {
findModal().vm.$emit('cancel');
});
it('closes modal', () => {
expect(actionSpies.closeModal).toHaveBeenCalled();
});
}); });
}); });
}); });
......
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